Files
pay/modules/build-transaction/PayToOthers.qml
T

532 lines
21 KiB
QML
Raw Permalink Normal View History

2023-03-10 22:16:37 +01:00
/*
* This file is part of the Flowee project
2025-02-04 21:20:35 +01:00
* Copyright (C) 2022-2025 Tom Zander <tom@flowee.org>
2023-03-10 22:16:37 +01:00
*
* 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/>.
*/
2023-06-12 15:26:31 +02:00
import QtQuick;
import QtQuick.Layouts;
import QtQuick.Controls as QQC2;
import "../Flowee" as Flowee;
import "../mobile";
2023-03-10 22:16:37 +01:00
import Flowee.org.pay;
2023-03-11 22:05:15 +01:00
2023-03-10 22:16:37 +01:00
Page {
id: root
headerText: qsTr("Build Transaction")
2023-03-10 22:16:37 +01:00
Item { // data and non-visible stuff for this page
/*
* Components embedded in this file:
* Editors:
destinationEditPage
editors can use the property 'paymentDetail'
* ListItems:
destinationFields
list items have the property 'edit' to link to Editors
property Component edit: [something]
list items can use the property 'paymentDetail'
* Other:
paymentDetailSelector
2023-03-13 11:52:47 +01:00
paymentInfoPage
* To open editors or list items, we use the pushToThePile() function.
*/
2023-03-10 22:16:37 +01:00
Payment {
id: payment
account: portfolio.current
fiatPrice: Fiat.price
onApprovedByUser: {
thePile.currentItem.hideHeader = true;
broadcastPage.start();
}
2023-03-10 22:16:37 +01:00
}
2023-03-13 12:53:33 +01:00
Flowee.Dialog {
id: errorDialog
standardButtons: QQC2.DialogButtonBox.Close
title: qsTr("Building Error", "error during build")
text: payment.error
}
2023-03-10 22:16:37 +01:00
// Extra page to create new details.
Component {
id: paymentDetailSelector
Page {
headerText: qsTr("Add Payment Detail", "page title")
Flickable {
anchors.fill: parent
contentHeight: col.height
Column {
id: col
width: parent.width
TextButton {
text: qsTr("Add Destination")
subtext: qsTr("an address to send money to")
2023-03-10 22:16:37 +01:00
onClicked: {
2023-03-11 22:05:15 +01:00
var detail = payment.addExtraOutput();
2023-03-10 22:16:37 +01:00
thePile.pop();
pushToThePile(destinationEditPage,
detail);
2023-03-10 22:16:37 +01:00
}
}
}
}
}
}
2023-03-13 11:52:47 +01:00
// show info about payment and allow broadcast
Component {
id: paymentInfoPage
Page {
id: root
headerText: qsTr("Confirm Sending", "confirm we want to send the transaction")
Flickable {
anchors.top: parent.top
anchors.bottom: slideToApprove.top
width: parent.width
contentHeight: col.implicitHeight
clip: true
Column {
id: col
width: parent.width
spacing: 10
// the below is all very technical stuff that most people don't care about.
// it was easy, but its not useful.
// So, TODO insert here actually useful to end users details like amount sent,
// change address used. Etc.
Flowee.Label {
text: qsTr("TXID") + ":"
}
Flowee.LabelWithClipboard {
id: txid
text: payment.txid
font.pixelSize: root.font.pixelSize * 0.8
width: parent.width
// Change the color when the portfolio changed since 'prepare' was clicked.
menuText: qsTr("Copy transaction-ID")
}
Flowee.Label {
text: qsTr("Fee") + ":"
}
Flowee.BitcoinAmountLabel {
value: payment.assignedFee
colorize: false
}
Flowee.Label {
text: qsTr("Transaction size") + ":"
}
Flowee.Label {
text: qsTr("%1 bytes").arg(payment.txSize)
}
Flowee.Label {
text: qsTr("Fee per byte") + ":"
}
Flowee.Label {
text: {
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);
}
}
}
}
SlideToApprove {
id: slideToApprove
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
width: parent.width
2023-07-07 09:41:04 +02:00
onActivated: {
2023-09-06 21:07:30 +02:00
payment.markUserApproved()
2023-07-07 09:41:04 +02:00
thePile.pop(); // the broadcast feedback is on the main screen.
2023-03-13 11:52:47 +01:00
}
}
}
}
2023-03-10 22:16:37 +01:00
// listitem of PaymentDetail showing payment-destination
Component {
id: destinationFields
Column {
property Component edit: destinationEditPage
width: parent.width
spacing: 6
/* This page just shows the results, editing is done
* on its own page.
*/
2023-05-31 15:47:14 +02:00
PageTitledBox {
2023-03-10 22:16:37 +01:00
width: parent.width
2023-05-31 15:47:14 +02:00
title: qsTr("Destination")
Flowee.LabelWithClipboard {
width: parent.width
font.italic: paymentDetail.address === ""
text: {
var s = paymentDetail.niceAddress
2023-06-04 19:20:18 +02:00
if (s === "") {
if (paymentDetail.address === "") // the user-specified text is empty
2024-10-14 12:55:51 +02:00
return qsTr("unset", "indication of desination not being set");
2023-06-04 19:20:18 +02:00
return qsTr("invalid", "address is not correct");
}
2023-05-31 15:47:14 +02:00
return s;
}
2023-05-31 15:47:14 +02:00
color: addressInfo.addressOk || paymentDetail.address === ""
? palette.windowText : mainWindow.errorRed
font.pixelSize: mainWindow.font.pixelSize * 0.9
menuText: qsTr("Copy Address")
clipboardText: paymentDetail.formattedTarget // the one WITH bitcoincash:
}
Flowee.AddressInfoWidget {
id: addressInfo
width: parent.width
addressType: Pay.identifyString(paymentDetail.address);
}
Flowee.BitcoinAmountLabel {
value: paymentDetail.paymentAmount
colorize: false
}
2023-03-10 22:16:37 +01:00
}
}
}
/*
* The different payment things work with a 'paymentDetail'
* and since we push those into the global stack, they get
* loaded and initialized first, only secondly we set the
* property paymentDetail on them.
*
* This means we either get a load of errors dereferencing a null
* object, or we need to alter a lot of code to account for that.
*
* This is the alternative solution: we add a layer of indirection
* and set the property on the loader and the item in the loader
* will simply find the paymentDetail present in the context in
* which it has been loaded.
*/
Component {
id: loaderForPayments
FocusScope {
property alias paymentDetail: loader2.paymentDetail
property Component sourceComponent: undefined
function takeFocus() {
// this is also present in 'page', and called from thePile
forceActiveFocus();
}
Loader {
property QtObject paymentDetail: null
id: loader2
anchors.fill: parent
onLoaded: item.takeFocus();
}
function load() {
if (paymentDetail != null && typeof sourceComponent != "undefined") {
loader2.sourceComponent = sourceComponent
}
}
onPaymentDetailChanged: load();
onSourceComponentChanged: load();
}
}
2023-03-10 22:16:37 +01:00
Component {
id: destinationEditPage
2025-02-04 18:07:46 +01:00
DestinationEditPage { }
2023-03-10 22:16:37 +01:00
}
/*
* A helper page that allows unlocking an account prior to paying from it.
*/
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()
}
}
}
}
}
}
// check the comment at loaderForPayments to understand this one
function pushToThePile(componentId, detail) {
thePile.push(loaderForPayments,
2024-10-01 19:57:17 +02:00
{ "paymentDetail": detail,
"sourceComponent": componentId }
);
}
2023-03-10 22:16:37 +01:00
Flickable {
id: contentArea
anchors.fill: parent
2023-03-12 20:31:37 +01:00
contentHeight: mainColumn.height + 10
2023-03-10 22:16:37 +01:00
contentWidth: width
clip: true
Column {
id: mainColumn
2023-03-12 20:31:37 +01:00
y: 10
2023-03-10 22:16:37 +01:00
width: parent.width
2024-10-01 21:14:54 +02:00
spacing: 10
2023-05-18 21:52:51 +02:00
AccountSelectorWidget {
visible: !portfolio.singleAccountSetup
}
2023-05-18 21:52:51 +02:00
VisualSeparator {
visible: !portfolio.singleAccountSetup
}
2023-03-10 22:16:37 +01:00
Repeater {
model: payment.details
delegate: Item {
id: listItem
width: mainColumn.width
height: loader.height + 20 + (index <= 1 ? (dragInstructions.height + 20) : 0)
2023-03-10 22:16:37 +01:00
Loader {
id: loader
width: parent.width
height: status === Loader.Ready ? item.implicitHeight : 0
property QtObject paymentDetail: modelData
2023-03-10 22:16:37 +01:00
sourceComponent: {
if (modelData.type === Payment.PayToAddress)
return destinationFields
if (modelData.type === Payment.InputSelector)
return inputFields
return null; // should never happen
}
}
2023-03-11 13:28:29 +01:00
Item {
// an invisible item that is used to handle the drag events.
id: dragItem
width: parent.width
height: parent.height
property bool editStarted: false
onXChanged: {
2024-10-01 19:57:17 +02:00
/* When we move, we move the icons etc with us. */
2023-03-11 13:28:29 +01:00
if (x < 0) {
leftArea.x = leftArea.width * -1 // out of screen
// moving left, opening the edit option
loader.x = x;
let a = x * -1;
editIcon.x = width - a + Math.max(0, a - editIcon.width);
if (!editStarted && x < -100) {
editStarted = true;
pushToThePile(loader.item.edit, modelData);
2023-03-11 13:28:29 +01:00
}
}
else {
editIcon.x = width;
// moving right, opening the trashcan option
let a = x;
let base = Math.min(40, a);
let rest = a - base;
deleteTimer.running = rest > 90;
if (rest < 90)
deleteTimer.deleteTriggered = false
leftBackground.opacity = Math.max(rest - 40) / 100;
rest = Math.min(60, rest) / 4; // slower
leftArea.x = leftArea.width * -1 + base + rest;
loader.x = base + rest;
}
}
DragHandler {
id: dragHandler
// property bool editStarted: false;
yAxis.enabled: false
xAxis.enabled: true
xAxis.maximum: index > 0 ? 200 : 0 // swipe left
2023-03-11 13:28:29 +01:00
xAxis.minimum: -140 // swipe right
onActiveChanged: {
if (active) {
parent.editStarted = false;
return;
}
// take action on user releasing the drag.
if (deleteTimer.deleteTriggered) {
deleteTimer.deleteTriggered = false;
payment.remove(modelData);
}
parent.x = 0;
}
}
}
2023-03-10 22:16:37 +01:00
// delete-detail interface
2023-03-11 13:28:29 +01:00
Item {
id: leftArea
2023-03-10 22:16:37 +01:00
width: 300
height: loader.height
2023-03-11 13:28:29 +01:00
x: -width;
Rectangle {
id: leftBackground
opacity: 0
anchors.fill: parent
color: mainWindow.errorRedBg
2024-10-01 19:57:17 +02:00
radius: 6
Image {
id: trashcan
source: "./recycle" + (Pay.useDarkSkin ? "-light" : "") + ".svg"
width: 40
height: 40
y: 10
x: {
let newX = parent.width - width;
let moved = parent.x + parent.width;
let additional = Math.max(0, Math.min(moved - width, 8));
return newX - additional;
}
2023-03-10 22:16:37 +01:00
}
}
2023-03-11 13:28:29 +01:00
Timer {
/*
The intention of this timer is that the user can't just swipe hard
and suddenly lose their data.
We need to have the user actually see the red for half a second
before we delete.
*/
id: deleteTimer
interval: 400
property bool deleteTriggered: false
onTriggered: deleteTriggered = true
}
2023-03-10 22:16:37 +01:00
}
2023-03-13 16:52:29 +01:00
Image {
2023-03-10 22:16:37 +01:00
id: editIcon
width: 40
height: 40
y: 10
x: {
let additional = Math.max(0, Math.min(listItem.x * -1 - width, 16))
return parent.width + additional;
}
2023-06-12 15:26:31 +02:00
source: "./edit" + (Pay.useDarkSkin ? "-light" : "") + ".svg"
2023-03-13 16:52:29 +01:00
smooth: true
2023-03-10 22:16:37 +01:00
}
// UX help
Row {
id: row
anchors.bottom: parent.bottom
anchors.bottomMargin: 26
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
visible: index <= 1
property bool dragToEdit: index === 0
Repeater {
model: 4
2023-03-13 13:33:45 +01:00
delegate: Flowee.ArrowPoint {
color: palette.highlight
anchors.verticalCenter: dragInstructions.verticalCenter
rotation: row.dragToEdit ? 180 : 0;
}
}
Flowee.Label {
id: dragInstructions
text: index === 0 ? qsTr("Drag to Edit") : qsTr("Drag to Delete");
font.italic: true
color: palette.highlight
}
Repeater {
model: 4
2023-03-13 13:33:45 +01:00
delegate: Flowee.ArrowPoint {
color: palette.highlight
anchors.verticalCenter: dragInstructions.verticalCenter
rotation: row.dragToEdit ? 180 : 0;
}
}
}
VisualSeparator {
anchors.bottom: parent.bottom
}
2023-03-10 22:16:37 +01:00
Behavior on x { NumberAnimation { duration: 100 } }
}
}
2023-03-11 13:28:29 +01:00
TextButton {
text: qsTr("Add Destination")
pageButton: true
onClicked: pushToThePile(destinationEditPage, payment.addExtraOutput());
2023-03-11 13:28:29 +01:00
}
/*TextButton {
2023-03-10 22:16:37 +01:00
text: qsTr("Add Detail...")
pageButton: true
2023-03-10 22:16:37 +01:00
onClicked: thePile.push(paymentDetailSelector);
}*/
2023-03-13 11:52:47 +01:00
Flowee.Button {
property bool walletNeedsDecryptFirst: !payment.account.isDecrypted && payment.account.needsPinToPay;
text: walletNeedsDecryptFirst ? qsTr("Unlock Wallet") : qsTr("Prepare Payment...")
2023-03-13 11:52:47 +01:00
enabled: payment.isValid
anchors.right: parent.right
onClicked: {
if (walletNeedsDecryptFirst) {
thePile.push(unlockInPage);
return;
}
2023-03-13 11:52:47 +01:00
payment.prepare();
if (payment.error !== "") {
2023-03-13 12:53:33 +01:00
errorDialog.visible = true;
2023-03-13 11:52:47 +01:00
}
else {
thePile.push(paymentInfoPage);
}
}
}
2023-03-10 22:16:37 +01:00
}
}
2023-07-07 09:41:04 +02:00
Flowee.BroadcastFeedback {
2025-02-04 21:20:35 +01:00
id: broadcastPage
bitcoinAmount: payment.paymentAmount
fiatPrice: payment.fiatPrice
2025-02-04 22:33:08 +01:00
targetAddress: payment.niceAddress
2025-02-04 21:20:35 +01:00
status: payment.broadcastStatus
2025-02-04 22:33:08 +01:00
personalNote: payment.userComment
onPersonalNoteChanged: payment.userComment = personalNote
2025-02-04 21:20:35 +01:00
2023-07-07 09:41:04 +02:00
onCloseButtonPressed: {
var mainView = thePile.get(0);
mainView.currentIndex = 0; // go to the 'main' tab.
thePile.pop();
}
}
2023-03-10 22:16:37 +01:00
}