2023-03-10 22:16:37 +01:00
/*
* This file is part of the Flowee project
* Copyright (C) 2022-2023 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/>.
*/
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
2023-03-13 10:36:27 +01:00
headerText: qsTr ( "Build Transaction" )
2023-03-10 22:16:37 +01:00
2023-03-12 17:21:02 +01:00
Item { // data and non-visible stuff for this page
/*
* Components embedded in this file:
* Editors:
destinationEditPage
2023-03-12 14:26:13 +01:00
2023-03-12 17:21:02 +01:00
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
2023-03-12 17:21:02 +01:00
* 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
}
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 {
2023-03-12 17:21:02 +01:00
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 ( ) ;
2023-03-12 14:26:13 +01:00
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
return qsTr ( "unset" , "indication of empty" ) ;
return qsTr ( "invalid" , "address is not correct" ) ;
}
2023-05-31 15:47:14 +02:00
return s ;
2023-03-12 17:21:02 +01:00
}
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-12 17:21:02 +01:00
}
2023-03-10 22:16:37 +01:00
}
}
}
2023-03-12 14:26:13 +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
Page {
2023-03-11 22:05:15 +01:00
headerText: qsTr ( "Edit Destination" )
2023-04-18 22:09:45 +02:00
property QtObject sendAllAction: QQC2 . Action {
checkable: true
checked: paymentDetail . maxSelected
text: qsTr ( "Send All" , "all money in wallet" )
onTriggered: paymentDetail . maxSelected = checked
}
2023-03-11 22:05:15 +01:00
Flowee . Label {
id: destinationLabel
text: qsTr ( "Bitcoin Cash Address" ) + ":"
}
Flowee . MultilineTextField {
id: destination
anchors.top: destinationLabel . bottom
2023-04-18 22:09:45 +02:00
anchors.topMargin: 10
2023-03-11 22:05:15 +01:00
anchors.left: parent . left
anchors.right: parent . right
2023-05-17 23:26:54 +02:00
height: Math . max ( destinationLabel . height * 2.3 , implicitHeight )
2023-09-02 20:23:29 +02:00
focus: enabled
2023-10-14 20:47:29 +02:00
property var addressType: Pay . identifyString ( totalText ) ;
2023-03-11 22:05:15 +01:00
text: paymentDetail . address
2023-03-12 17:21:02 +01:00
nextFocusTarget: priceInput
2023-09-02 20:23:29 +02:00
enabled: paymentDetail . editable
2023-10-14 20:47:29 +02:00
onTotalTextChanged: {
paymentDetail . address = totalText
2023-03-11 22:05:15 +01:00
addressInfo . createInfo ( ) ;
}
2023-03-12 17:21:02 +01:00
color: {
2023-10-14 20:47:29 +02:00
if ( ! activeFocus && totalText !== "" && ! addressInfo . addressOk )
2023-03-12 17:21:02 +01:00
return mainWindow . errorRed
return palette . windowText
2023-03-11 22:05:15 +01:00
}
}
2023-10-14 20:47:29 +02:00
Rectangle {
id: pasteButton
2023-10-17 12:00:28 +02:00
property bool appWasHidden: false
2023-10-14 20:47:29 +02:00
anchors.verticalCenter: destination . bottom
anchors.horizontalCenter: destination . horizontalCenter
width: labelText . height + labelText . width + 20 + 5 + 20
height: labelText . height + 10
radius: 6
color: palette . window
border.color: palette . midlight
border.width: 1
2023-10-17 12:00:28 +02:00
visible: destination . totalText === "" && ( cbh . text !== "" || appWasHidden )
Connections {
/*
* A usecase of pasting is that the user opens this screen, goes to another app and
* copies something they want to copy and then comes back here to paste it.
* This causes the 'appWasHidden' to be set to true and we then 'enable' the clipboardHelper
* in order to read the clipboard-data.
*
* Notice that this is needed because the clipboard doesn't notice something new being
* present after we just got unfrozen.
*/
target: Application
function onActiveChanged ( ) {
if ( Application . active !== Qt . ApplicationActive ) {
pasteButton . appWasHidden = true ;
cbh . enabled = false ;
}
}
}
2023-10-14 20:47:29 +02:00
ClipboardHelper {
id: cbh
filter: ClipboardHelper . Addresses + ClipboardHelper . LegacyAddresses
}
Image {
x: 20
width: labelText . height
height: width
source: "qrc:/edit-clipboard" + ( Pay . useDarkSkin ? "-light.svg" : ".svg" ) ;
anchors.verticalCenter: parent . verticalCenter
}
Flowee . Label {
id: labelText
anchors.verticalCenter: parent . verticalCenter
anchors.right: parent . right
anchors.rightMargin: 20
text: qsTr ( "Paste" )
}
MouseArea {
anchors.fill: parent
2023-10-17 12:00:28 +02:00
onClicked: {
if ( pasteButton . appWasHidden ) {
pasteButton . appWasHidden = false ;
cbh . enabled = true ; // fills text, if there is something to fill.
}
if ( cbh . text !== "" ) {
destination . text = cbh . text
priceInput . takeFocus ( ) ;
}
}
2023-10-14 20:47:29 +02:00
}
}
2023-03-11 22:05:15 +01:00
Flowee . LabelWithClipboard {
id: nativeLabel
width: parent . width
anchors.top: destination . bottom
anchors.topMargin: 10
2023-05-31 15:47:14 +02:00
// only show if its substantially different
visible: text !== "" && text !== destination . text && destination . text !== paymentDetail . formattedTarget
text: paymentDetail . niceAddress
2023-03-11 22:05:15 +01:00
clipboardText: paymentDetail . formattedTarget // the one WITH bitcoincash:
font.italic: true
menuText: qsTr ( "Copy Address" )
}
2023-03-12 17:21:02 +01:00
Flowee . AddressInfoWidget {
2023-03-11 22:05:15 +01:00
id: addressInfo
2023-05-17 23:26:54 +02:00
anchors.top: nativeLabel . visible ? nativeLabel.bottom : destination . bottom
2023-03-11 22:05:15 +01:00
width: parent . width
2023-03-12 17:21:02 +01:00
addressType: destination . addressType
2023-03-11 22:05:15 +01:00
}
2023-04-18 22:09:45 +02:00
PriceInputWidget {
id: priceInput
width: parent . width
anchors.top: addressInfo . bottom
paymentBackend: paymentDetail
fiatFollowsSats: paymentDetail . fiatFollows
onFiatFollowsSatsChanged: paymentDetail . fiatFollows = fiatFollowsSats
}
AccountSelectorWidget {
id: walletNameBackground
anchors.bottom: numericKeyboard . top
anchors.bottomMargin: 10
stickyAccount: true
balanceActions: [ sendAllAction ]
}
NumericKeyboardWidget {
id: numericKeyboard
anchors.bottom: parent . bottom
anchors.bottomMargin: 15
width: parent . width
enabled: ! paymentDetail . maxSelected
2023-06-27 18:57:50 +02:00
dataInput: priceInput
2023-04-18 22:09:45 +02:00
}
2023-03-11 22:05:15 +01:00
Rectangle {
2023-04-18 22:09:45 +02:00
color: mainWindow . errorRedBg
2023-03-11 22:05:15 +01:00
radius: 15
width: parent . width
height: warningColumn . height + 20
anchors.top: nativeLabel . bottom
// BTC address entered warning.
visible: ( destination . addressType === Wallet . LegacySH
|| destination . addressType === Wallet . LegacyPKH )
&& paymentDetail . forceLegacyOk === false ;
Column {
id: warningColumn
x: 10
y: 10
width: parent . width - 20
spacing: 10
Flowee . Label {
font.bold: true
font.pixelSize: warning . font . pixelSize * 1.2
text: qsTr ( "Warning" )
color: "white"
anchors.horizontalCenter: parent . horizontalCenter
}
Flowee . Label {
id: warning
width: parent . width
color: "white"
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
}
Item {
width: parent . width
height: okButton . height
QQC2 . Button {
id: okButton
anchors.right: parent . right
text: qsTr ( "I am certain" )
onClicked: paymentDetail . forceLegacyOk = true
}
}
}
}
2023-03-10 22:16:37 +01:00
}
}
2023-06-30 22:27:27 +02: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 ( )
}
}
}
}
}
}
2023-03-12 17:21:02 +01:00
// check the comment at loaderForPayments to understand this one
function pushToThePile ( componentId , detail ) {
thePile . push ( loaderForPayments ,
{ "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
2023-03-13 10:36:27 +01:00
spacing: 6
2023-03-12 17:21:02 +01:00
2023-05-18 21:52:51 +02:00
AccountSelectorWidget {
visible: ! portfolio . singleAccountSetup
}
2023-03-13 10:36:27 +01:00
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
2023-03-12 17:21:02 +01:00
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
2023-03-12 14:26:13 +01:00
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: {
/* When we move, we move the icons etc with us.
*/
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 ;
2023-03-12 14:26:13 +01:00
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
2023-03-12 17:21:02 +01:00
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
2023-03-12 17:21:02 +01:00
height: loader . height
2023-03-11 13:28:29 +01:00
x: - width ;
Rectangle {
id: leftBackground
opacity: 0
anchors.fill: parent
2023-04-18 22:09:45 +02:00
color: mainWindow . errorRedBg
2023-03-11 13:28:29 +01:00
}
2023-03-10 22:16:37 +01:00
Rectangle {
id: trashcan
color: "orange" // TODO replace this with an icon
width: 40
height: 40
y: 10
x: {
let newX = parent . width - width ;
2023-03-11 13:28:29 +01:00
let moved = parent . x + parent . width ;
let additional = Math . max ( 0 , Math . min ( moved - width , 8 ) ) ;
2023-03-10 22:16:37 +01:00
return newX - additional ;
}
}
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
}
2023-03-12 17:21:02 +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
2023-03-12 17:21:02 +01:00
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
2023-03-12 17:21:02 +01:00
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" )
showPageIcon: true
2023-03-12 14:26:13 +01:00
onClicked: pushToThePile ( destinationEditPage , payment . addExtraOutput ( ) ) ;
2023-03-11 13:28:29 +01:00
}
2023-03-12 17:21:02 +01:00
/*
2023-03-10 22:16:37 +01:00
TextButton {
text: qsTr("Add Detail...")
showPageIcon: true
onClicked: thePile.push(paymentDetailSelector);
}
2023-03-12 17:21:02 +01:00
*/
2023-03-13 11:52:47 +01:00
Flowee . Button {
2023-06-30 22:27:27 +02:00
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: {
2023-06-30 22:27:27 +02:00
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 {
anchors.leftMargin: - 10 // go against the margins that Page gave us to show more fullscreen.
anchors.rightMargin: - 10
onStatusChanged: {
if ( status !== "" )
root . headerText = status ;
}
onCloseButtonPressed: {
var mainView = thePile . get ( 0 ) ;
mainView . currentIndex = 0 ; // go to the 'main' tab.
thePile . pop ( ) ;
}
}
2023-03-10 22:16:37 +01:00
}