Files
pay/guis/mobile/PayWithQR.qml
2026-03-14 21:14:42 +01:00

476 lines
16 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2022-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 QtQuick.Layouts
import QtQuick.Controls.Basic as QQC2
import "../Flowee" as Flowee
import Flowee.org.pay
Page {
id: root
headerText: qsTr("Approve Payment")
property QtObject sendAllAction: QQC2.Action {
checkable: true
checked: payment.details[0].maxSelected
text: qsTr("Select All", "all money in wallet")
onTriggered: {
payment.details[0].maxSelected = checked
if (payment.isValid)
payment.prepare() // auto-prepare doesn't act on changes done on the details.
}
}
property QtObject showTargetAddress: QQC2.Action {
checkable: true
checked: true
text: qsTr("Show Address", "to show a bitcoincash address")
}
property QtObject makeRepeating: QQC2.Action {
text: qsTr("Schedule Payment")
enabled: payment.isValid
onTriggered: {
var repeatingCopy = payment.copyToRepeating(root)
repeatingCopy.payment.userComment = qsTr("Scheduled Payment") // try to reuse string from RepeatPaymentDetails.qml
thePile.push("RepeatPaymentDetails.qml", { "savedPayment": repeatingCopy,
"withScheduleButton": true,
"acceptHandler": function handler() {
var mainView = thePile.get(0)
mainView.currentIndex = 0 // go to the 'Home' tab.
thePile.pop()
thePile.pop() // remove current page too
}
})
}
}
property QtObject editPrice: QQC2.Action {
text: qsTr("Edit Amount", "Edit amount of money to send")
onTriggered: {
root.closeHeaderMenu()
// this action can only ever be used to start editing.
root.allowEditAmount = true
priceInput.fiatFollowsSats = false
priceInput.takeFocus()
}
checked: root.allowEditAmount
}
menuItems: {
// only have menu items as long as we are effectively
// showing this page and not some overlay.
var items = []
if (payment.broadcastStatus === Payment.NotStarted) {
// a QR _with_ a bch-amount will turn off editing of amount-to-send
if (allowEditAmount)
items.push(sendAllAction)
else
items.push(editPrice)
items.push(showTargetAddress)
items.push(makeRepeating)
}
return items
}
// if true, show widgets to edit the amount-to-send
property bool allowEditAmount: true
function start(paymentAddress) {
let success = payment.pasteTargetAddress(paymentAddress)
if (!success) {
scannedUrlFaultyDialog.scanResult = paymentAddress
scannedUrlFaultyDialog.open()
}
// should the price be included in the QR code, don't show editing widgets.
root.allowEditAmount = payment.paymentAmount <= 0
if (root.allowEditAmount) {
priceInput.takeFocus()
payment.instaPay = false
}
else {
root.takeFocus()
}
}
Item { // data
Payment {
id: payment
account: portfolio.current
fiatPrice: Fiat.price
autoPrepare: true
instaPay: true
onApprovedByUser: {
root.fullScreen = true
broadcastPage.start()
}
}
Flowee.Dialog {
id: scannedUrlFaultyDialog
title: qsTr("Invalid QR code")
property string scanResult: ""
standardButtons: QQC2.DialogButtonBox.Close
onRejected: thePile.pop()
contentComponent: dialogForFaultyUrl
}
Component {
id: dialogForFaultyUrl
Column {
width: parent.width
spacing: 10
Flowee.Label {
id: mainText
width: parent.width
text: qsTr("I don't understand the scanned code. I'm sorry, I can't start a payment.")
wrapMode: Text.Wrap
}
Flowee.Label {
text: qsTr("details")
font.italic: true
color: palette.link
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: detailsLabel.visible = !detailsLabel.visible
}
}
Flowee.Label {
id: detailsLabel
text: qsTr("Scanned text: <pre>%1</pre>").arg(scannedUrlFaultyDialog.scanResult)
visible: false
font.pixelSize: mainText.pixelSize * 0.8
wrapMode: Text.Wrap
width: parent.width
}
}
}
}
PriceInputWidget {
id: priceInput // when the price / amount is editable
paymentBackend: payment
width: parent.width
x: allowEditAmount ? 0 : 0 - width
fiatFollowsSats: Pay.preferSatsPayments
Behavior on x { NumberAnimation { } }
}
Item {
id: priceFeedback // when the price / amount is given by the scanned QR
width: parent.width
x: allowEditAmount ? 0 - width : 0
y: 10
height: col.height
Column {
id: col
width: parent.width
Flowee.Label {
id: fiatPrice
anchors.horizontalCenter: parent.horizontalCenter
text: Fiat.formattedPrice(payment.paymentAmountFiat)
font.pixelSize: 38
}
Flowee.BitcoinAmountLabel {
value: payment.paymentAmount
colorize: false
showFiat: false
font.pixelSize: mainWindow.font.pixelSize * 0.8
anchors.horizontalCenter: parent.horizontalCenter
}
}
Behavior on x { NumberAnimation { } }
}
Flowee.Label {
id: commentLabel
text: qsTr("Payment description")
visible: userComment.visible
y: {
if (!userComment.visible)
return 0
if (root.allowEditAmount) { // the numpad is on
/*
position based on available space and how many items
need to be visible.
If invisible, place at -100
*/
let space = walletNameBackground.y - (priceInput.y + priceInput.height)
if (destinationAddress.visible)
space -= destinationAddress.height + 10
space -= userComment.height + 10
if (space < height + 10)
return -100
return priceInput.y + priceInput.height + 10
}
return walletNameBackground.y + walletNameBackground.height + 10
}
}
Flowee.Label {
id: userComment
text: payment.userComment
visible: text !== "" && !errorLabel.visible
color: palette.highlight
font.italic: true
y: {
if (!visible)
return 0
if (commentLabel.y < 0)
return priceInput.y + priceInput.height + 10
return commentLabel.y + commentLabel.height + 6
}
}
Flowee.Label {
id: destinationAddressHeader
text: qsTr("Destination Address")
visible: destinationAddress.visible
y: {
if (!visible)
return 0
if (root.allowEditAmount) { // the numpad is on
let space = walletNameBackground.y - (priceInput.y + priceInput.height)
if (userComment.visible)
space -= userComment.height + 10
if (commentLabel.visible)
space -= commentLabel.height + 10
space -= destinationAddress.height + 10
if (space < height + 10)
return -100
if (userComment.visible)
return userComment.y + userComment.height + 10
return priceInput.y + priceInput.height + 10
}
if (userComment.visible)
return userComment.y + userComment.height + 10
return walletNameBackground.y + walletNameBackground.height + 10
}
}
Flowee.LabelWithClipboard {
id: destinationAddress
text: payment.niceAddress
width: parent.width
visible: showTargetAddress.checked && !errorLabel.visible
font.pixelSize: mainWindow.font.pixelSize * 0.9
y: {
if (!visible)
return 0
if (destinationAddressHeader.y < 0){
if (userComment.visible)
return userComment.y + userComment.height + 10
return priceInput.y + priceInput.height + 10
}
return destinationAddressHeader.y + destinationAddressHeader.height + 6
}
}
Rectangle {
visible: errorLabel.text !== ""
color: mainWindow.errorRedBg
radius: 10
width: parent.width
anchors.bottom: walletNameBackground.top
anchors.bottomMargin: 6
height: errorLabel.height + 20
Flowee.Label {
id: errorLabel
text: payment.error
wrapMode: Text.Wrap
x: 10
y: 10
width: parent.width - 20
horizontalAlignment: Qt.AlignHCenter
}
MouseArea {
anchors.fill: parent
// just here to catch mouse clicks.
}
}
AccountSelectorWidget {
id: walletNameBackground
y: {
var y = priceInput.height
if (commentLabel.visible && !root.allowEditAmount && commentLabel.y > 0)
y += commentLabel.height + userComment.height + 6 + 10
else if (userComment.visible)
y += userComment.height + 10
if (destinationAddressHeader.visible && !root.allowEditAmount && destinationAddressHeader.y > 0)
y += destinationAddressHeader.height + destinationAddress.height + 6
else if (destinationAddress.visible)
y += destinationAddress.height + 10
y += 10
var altY = parent.height
altY -= 10 + slideToApprove.height + 25
altY -= numericKeyboard.contentHeight + height + 10
return Math.max(y, altY)
}
onSelectedAccountChanged: payment.account = selectedAccount
balanceActions: {
if (editPrice.checked)
return [ sendAllAction ]
return []
}
}
NumericKeyboardWidget {
id: numericKeyboard
anchors.bottom: slideToApprove.top
anchors.bottomMargin: 15
anchors.top: walletNameBackground.bottom
width: parent.width
height: implicitHeight
enabled: !payment.details[0].maxSelected
x: allowEditAmount ? 0 : 0 - width
dataInput: priceInput
Behavior on x { NumberAnimation { } }
}
SlideToApprove {
id: slideToApprove
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
width: parent.width
enabled: payment.isValid && payment.txPrepared
onActivated: payment.markUserApproved()
visible: payment.account.isDecrypted || !payment.account.needsPinToPay
}
Flickable {
anchors.fill: parent
contentWidth: width
contentHeight: warningsColumn.implicitHeight
enabled: warningsColumn.implicitHeight > 0
Column {
id: warningsColumn
width: parent.width
Repeater {
model: payment.warnings
Rectangle {
y: 8
width: root.width - 16
height: Math.max(75, Math.max(warningIcon.height, warningText.height) + 20)
radius: 20
color: palette.alternateBase
border.width: 1
border.color: palette.midlight
Rectangle { // placeholder icon
id: warningIcon
x: 20
width: 40
height: 40
radius: 20
color: mainWindow.errorRedBg
anchors.verticalCenter: parent.verticalCenter
}
Flowee.Label {
id: warningText
text: modelData
wrapMode: Text.Wrap
anchors.left: warningIcon.right
anchors.leftMargin: 10
anchors.right: closeIcon.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
}
Flowee.CloseIcon {
id: closeIcon
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 10
onClicked: payment.clearWarnings()
}
}
}
}
}
Item {
id: decryptButton
visible: !slideToApprove.visible
anchors.fill: slideToApprove
Image {
id: lockIcon
source: "qrc:/lock" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
x: 10
y: 5
width: 50
height: 50
smooth: true
}
Flowee.Button {
height: parent.height - 10
width: parent.width - 80
x: 70
y: 5
text: qsTr("Unlock Wallet")
onClicked: thePile.push(unlockInPage)
}
Component {
id: unlockInPage
Page {
headerText: payment.account.name
UnlockWalletPanel {
anchors.fill: parent
anchors.margins: 10
account: payment.account
Connections {
target: payment.account
function onIsDecryptedChanged() {
if (payment.account.isDecrypted)
thePile.pop()
}
}
}
}
}
}
Flowee.BroadcastFeedback {
id: broadcastPage
bitcoinAmount: payment.paymentAmount
fiatPrice: payment.fiatPrice
targetAddress: payment.niceAddress
status: payment.broadcastStatus
personalNote: payment.userComment
processNote: function process(note) {
payment.userComment = note
}
onCloseButtonPressed: {
var mainView = thePile.get(0)
mainView.currentIndex = 0 // go to the 'Home' tab.
thePile.pop()
}
}
}