From 1e88756c131a3215fcad8b21db2ffa81a06d24e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Wed, 17 Jul 2024 17:48:56 +0200 Subject: [PATCH] SendModal: new AmountToSend initial integration - new AmountToSend used in SendModal - MaxSendButton simplified Closes: #15207 --- ui/StatusQ/src/objectproxymodel.cpp | 2 +- .../Wallet/controls/MaxSendButton.qml | 28 +-- .../Wallet/panels/SwapInputPanel.qml | 19 +- ui/imports/shared/popups/send/SendModal.qml | 173 +++++++++++++----- 4 files changed, 148 insertions(+), 74 deletions(-) diff --git a/ui/StatusQ/src/objectproxymodel.cpp b/ui/StatusQ/src/objectproxymodel.cpp index fec5da5b33..ae160c9796 100644 --- a/ui/StatusQ/src/objectproxymodel.cpp +++ b/ui/StatusQ/src/objectproxymodel.cpp @@ -176,7 +176,7 @@ QObject* ObjectProxyModel::proxyObject(int index) auto parentContext = creationContext ? creationContext : m_delegate->engine()->rootContext(); - auto context = new QQmlContext(parentContext/*, submodelObj*/); + auto context = new QQmlContext(parentContext); auto rowData = new QQmlPropertyMap(context); auto model = sourceModel(); diff --git a/ui/app/AppLayouts/Wallet/controls/MaxSendButton.qml b/ui/app/AppLayouts/Wallet/controls/MaxSendButton.qml index 8af91dccc7..57f18fe9d9 100644 --- a/ui/app/AppLayouts/Wallet/controls/MaxSendButton.qml +++ b/ui/app/AppLayouts/Wallet/controls/MaxSendButton.qml @@ -1,33 +1,19 @@ import QtQuick 2.15 -import QtQuick.Controls 2.15 -import StatusQ.Core 0.1 import StatusQ.Controls 0.1 -import AppLayouts.Wallet 1.0 - -import utils 1.0 - StatusButton { - id: root + required property string formattedValue + property bool markAsInvalid: false - required property double value - required property string symbol - required property bool valid - property var formatCurrencyAmount: (amount, symbol) => { return "FIXME" } - - readonly property double maxSafeValue: WalletUtils.calculateMaxSafeSendAmount(value, symbol) - readonly property string maxSafeValueAsString: maxSafeValue.toLocaleString(locale, 'f', -128) - - locale: LocaleUtils.userInputLocale - - implicitHeight: 22 - - type: valid ? StatusBaseButton.Type.Normal : StatusBaseButton.Type.Danger - text: qsTr("Max. %1").arg(value === 0 ? locale.zeroDigit : root.formatCurrencyAmount(maxSafeValue, root.symbol)) + text: qsTr("Max. %1").arg(formattedValue) + type: markAsInvalid ? StatusBaseButton.Type.Danger + : StatusBaseButton.Type.Normal horizontalPadding: 8 verticalPadding: 3 + implicitHeight: 22 + radius: 20 font.pixelSize: 12 font.weight: Font.Normal diff --git a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml index 952f3da9f6..8b6ca74b48 100644 --- a/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml @@ -10,6 +10,7 @@ import StatusQ.Core 0.1 import StatusQ.Core.Utils 0.1 as SQUtils import StatusQ.Core.Theme 0.1 +import AppLayouts.Wallet 1.0 import AppLayouts.Wallet.controls 1.0 import AppLayouts.Wallet.stores 1.0 import AppLayouts.Wallet.adaptors 1.0 @@ -256,14 +257,24 @@ Control { MaxSendButton { id: maxSendButton + Layout.alignment: Qt.AlignRight Layout.maximumWidth: parent.width objectName: "maxTagButton" - value: d.maxInputBalance - symbol: d.inputSymbol - valid: (amountToSendInput.input.valid || !amountToSendInput.input.text) && value > 0 - formatCurrencyAmount: (amount, symbol) => root.currencyStore.formatCurrencyAmount(amount, symbol, {noSymbol: !amountToSendInput.inputIsFiat}) + readonly property double maxSafeValue: WalletUtils.calculateMaxSafeSendAmount( + d.maxInputBalance, d.inputSymbol) + readonly property string maxSafeValueAsString: maxSafeValue.toLocaleString( + LocaleUtils.userInputLocale, 'f', -128) + + markAsInvalid: (!amountToSendInput.input.valid && !!amountToSendInput.input.text) + || d.maxInputBalance === 0 + + formattedValue: + d.maxInputBalance === 0 ? LocaleUtils.userInputLocale.zeroDigit + : root.currencyStore.formatCurrencyAmount( + maxSafeValue, d.inputSymbol, + { noSymbol: !amountToSendInput.inputIsFiat }) visible: d.isSelectedHoldingValidAsset && root.swapSide === SwapInputPanel.SwapSide.Pay diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index ec566f9efc..e984a2fdbf 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -73,9 +73,9 @@ StatusDialog { property var recalculateRoutesAndFees: Backpressure.debounce(popup, 600, function() { if(!!popup.preSelectedAccount && !!holdingSelector.selectedItem - && recipientInputLoader.ready && (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer)) { + && recipientInputLoader.ready && (amountToSend.ready || d.isCollectiblesTransfer)) { popup.isLoading = true - popup.store.suggestedRoutes(d.isCollectiblesTransfer ? "1" : amountToSendInput.cryptoValueToSend) + popup.store.suggestedRoutes(d.isCollectiblesTransfer ? "1" : amountToSend.amount) } }) @@ -94,15 +94,34 @@ StatusDialog { popup.preSelectedSendType === Constants.SendType.StickersBuy readonly property var currencyStore: store.currencyStore - readonly property int errorType: !amountToSendInput.input.valid && (!isCollectiblesTransfer) ? Constants.SendAmountExceedsBalance : - (popup.bestRoutes && popup.bestRoutes.count === 0 && - !!amountToSendInput.input.text && recipientInputLoader.ready && !popup.isLoading) ? - Constants.NoRoute : Constants.NoError + + readonly property int errorType: { + if (amountToSend.balanceExceeded && !isCollectiblesTransfer) + return Constants.SendAmountExceedsBalance + + if (popup.bestRoutes && popup.bestRoutes.count === 0 + && !amountToSend.empty && recipientInputLoader.ready + && !popup.isLoading) + return Constants.NoRoute + + return Constants.NoError + } + readonly property double maxFiatBalance: isSelectedHoldingValidAsset ? selectedHolding.currentCurrencyBalance : 0 readonly property double maxCryptoBalance: isSelectedHoldingValidAsset ? selectedHolding.currentBalance : 0 - readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance - readonly property string inputSymbol: amountToSendInput.inputIsFiat ? currencyStore.currentCurrency : !!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: "" - readonly property bool errorMode: popup.isLoading || !recipientInputLoader.ready ? false : errorType !== Constants.NoError || networkSelector.errorMode || !(amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) + readonly property double maxInputBalance: amountToSend.fiatMode ? maxFiatBalance : maxCryptoBalance + + readonly property string tokenSymbol: !!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: "" + readonly property string inputSymbol: amountToSend.fiatMode ? currencyStore.currentCurrency : tokenSymbol + readonly property bool errorMode: { + if (popup.isLoading || !recipientInputLoader.ready) + return false + + return errorType !== Constants.NoError + || networkSelector.errorMode + || !(amountToSend.ready || d.isCollectiblesTransfer) + } + readonly property string uuid: Utils.uuid() property bool isPendingTx: false property string totalTimeEstimate @@ -114,9 +133,6 @@ StatusDialog { property var selectedHolding: null property var selectedHoldingType: Constants.TokenType.Unknown readonly property bool isSelectedHoldingValidAsset: !!selectedHolding && selectedHoldingType === Constants.TokenType.ERC20 - property var hoveredHolding: null - property var hoveredHoldingType: Constants.TokenType.Unknown - readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.TokenType.ERC20 onSelectedHoldingChanged: { if (d.selectedHoldingType === Constants.TokenType.ERC20) { @@ -128,7 +144,7 @@ StatusDialog { d.selectedHoldingType === Constants.TokenType.ERC1155) { let sendType = d.selectedHoldingType === Constants.TokenType.ERC721 ? Constants.SendType.ERC721Transfer : Constants.SendType.ERC1155Transfer store.setSendType(sendType) - amountToSendInput.input.text = 1 + amountToSend.setValue("1") store.setSelectedAssetKey(selectedHolding.contractAddress+":"+selectedHolding.tokenId) store.setRouteEnabledFromChains(selectedHolding.chainId) store.updateRoutePreferredChains(selectedHolding.chainId) @@ -168,7 +184,7 @@ StatusDialog { } onOpened: { - amountToSendInput.input.input.edit.forceActiveFocus() + amountToSend.forceActiveFocus() // IMPORTANT: This step must be the first one since it's storing the send type // into the backend at this stage so that, before this assignement, some properties @@ -216,7 +232,14 @@ StatusDialog { } if(!!popup.preDefinedAmountToSend) { - amountToSendInput.input.text = Number(popup.preDefinedAmountToSend).toLocaleString(Qt.locale(), 'f', -128) + // TODO: At this stage the number should not be localized. However + // in many places when initializing popup the number is provided + // in localized version. It should be refactored to provide raw + // number consistently. Only the displaying component should apply + // final localized formatting. + const delocalized = popup.preDefinedAmountToSend.replace(",", ".") + + amountToSend.setValue(delocalized) } } @@ -379,45 +402,98 @@ StatusDialog { } MaxSendButton { + id: maxButton + + readonly property double maxSafeValue: WalletUtils.calculateMaxSafeSendAmount( + d.maxInputBalance, d.inputSymbol) + + readonly property double maxSafeCryptoValue: WalletUtils.calculateMaxSafeSendAmount( + d.maxCryptoBalance, d.tokenSymbol) + + formattedValue: d.currencyStore.formatCurrencyAmount( + maxSafeValue, d.inputSymbol, + { noSymbol: !amountToSend.fiatMode }) + + markAsInvalid: amountToSend.markAsInvalid + Layout.maximumWidth: 300 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - visible: d.isSelectedHoldingValidAsset || d.isHoveredHoldingValidAsset && !d.isCollectiblesTransfer - value: d.maxInputBalance - symbol: d.inputSymbol - valid: amountToSendInput.input.valid || !amountToSendInput.input.text - formatCurrencyAmount: (amount, symbol) => d.currencyStore.formatCurrencyAmount(amount, symbol, {noSymbol: !amountToSendInput.inputIsFiat}) + visible: d.isSelectedHoldingValidAsset && !d.isCollectiblesTransfer onClicked: { - if (maxSafeValue > 0) - amountToSendInput.input.text = maxSafeValueAsString - else - amountToSendInput.input.input.edit.clear() - amountToSendInput.input.forceActiveFocus() + if (maxSafeValue > 0) { + amountToSend.setValue(SQUtils.AmountsArithmetic.fromNumber(maxSafeValue).toString()) + }else { + amountToSend.clear() + } + + amountToSend.forceActiveFocus() } } } RowLayout { visible: d.isSelectedHoldingValidAsset && !d.isCollectiblesTransfer - AmountToSend { - id: amountToSendInput - Layout.fillWidth: true - isBridgeTx: d.isBridgeTx + AmountToSendNew { + id: amountToSend + + caption: d.isBridgeTx ? qsTr("Amount to bridge") + : qsTr("Amount to send") interactive: popup.interactive - selectedHolding: d.selectedHolding - maxInputBalance: d.maxInputBalance - currentCurrency: d.currencyStore.currentCurrency + + readonly property bool balanceExceeded: + SQUtils.AmountsArithmetic.cmp( + SQUtils.AmountsArithmetic.fromNumber(maxButton.maxSafeCryptoValue, multiplierIndex), + SQUtils.AmountsArithmetic.fromString(amount)) === -1 + + readonly property bool ready: valid && !empty && !balanceExceeded + && amount !== "0" + + readonly property string selectedSymbol: + !!d.selectedHolding && !!d.selectedHolding.symbol + ? d.selectedHolding.symbol : "" + + // For backward compatibility. To be removed when + // dependent components (NetworkSelector, AmountToReceive) + // are refactored. + readonly property double asNumber: { + if (!valid) + return 0 + + return parseFloat(text.replace(",", ".")) + } + readonly property int minSendCryptoDecimals: + !fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0 + readonly property int minReceiveCryptoDecimals: + !fiatMode ? minSendCryptoDecimals + 1 : 0 + readonly property int minSendFiatDecimals: + fiatMode ? LocaleUtils.fractionalPartLength(asNumber) : 0 + readonly property int minReceiveFiatDecimals: + fiatMode ? minSendFiatDecimals + 1 : 0 + // End of to-be-removed part + + decimalPoint: LocaleUtils.userInputLocale.decimalPoint + markAsInvalid: balanceExceeded // Collectibles do not have decimals - multiplierIndex: d.isSelectedHoldingValidAsset && !!holdingSelector.selectedItem && !!holdingSelector.selectedItem.decimals - ? holdingSelector.selectedItem.decimals - : 0 + multiplierIndex: + d.isSelectedHoldingValidAsset + && !!holdingSelector.selectedItem + && !!holdingSelector.selectedItem.decimals + ? holdingSelector.selectedItem.decimals : 0 - formatCurrencyAmount: d.currencyStore.formatCurrencyAmount - onReCalculateSuggestedRoute: popup.recalculateRoutesAndFees() - input.input.tabNavItem: recipientInputLoader.item - Keys.onTabPressed: event.accepted = true + price: d.isSelectedHoldingValidAsset + ? (d.selectedHolding ? + d.selectedHolding.marketDetails.currencyPrice.amount : 1) + : 1 + + formatFiat: amount => d.currencyStore.formatCurrencyAmount( + amount, d.currencyStore.currentCurrency) + formatBalance: amount => d.currencyStore.formatCurrencyAmount( + amount, selectedSymbol) + + onAmountChanged: popup.recalculateRoutesAndFees() } // Horizontal spacer @@ -428,14 +504,14 @@ StatusDialog { Layout.alignment: Qt.AlignRight Layout.fillWidth:true visible: !!popup.bestRoutes && popup.bestRoutes !== undefined && - popup.bestRoutes.count > 0 && amountToSendInput.inputNumberValid + popup.bestRoutes.count > 0 && amountToSend.ready isLoading: popup.isLoading selectedHolding: d.selectedHolding isBridgeTx: d.isBridgeTx cryptoValueToReceive: d.totalAmountToReceive - inputIsFiat: amountToSendInput.inputIsFiat - minCryptoDecimals: amountToSendInput.minReceiveCryptoDecimals - minFiatDecimals: amountToSendInput.minReceiveFiatDecimals + inputIsFiat: amountToSend.fiatMode + minCryptoDecimals: amountToSend.minReceiveCryptoDecimals + minFiatDecimals: amountToSend.minReceiveFiatDecimals currentCurrency: d.currencyStore.currentCurrency formatCurrencyAmount: d.currencyStore.formatCurrencyAmount } @@ -520,7 +596,7 @@ StatusDialog { contentWidth: availableWidth visible: recipientInputLoader.ready && - (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) + (amountToSend.ready || d.isCollectiblesTransfer) objectName: "sendModalScroll" @@ -537,9 +613,9 @@ StatusDialog { interactive: popup.interactive selectedRecipient: popup.preSelectedRecipient ensAddressOrEmpty: recipientInputLoader.resolvedENSAddress - amountToSend: amountToSendInput.cryptoValueToSendFloat - minSendCryptoDecimals: amountToSendInput.minSendCryptoDecimals - minReceiveCryptoDecimals: amountToSendInput.minReceiveCryptoDecimals + amountToSend: amountToSend.cryptoValueToSendFloat + minSendCryptoDecimals: amountToSend.minSendCryptoDecimals + minReceiveCryptoDecimals: amountToSend.minReceiveCryptoDecimals selectedAsset: d.selectedHolding onReCalculateSuggestedRoute: popup.recalculateRoutesAndFees() errorType: d.errorType @@ -562,7 +638,8 @@ StatusDialog { maxFiatFees: popup.isLoading ? "..." : d.currencyStore.formatCurrencyAmount(d.totalFeesInFiat, d.currencyStore.currentCurrency) totalTimeEstimate: popup.isLoading? "..." : d.totalTimeEstimate pending: d.isPendingTx || popup.isLoading - visible: recipientInputLoader.ready && (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) && !d.errorMode + visible: recipientInputLoader.ready && (amountToSend.ready || d.isCollectiblesTransfer) && !d.errorMode + onNextButtonClicked: popup.sendTransaction() }