feat: [UI - Wallet Stability] Create Max button component

- create a reusable "Max" send button component
- use it in SwapModal and SendModal
- add some more tests in tst_SwapInputPanel.qml

Fixes #15066
This commit is contained in:
Lukáš Tinkl 2024-06-10 12:37:39 +02:00 committed by Lukáš Tinkl
parent 4d080e12aa
commit 46b81b30a6
10 changed files with 143 additions and 69 deletions

View File

@ -40,7 +40,7 @@ SplitView {
swapStore: SwapStore {
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: false
readonly property bool areTestNetworksEnabled: true
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore

View File

@ -26,7 +26,7 @@ Item {
swapStore: SwapStore {
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: false
readonly property bool areTestNetworksEnabled: true
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
@ -201,6 +201,46 @@ Item {
verify(controlUnderTest.cryptoValueValid)
}
// verify that when "fiatInputInteractive" mode is on, the Max send button text shows fiat currency symbol (e.g. "1.2 USD")
function test_maxButtonFiatCurrencySymbol() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
controlUnderTest.fiatInputInteractive = true
const maxTagButton = findChild(controlUnderTest, "maxTagButton")
verify(!!maxTagButton)
waitForRendering(maxTagButton)
verify(maxTagButton.visible)
verify(!maxTagButton.text.endsWith("ETH"))
mouseClick(maxTagButton)
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
waitForRendering(amountToSendInput)
const bottomItemText = findChild(amountToSendInput, "bottomItemText")
verify(!!bottomItemText)
verify(bottomItemText.visible)
mouseClick(bottomItemText)
waitForRendering(amountToSendInput)
verify(maxTagButton.text.endsWith("USD"))
}
// verify that in default mode, the Max send button text doesn't show the currency symbol for crypto (e.g. "1.2" for ETH)
function test_maxButtonNoCryptoCurrencySymbol() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
const maxTagButton = findChild(controlUnderTest, "maxTagButton")
verify(!!maxTagButton)
waitForRendering(maxTagButton)
verify(maxTagButton.visible)
verify(!maxTagButton.text.endsWith("ETH"))
}
function test_clickingMaxButton() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
verify(!!controlUnderTest)

View File

@ -211,6 +211,12 @@ QtObject {
root.showUnPreferredChains = !root.showUnPreferredChains
}
function setSelectedTokenIsOwnerToken(isOwnerToken) {
}
function setSelectedTokenName(tokenName) {
}
property string amountToSend
property bool suggestedRoutesCalled: false
function suggestedRoutes(amount) {

View File

@ -57,4 +57,20 @@ QtObject {
}
return hovered? WalletUtils.colorizedChainPrefix(chainShortNames) + Utils.richColorText(finalAddress, Theme.palette.directColor1) : chainShortNames + finalAddress
}
/**
Calculate max safe amount to be used when making a transaction
This logic is here to make sure there is enough eth to pay for the gas.
Context, when making a transaction, whatever the type: swap/bridge/send, you need eth to pay for the gas.
rationale: https://github.com/status-im/status-desktop/pull/14959#discussion_r1627110880
*/
function calculateMaxSafeSendAmount(value, symbol) {
if (symbol !== Constants.ethToken) {
return value
}
return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
}
}

View File

@ -0,0 +1,41 @@
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 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
QtObject {
id: d
readonly property string maxInputBalanceFormatted:
root.formatCurrencyAmount(Math.trunc(root.maxSafeValue*100)/100, root.symbol)
}
implicitHeight: 22
type: valid ? StatusBaseButton.Type.Normal : StatusBaseButton.Type.Danger
text: qsTr("Max. %1").arg(value === 0 ? locale.zeroDigit : d.maxInputBalanceFormatted)
horizontalPadding: 8
verticalPadding: 3
radius: 20
font.pixelSize: 12
font.weight: Font.Normal
}

View File

@ -10,6 +10,7 @@ ManageTokenMenuButton 1.0 ManageTokenMenuButton.qml
ManageTokensCommunityTag 1.0 ManageTokensCommunityTag.qml
ManageTokensDelegate 1.0 ManageTokensDelegate.qml
ManageTokensGroupDelegate 1.0 ManageTokensGroupDelegate.qml
MaxSendButton 1.0 MaxSendButton.qml
InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml
StatusNetworkListItemTag 1.0 StatusNetworkListItemTag.qml
CollectibleBalanceTag 1.0 CollectibleBalanceTag.qml

View File

@ -10,6 +10,8 @@ import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Core.Theme 0.1
import AppLayouts.Wallet.controls 1.0
import shared.popups.send.views 1.0
import shared.popups.send.panels 1.0
@ -92,17 +94,6 @@ Control {
readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
readonly property string inputSymbol: amountToSendInput.inputIsFiat ? root.currencyStore.currentCurrency :
!!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: ""
readonly property string maxInputBalanceFormatted:
root.currencyStore.formatCurrencyAmount(Math.trunc(prepareForMaxSend(d.maxInputBalance, d.inputSymbol)*100)/100, d.inputSymbol, {noSymbol: !amountToSendInput.inputIsFiat})
function prepareForMaxSend(value, symbol) {
if (symbol !== Constants.ethToken) {
return value
}
return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
}
property string searchText
}
@ -197,13 +188,15 @@ Control {
interactive: true
selectedHolding: d.selectedHolding
fiatInputInteractive: root.fiatInputInteractive
input.input.edit.color: !input.valid ? Theme.palette.dangerColor1 : maxSendButton.hovered ? Theme.palette.baseColor1
: Theme.palette.directColor1
multiplierIndex: d.isSelectedHoldingValidAsset && !!holdingSelector.selectedItem && !!holdingSelector.selectedItem.decimals
? holdingSelector.selectedItem.decimals
: 0
maxInputBalance: (root.swapSide === SwapInputPanel.SwapSide.Receive || !d.isSelectedHoldingValidAsset) ? Number.POSITIVE_INFINITY
: d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
: maxSendButton.maxSafeValue
currentCurrency: root.currencyStore.currentCurrency
formatCurrencyAmount: root.currencyStore.formatCurrencyAmount
loading: root.loading
@ -247,26 +240,24 @@ Control {
onSearchTextChanged: d.searchText = searchText
}
Item { Layout.fillHeight: !itemTag.visible }
Item { Layout.fillHeight: !maxSendButton.visible }
StatusListItemTag {
id: itemTag
objectName: "maxTagButton"
MaxSendButton {
id: maxSendButton
Layout.alignment: Qt.AlignRight
Layout.maximumWidth: parent.width
Layout.preferredHeight: 22
objectName: "maxTagButton"
value: d.maxInputBalance
symbol: d.inputSymbol
valid: amountToSendInput.input.valid || !amountToSendInput.input.text
formatCurrencyAmount: (amount, symbol) => root.currencyStore.formatCurrencyAmount(amount, symbol, {noSymbol: !amountToSendInput.inputIsFiat})
visible: d.isSelectedHoldingValidAsset && root.swapSide === SwapInputPanel.SwapSide.Pay
title: d.maxInputBalance > 0 ? qsTr("Max: %1").arg(d.maxInputBalanceFormatted)
: qsTr("No balances active")
tagClickable: true
closeButtonVisible: false
titleText.font.pixelSize: 12
bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
onTagClicked: {
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
if (max > 0)
amountToSendInput.input.text = max.toLocaleString(Qt.locale(), 'f', -128)
onClicked: {
if (maxSafeValue)
amountToSendInput.input.text = maxSafeValueAsString
else
amountToSendInput.input.input.edit.clear()
amountToSendInput.input.forceActiveFocus()

View File

@ -181,6 +181,10 @@ QObject {
roleName: "chainId"
value: root.swapFormData.selectedNetworkChainId
enabled: root.swapFormData.selectedNetworkChainId !== -1
},
ValueFilter {
roleName: "isTest"
value: root.swapStore.areTestNetworksEnabled
}/*,
// TODO enable once AccountsModalHeader is reworked!!
ValueFilter {

View File

@ -17,6 +17,8 @@ import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Wallet.controls 1.0
import "./panels"
import "./controls"
import "./views"
@ -141,14 +143,6 @@ StatusDialog {
recalculateRoutesAndFees()
}
function prepareForMaxSend(value, symbol) {
if(symbol !== "ETH") {
return value
}
return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
}
}
bottomPadding: 16
@ -277,36 +271,22 @@ StatusDialog {
}
}
StatusListItemTag {
MaxSendButton {
Layout.maximumWidth: 300
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredHeight: 22
visible: d.isSelectedHoldingValidAsset || d.isHoveredHoldingValidAsset && !d.isCollectiblesTransfer
title: {
if(d.isHoveredHoldingValidAsset && !!d.hoveredHolding.symbol) {
const input = amountToSendInput.inputIsFiat ? d.hoveredHolding.currentCurrencyBalance : d.hoveredHolding.currentBalance
const max = d.prepareForMaxSend(input, d.hoveredHolding.symbol)
if (max <= 0)
return qsTr("No balances active")
const balance = d.currencyStore.formatCurrencyAmount(max, amountToSendInput.inputIsFiat ? amountToSendInput.currentCurrency
: d.selectedHolding.symbol)
return qsTr("Max: %1").arg(balance.toString())
}
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
if (max <= 0)
return qsTr("No balances active")
const balance = d.currencyStore.formatCurrencyAmount(max, d.inputSymbol)
return qsTr("Max: %1").arg(balance.toString())
}
tagClickable: true
closeButtonVisible: false
titleText.font.pixelSize: 12
bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
onTagClicked: {
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
amountToSendInput.input.text = d.currencyStore.formatCurrencyAmount(max, d.inputSymbol, {noSymbol: true, rawAmount: true}, LocaleUtils.userInputLocale)
value: d.maxInputBalance
symbol: d.inputSymbol
valid: amountToSendInput.input.valid || !amountToSendInput.input.text
formatCurrencyAmount: (amount, symbol) => d.currencyStore.formatCurrencyAmount(amount, symbol, {noSymbol: !amountToSendInput.inputIsFiat})
onClicked: {
if (maxSafeValue > 0)
amountToSendInput.input.text = maxSafeValueAsString
else
amountToSendInput.input.input.edit.clear()
amountToSendInput.input.forceActiveFocus()
}
}
}

View File

@ -5,7 +5,6 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Controls.Validators 0.1
@ -128,10 +127,6 @@ ColumnLayout {
Layout.fillWidth: true
id: topItem
property double topAmountToSend: !inputIsFiat ? d.cryptoValueToSend
: d.fiatValueToSend
property string topAmountSymbol: !inputIsFiat ? d.selectedSymbol
: root.currentCurrency
AmountInputWithCursor {
id: topAmountToSendInput
Layout.fillWidth: true