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:
parent
4d080e12aa
commit
46b81b30a6
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue