Files
pay/guis/mobile/UnlockWidget.qml
2026-05-05 19:24:42 +02:00

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.FullKeyboard)
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 {} }
}
}