Files
pay/guis/desktop/SendTransactionPane.qml
T
tomFlowee 7e60f1fdea Refactor Intent: disconnect from app lifetime
The idea of using Flowee Pay to open a payment screen, or a sweep
screen, was so far married to the executable lifetime due to it being
passed as a command line argument.
This does not reflect reality, on neither desktop nor on mobile as
multi-tasking is possible and we should allow that.

As a result the new object "Intent" has been introduced with the
usecase specific properties. Setting those properties at any time
during the lifetime of the app now pushes the correct page to the
stack on mobile. Desktop is in need of more love in this department.
2024-10-27 21:54:48 +01:00

710 lines
28 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2020-2024 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.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Window
import "../Flowee" as Flowee
import "../ControlColors.js" as ControlColors
import Flowee.org.pay
Item {
id: sendPanel
focus: true
onActiveFocusChanged: {
if (!activeFocus)
return;
/*
The application can be started with a click on a payment link,
in that case the link gets made available in the following property
and we start a payment protocol with the value.
Afterwards we reset the property to avoid the next opening of this
screen repeating the payment.
*/
var paymentProtcolUrl = Intent.paymentUrl;
if (paymentProtcolUrl !== "") {
payment.targetAddress = paymentProtcolUrl;
Intent.paymentUrl = "";
}
}
Payment { // the model behind the Payment logic
id: payment
fiatPrice: Fiat.price
account: portfolio.current
}
Rectangle { // background
anchors.fill: parent
color: palette.window
}
Flickable {
id: contentArea
width: sendPanel.width - 20
y: 40
x: 10
height: parent.height - 40
contentHeight: mainColumn.height
contentWidth: width
clip: true
Column {
id: mainColumn
width: parent.width
spacing: 10
Repeater {
model: payment.details
delegate: Item {
width: mainColumn.width
height: loader.height + 6
Loader {
id: loader
width: parent.width
height: status === Loader.Ready ? item.implicitHeight : 0
sourceComponent: {
if (modelData.type === Payment.PayToAddress)
return destinationFields;
if (modelData.type === Payment.InputSelector)
return inputFields;
if (modelData.type === Payment.CommentOutput)
return opReturnInput;
return null; // should never happen
}
onLoaded: item.paymentDetail = modelData
}
Rectangle {
id: deleteDetailButton
anchors.right: parent.right
anchors.rightMargin: 10
y: -3
width: 32
height: 32
visible: modelData.collapsable && !modelData.collapsed
color: mouseArea.containsMouse ? palette.button : palette.window
border.color: palette.button
Image {
source: "qrc:/edit-delete.svg"
width: 24
height: 24
anchors.centerIn: parent
}
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.margins: -3
cursorShape: Qt.ArrowCursor
hoverEnabled: true
onClicked: okCancelDiag.visible = true;
}
}
Flowee.Dialog {
id: okCancelDiag
onAccepted: payment.remove(modelData);
title: qsTr("Confirm delete")
text: qsTr("Do you really want to delete this detail?")
}
}
}
RowLayout {
width: parent.width
spacing: 0
Flowee.Button {
text: qsTr("Add Destination")
onClicked: payment.addExtraOutput();
}
Item { Layout.fillWidth: true }
Flowee.Button {
id: prepareButton
text: qsTr("Prepare")
enabled: payment.isValid
property QtObject portfolioUsed: null
onClicked: {
portfolioUsed = portfolio.current
if (payment.walletNeedsPin) {
passwdDialog.start()
} else {
payment.prepare();
}
}
}
Flowee.PasswdDialog {
id: passwdDialog
title: qsTr("Enter your PIN")
onAccepted: {
payment.decrypt(pwd);
if (payment.error === "")
payment.prepare();
}
}
Flowee.Dialog {
title: qsTr("Warning")
standardButtons: QQC2.DialogButtonBox.Ok
text: {
var warnings = payment.warnings
if (warnings.length === 0)
return "";
return qsTr("Payment request warnings:")
+ "\n" + warnings.join("\n");
}
visible: text !== ""
onAccepted: payment.clearWarnings();
}
}
Flowee.WarningLabel {
id: warningLabel
width: parent.width
text: payment.error
color: txid.color // make sure this is 'disabled' when the warning is not for this wallet.
}
Flowee.GroupBox {
id: txDetails
Layout.columnSpan: 4
title: qsTr("Transaction Details")
width: parent.width
GridLayout {
columns: 2
property bool txOk: payment.txPrepared
Flowee.Label {
// no need translating this one.
text: "TxId:"
Layout.alignment: Qt.AlignRight | Qt.AlignTop
}
Flowee.LabelWithClipboard {
id: txid
text: payment.txid === "" ? qsTr("Not prepared yet") : payment.txid
Layout.fillWidth: true
// Change the color when the portfolio changed since 'prepare' was clicked.
color: prepareButton.portfolioUsed === portfolio.current
? palette.windowText
: Qt.darker(palette.windowText, (Pay.useDarkSkin ? 1.6 : 0.4))
menuText: qsTr("Copy transaction-ID")
}
Flowee.Label {
text: qsTr("Fee") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.BitcoinAmountLabel {
value: !parent.txOk ? 0 : payment.assignedFee
colorize: false
color: txid.color
}
Flowee.Label {
text: qsTr("Transaction size") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.Label {
text: {
if (!parent.txOk)
return "";
var rc = payment.txSize;
return qsTr("%1 bytes").arg(rc)
}
color: txid.color
}
Flowee.Label {
text: qsTr("Fee per byte") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.Label {
text: {
if (!parent.txOk)
return "";
var rc = payment.assignedFee / payment.txSize;
var fee = rc.toFixed(3); // no more than 3 numbers behind the separator
fee = (fee * 1.0).toString(); // remove trailing zero's (1.000 => 1)
return qsTr("%1 sat/byte", "fee").arg(fee);
}
color: txid.color
}
}
}
Flowee.DialogButtonBox {
id: box
width: parent.width
standardButtons: QQC2.DialogButtonBox.Cancel | QQC2.DialogButtonBox.Ok
onStandardButtonsChanged: {
var okButton = standardButton(QQC2.DialogButtonBox.Ok)
if (okButton !== null) {
okButton.text = qsTr("Send")
okButton.enabled = canSend
}
}
property bool canSend: payment.isValid && payment.txPrepared
&& prepareButton.portfolioUsed === portfolio.current; // also make sure we prepared for the current portfolio.
onCanSendChanged: setEnabled(QQC2.DialogButtonBox.Ok, canSend)
onRejected: payment.reset();
onAccepted: {
payment.markUserApproved();
broadcastFeedback.start();
}
}
}
}
// the panel that allows us to tweak the payment (add details)
PaymentTweakingPanel {
anchors.fill: parent
}
Keys.forwardTo: Flowee.ListViewKeyHandler {
id: listViewKeyHandler
}
Flowee.BroadcastFeedback {
id: broadcastFeedback
anchors.fill: parent
}
// ============= Payment components ===============
/*
* Destination.
* The payment-output (address based) component.
*/
Component {
id: destinationFields
Flowee.GroupBox {
id: destinationPane
property QtObject paymentDetail: null
collapsable: paymentDetail.collapsable
onEffectiveCollapsedChanged: paymentDetail.collapsed = effectiveCollapsed
collapsed: paymentDetail.collapsed
title: qsTr("Destination")
summary: {
var ad = paymentDetail.address
if (ad === "")
ad = "\'\'";
if (paymentDetail.fiatFollows) {
if (paymentDetail.maxSelected)
var amount = qsTr("Max available", "The maximum balance available")
else
amount = Pay.amountToStringPretty(paymentDetail.paymentAmount)
+ " " + Pay.unitName;
}
else {
amount = Fiat.formattedPrice(paymentDetail.paymentAmountFiat)
}
return qsTr("%1 to %2", "summary text to pay X-euro to address M")
.arg(amount).arg(ad);
}
Item {
implicitWidth: parent.width
implicitHeight: Math.max(contentColumn.height, warningArea.visible ? warningArea.height : 0)
ColumnLayout {
id: contentColumn
width: parent.width
RowLayout {
width: parent.width
Flowee.TextField {
id: destination
focus: true
property var addressType: Pay.identifyString(text);
Layout.fillWidth: true
Layout.columnSpan: 3
placeholderText: enabled ? qsTr("Enter Bitcoin Cash Address") : ""
text: destinationPane.paymentDetail.address
onTextChanged: {
destinationPane.paymentDetail.address = text
addressInfo.createInfo();
}
color: {
if (!activeFocus && text !== "" && !addressInfo.addressOk)
return mainWindow.errorRed
return palette.windowText
}
enabled: paymentDetail.editable
}
Flowee.Label {
color: "green"
font.pixelSize: 24
text: addressInfo.addressOk ? "✔" : " "
}
}
Flowee.LabelWithClipboard {
visible: paymentDetail.address !== text
Layout.fillWidth: true
text: paymentDetail.formattedTarget
horizontalAlignment: Qt.AlignRight
font.italic: true
menuText: qsTr("Copy Address")
}
Flowee.AddressInfoWidget {
id: addressInfo
width: parent.width
addressType: destination.addressType
}
Flowee.Label {
id: payAmount
text: qsTr("Amount") + ":"
}
RowLayout {
Flowee.FiatValueField {
id: fiatValueField
visible: Fiat.price > 0
onValueEdited: destinationPane.paymentDetail.paymentAmountFiat = value
value: destinationPane.paymentDetail.paymentAmountFiat
}
Flowee.CheckBox {
id: amountSelector
sliderOnIndicator: false
visible: Fiat.price > 0
enabled: false
checked: destinationPane.paymentDetail.fiatFollows
}
Flowee.BitcoinValueField {
id: bitcoinValueField
value: destinationPane.paymentDetail.paymentAmount
onValueEdited: destinationPane.paymentDetail.paymentAmount = value
}
Flowee.Button {
id: sendAll
visible: destinationPane.paymentDetail.maxAllowed
text: qsTr("Max")
checkable: true
checked: destinationPane.paymentDetail.maxSelected
onClicked: destinationPane.paymentDetail.maxSelected = checked
}
}
}
Item {
id: warningArea
// BTC address entered warning.
visible: (destination.addressType === Wallet.LegacySH || destination.addressType === Wallet.LegacyPKH)
&& paymentDetail.forceLegacyOk === false;
width: parent.width - 40
height: warningColumn.height + 20 + destination.height
Rectangle {
anchors.fill: warningColumn
anchors.margins: -7
color: palette.window
border.width: 2
border.color: mainWindow.errorRed
radius: 10
}
Flowee.ArrowPoint {
x: 20
anchors.bottom: warningColumn.top
anchors.bottomMargin: 4
rotation: -90
color: mainWindow.errorRed
}
Column {
id: warningColumn
x: 10
y: destination.height + 10
width: parent.width
spacing: 10
Flowee.Label {
font.bold: true
font.pixelSize: warning.font.pixelSize * 1.2
text: qsTr("Warning")
}
Flowee.Label {
id: warning
width: parent.width
text: qsTr("This is a BTC address, which is an incompatible coin. Your funds could get lost and Flowee will have no way to recover them. Are you sure this is the right address?")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
RowLayout {
width: parent.width
Item {
width: 1; height: 1
Layout.fillWidth: true
}
Flowee.Button {
text: qsTr("Continue")
onClicked: paymentDetail.forceLegacyOk = true
}
Flowee.Button {
text: qsTr("Cancel")
onClicked: destination.text = ""
}
}
}
}
}
}
}
/*
* The input selector component.
*/
Component {
id: inputFields
Flowee.GroupBox {
id: inputsPane
collapsable: paymentDetail.collapsable
collapsed: paymentDetail.collapsed
onEffectiveCollapsedChanged: paymentDetail.collapsed = effectiveCollapsed
property QtObject paymentDetail: null
title: qsTr("Coin Selector")
summary: qsTr("Selected %1 %2 in %3 coins", "selected 2 BCH in 5 coins", paymentDetail.selectedCount)
.arg(Pay.amountToStringPretty(paymentDetail.selectedValue))
.arg(Pay.unitName)
.arg(paymentDetail.selectedCount)
// make this tabs arrow-navigation go to our coinsListView.
Component.onCompleted: listViewKeyHandler.target = coinsListView
columns: 4
Flowee.Label {
text: qsTr("Total", "Number of coins") + ":"
}
Flowee.Label {
text: coinsListView.count
Layout.fillWidth: true
}
Flowee.Label {
text: qsTr("Needed") +":"
}
Flowee.BitcoinAmountLabel {
id: neededAmountLabel
value: payment.paymentAmount
Layout.fillWidth: true
colorize: false
}
// next row
Flowee.Label {
text: qsTr("Selected") + ":"
}
Flowee.Label {
text: inputsPane.paymentDetail.selectedCount
Layout.fillWidth: true
}
Flowee.Label {
text: qsTr("Value") + ":"
}
Flowee.BitcoinAmountLabel {
value: inputsPane.paymentDetail.selectedValue
Layout.fillWidth: true
colorize: false
}
// next row
ListView {
id: coinsListView
clip: true
Layout.columnSpan: 4
Layout.fillWidth: true
implicitHeight: {
var ch = contentHeight
var suggested = contentArea.height * 0.7
if (ch < 0 || suggested < ch)
return suggested
return ch
}
model: inputsPane.paymentDetail.coins
property bool menuIsOpen: false
delegate: Rectangle {
width: ListView.view.width - 5
height: mainText.height + ageLabel.height + 12
color: index %2 == 0 ? palette.alternateBase : palette.base
Rectangle {
id: lockedRect
color: Pay.useDarkSkin ? "#002558" : "#1a6ae2"
anchors.fill: parent
visible: locked // if the UTXO is user-locked
QQC2.ToolTip {
delay: 600
text: qsTr("Locked coins will never be used for payments. Right-click for menu.")
visible: model.locked && rowMouseArea.containsMouse
}
}
Flowee.CheckBox {
y: 6
id: selectedBox
checked: model.selected
visible: !lockedRect.visible
enabled: visible
}
Flowee.Label {
id: mainText
y: 6
anchors.right: amountLabel.left
anchors.left: parent.left
anchors.leftMargin: 50
text: {
var fancy = cloakedAddress;
if (typeof fancy != "undefined")
return fancy;
return address;
}
elide: Flowee.Label.ElideRight
}
Flowee.BitcoinAmountLabel {
id: amountLabel
value: model.value
anchors.baseline: mainText.baseline
anchors.right: cfIcon.visible ? cfIcon.left : parent.right
// only HD wallets can use this
anchors.rightMargin: portfolio.current.isHDWallet ? 30 : 0
}
Flowee.Label {
id: ageLabel
text: qsTr("Age") + ": " + age
anchors.left: mainText.left
anchors.top: mainText.bottom
font.pixelSize: mainText.font.pixelSize * 0.8
}
MouseArea {
id: rowMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: locked
onClicked: (mouse)=> {
// make it easy for the user to close a menu with either mouse
// button without instantly triggering another action.
if (coinsListView.menuIsOpen) {
coinsListView.menuIsOpen = false
return;
}
if (mouse.button === Qt.LeftButton) {
if (!model.locked) {
var willCheck = !selectedBox.checked
selectedBox.checked = willCheck
inputsPane.paymentDetail.setRowIncluded(index, willCheck)
}
}
else {
coinsListView.menuIsOpen = true
// Make sure that the menu
// opens where we clicked.
mousePos.x = mouse.x
mousePos.y = mouse.y
lockingMenu.open();
}
}
Item {
id: mousePos
width: 1; height: 1
QQC2.Menu {
id: lockingMenu
QQC2.MenuItem {
text: selectedBox.checked ? qsTr("Unselect All") : qsTr("Select All")
onClicked: {
coinsListView.menuIsOpen = false
if (selectedBox.checked)
inputsPane.paymentDetail.unselectAll();
else
inputsPane.paymentDetail.selectAll();
}
}
QQC2.MenuItem {
text: locked ? qsTr("Unlock coin") : qsTr("Lock coin")
onClicked: {
inputsPane.paymentDetail.setOutputLocked(index, !locked)
coinsListView.menuIsOpen = false
}
}
QQC2.MenuItem {
text: qsTr("Copy Address")
onClicked: Pay.copyToClipboard(Pay.chainPrefix + address);
}
}
}
}
Flowee.CFIcon {
id: cfIcon
anchors.right: parent.right
anchors.verticalCenter: mainText.verticalCenter
visible: model.fused
}
}
}
}
}
/*
* Op-Return comment / data field.
*/
Component {
id: opReturnInput
Flowee.GroupBox {
property QtObject paymentDetail: null
collapsable: paymentDetail.collapsable
onEffectiveCollapsedChanged: paymentDetail.collapsed = effectiveCollapsed
collapsed: paymentDetail.collapsed
title: qsTr("Public-comment")
columns: 2
Flowee.Label {
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.columnSpan: 2
text: {
if (paymentDetail.editable) // user-created.
return qsTr("Add a comment you want to include in the transaction, visible for everyone.")
return qsTr("Custom message, to be included in the transaction.")
}
font.italic: true
}
Flowee.Label {
text: qsTr("Text") + ":"
}
Flowee.TextField {
id: textData
Layout.fillWidth: true
text: paymentDetail.editable ? paymentDetail.commentString : paymentDetail.preview
enabled: paymentDetail.editable
onTextChanged: if (paymentDetail.editable) paymentDetail.commentString = text
}
Flowee.Label {
text: qsTr("Size")+ ":"
}
Flowee.Label {
text: paymentDetail.size
}
}
}
}