Files
pay/guis/mobile/UnlockWidget.qml
T

358 lines
11 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2023-2025 Tom Zander <tom@flowee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick
import "../Flowee" as Flowee
import Flowee.org.pay;
Item {
id: root
implicitWidth: 300
implicitHeight: 600
/// This will hold the password when the user is done typing it.
property string password : "";
property alias buttonText: openButton.text
property bool assumeDarkBackground: false
/// Emitted when the user submits the password
signal passwordEntered;
/// in an onPasswordEntered callback, call this method
/// to signify the password is incorrect.
/// Otherwise just close this widget, or call acceptedPassword()
function passwordIncorrect() {
shaker.start();
moveFocusTimer.start();
keyboard.flashErrorFeedback();
smallKeyboard.flashErrorFeedback();
}
/// clears the password typed, if needed.
function acceptedPassword() {
pwdField.text = "";
lockIcon.open = true;
}
function takeFocus() {
moveFocusTimer.start();
lockIcon.open = false;
}
Item {
id: passwordData
property QtObject editor: Item {
property string enteredString;
property int insertedIndex: -1
function insertNumber(character) {
if (enteredString.length >= 10)
return false;
insertedIndex = enteredString.length;
enteredString = enteredString + character;
return true;
}
function addSeparator() { return false; }
function backspacePressed() {
if (enteredString == "")
return false;
insertedIndex = -1;
enteredString = enteredString.substring(0, enteredString.length - 1);
return true;
}
function reset() {
insertedIndex = -1;
enteredString = "";
}
}
function finished() {
var pwd = editor.enteredString;
if (pwd !== "") {
root.password = pwd;
editor.reset();
root.passwordEntered();
}
}
function shake() { }
}
// we can't move focus from things like the onClicked handler of a button
// therefore a little timer that does it very briefly afterwards is used.
Timer {
id: moveFocusTimer
interval: 50
onTriggered: {
if (Pay.unlockingKeyboard === FloweePay.FullKeyboard) {
pwdField.selectAll();
pwdField.forceActiveFocus();
}
}
}
Item {
id: lockIcon
property bool open: false
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height > 700 ? 40 : 10
width: 60
height: 60
Item {
clip: true
width: 60
height: 21
rotation: lockIcon.open ? 20 : 0
transformOrigin: Item.Bottom
Image {
id: lockIconOne
source: "qrc:/lock" + (Pay.useDarkSkin || assumeDarkBackground ? "-light.svg" : ".svg");
width: 60
height: 60
}
}
Item {
clip: true
y: 21
width: 60
height: 40
rotation: lockIcon.open ? -10 : 0
transformOrigin: Item.TopRight
Image {
source: lockIconOne.source
y: -22
width: 60
height: 60
}
}
}
Image {
width: Pay.unlockingKeyboard === FloweePay.FullKeyboard ? 30 : 40
height: width
anchors.right: parent.right
source: {
var s = "qrc:/";
if (Pay.unlockingKeyboard === FloweePay.BigNumbersKeyboard)
s += "keyboard" // next one
else
s += "num-keyboard"
return s + (Pay.useDarkSkin || assumeDarkBackground ? "-light.svg" : ".svg");
}
MouseArea {
anchors.fill: parent
onClicked: {
passwordData.editor.reset();
var cur = Pay.unlockingKeyboard;
if (cur === FloweePay.BigNumbersKeyboard)
var newValue = FloweePay.FullKeyboard;
else
newValue = cur + 1;
Pay.unlockingKeyboard = newValue;
if (newValue === FloweePay.FullKeyoard)
pwdField.forceActiveFocus();
}
}
}
Flowee.Label {
id: introText
text: qsTr("Enter your wallet passcode")
anchors.top: lockIcon.bottom
anchors.topMargin: 20
horizontalAlignment: Text.AlignHCenter
width: parent.width
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
color: assumeDarkBackground ? "#fcfcfc" : palette.text
Flowee.ObjectShaker { id: shaker } // 'shake' to give feedback on mistakes
}
// Show the typed pin code, but as bullets as you type.
Row {
id: pinPreview
anchors.top: introText.bottom
anchors.topMargin: root.height > 700 ? 20 : 6
spacing: 6
anchors.horizontalCenter: parent.horizontalCenter
visible: Pay.unlockingKeyboard !== FloweePay.FullKeyboard
Flowee.Label { // for height
text: " ";
font.pixelSize: introText.font.pixelSize * 2
visible: repeater.model == 0 // 2 equals only!!
}
Repeater {
id: repeater
// take the number typed and turn it into an array of characters.
model: {
var inputString = passwordData.editor.enteredString
var answer = [];
for (let i = 0; i < inputString.length; ++i) {
answer.push(inputString.substr(i, 1));
}
return answer;
}
Rectangle {
width: 40
height: parent.height
color: !Pay.useDarkSkin && assumeDarkBackground ? palette.base : "#00000000"
Flowee.Label {
id: dot
anchors.centerIn: parent
text: {
if (index !== passwordData.editor.insertedIndex)
return "∙"
return modelData;
}
font.pixelSize: introText.font.pixelSize * 2
Timer {
interval: 1000
running: index === passwordData.editor.insertedIndex
onTriggered: dot.text = "∙"
}
}
}
}
}
Rectangle {
anchors.bottom: pinPreview.top
width: parent.width / 10 * 8
color: mainWindow.floweeGreen
height: 2
opacity: Pay.unlockingKeyboard === FloweePay.FullKeyboard ? 0 : 1
x: parent.width / 10
Behavior on opacity { NumberAnimation {} }
}
Rectangle {
anchors.top: pinPreview.bottom
width: parent.width / 10 * 8
color: mainWindow.floweeGreen
x: parent.width / 10
height: 2
opacity: Pay.unlockingKeyboard === FloweePay.FullKeyboard ? 0 : 1
Behavior on opacity { NumberAnimation {} }
}
NumericKeyboardWidget {
id: keyboard
opacity: Pay.unlockingKeyboard === FloweePay.BigNumbersKeyboard ? 1 : 0
enabled: opacity > 0.1
hasSeparator: false
width: parent.width
anchors.top: pinPreview.top
anchors.topMargin: introText.height * 2
anchors.bottom: parent.bottom
anchors.bottomMargin: 40
dataInput: passwordData
onFinished: passwordData.finished();
Behavior on opacity { NumberAnimation {} }
}
Flowee.TextField {
id: pwdField
opacity: Pay.unlockingKeyboard === FloweePay.FullKeyboard ? 1 : 0
enabled: opacity > 0.1
anchors.top: introText.bottom
anchors.topMargin: 20
width: parent.width
focus: visible
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhSensitiveDataTyped
| (hideText ? Qt.ImhHiddenTextCharacters : Qt.ImhNone)
echoMode: hideText ? TextInput.Password : TextInput.Normal
property bool hideText: true
onEditingFinished: openButton.clicked()
Image {
width: 14
height: 14
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
source: {
var state = (pwdField.hideText) ? "open" : "closed";
var skin = Pay.useDarkSkin ? "-light" : ""
return "qrc:/eye-" + state + skin + ".png";
}
MouseArea {
width: parent.width + 50 // extend to right physical edge
x: -15
y: 0 - parent.y - 5
height: pwdField.height + 10
onClicked: pwdField.hideText = !pwdField.hideText
}
}
Behavior on opacity { NumberAnimation {} }
}
Flowee.Button {
id: openButton
opacity: Pay.unlockingKeyboard === FloweePay.FullKeyboard ? 1 : 0
enabled: opacity > 0.1
anchors.right: parent.right
y: pwdField.y + pwdField.height + 20
text: qsTr("Open", "open wallet with PIN")
onClicked: {
var pwd = pwdField.text;
if (pwd !== "") {
root.password = pwd;
passwordData.editor.reset();
root.passwordEntered();
}
}
Behavior on opacity { NumberAnimation {} }
}
Rectangle {
width: parent.width + 20
x: -10
opacity: Pay.unlockingKeyboard === FloweePay.SmallNumbersKeyboard ? 1 : 0
anchors.bottom: parent.bottom
anchors.bottomMargin: -10
color: palette.base
height: 225 + Pay.screenInsets.height
Behavior on opacity { NumberAnimation {} }
}
NumericKeyboardWidget {
id: smallKeyboard
opacity: Pay.unlockingKeyboard === FloweePay.SmallNumbersKeyboard ? 1 : 0
enabled: opacity > 0.1
hasSeparator: false
width: parent.width
height: 200
anchors.bottom: parent.bottom
anchors.bottomMargin: Pay.screenInsets.height
dataInput: passwordData
onFinished: passwordData.finished();
buttonBackground: Item {
property int index: 0
property bool pressed: false
Rectangle {
anchors.fill: parent
visible: parent.index === 11
color: palette.midlight
opacity: 0.6
}
}
Behavior on opacity { NumberAnimation {} }
}
}