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 {
|
swapStore: SwapStore {
|
||||||
readonly property var accounts: WalletAccountsModel {}
|
readonly property var accounts: WalletAccountsModel {}
|
||||||
readonly property var flatNetworks: NetworksModel.flatNetworks
|
readonly property var flatNetworks: NetworksModel.flatNetworks
|
||||||
readonly property bool areTestNetworksEnabled: false
|
readonly property bool areTestNetworksEnabled: true
|
||||||
}
|
}
|
||||||
walletAssetsStore: WalletAssetsStore {
|
walletAssetsStore: WalletAssetsStore {
|
||||||
id: thisWalletAssetStore
|
id: thisWalletAssetStore
|
||||||
|
|
|
@ -26,7 +26,7 @@ Item {
|
||||||
swapStore: SwapStore {
|
swapStore: SwapStore {
|
||||||
readonly property var accounts: WalletAccountsModel {}
|
readonly property var accounts: WalletAccountsModel {}
|
||||||
readonly property var flatNetworks: NetworksModel.flatNetworks
|
readonly property var flatNetworks: NetworksModel.flatNetworks
|
||||||
readonly property bool areTestNetworksEnabled: false
|
readonly property bool areTestNetworksEnabled: true
|
||||||
}
|
}
|
||||||
walletAssetsStore: WalletAssetsStore {
|
walletAssetsStore: WalletAssetsStore {
|
||||||
id: thisWalletAssetStore
|
id: thisWalletAssetStore
|
||||||
|
@ -201,6 +201,46 @@ Item {
|
||||||
verify(controlUnderTest.cryptoValueValid)
|
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() {
|
function test_clickingMaxButton() {
|
||||||
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
|
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
|
||||||
verify(!!controlUnderTest)
|
verify(!!controlUnderTest)
|
||||||
|
|
|
@ -211,6 +211,12 @@ QtObject {
|
||||||
root.showUnPreferredChains = !root.showUnPreferredChains
|
root.showUnPreferredChains = !root.showUnPreferredChains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSelectedTokenIsOwnerToken(isOwnerToken) {
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedTokenName(tokenName) {
|
||||||
|
}
|
||||||
|
|
||||||
property string amountToSend
|
property string amountToSend
|
||||||
property bool suggestedRoutesCalled: false
|
property bool suggestedRoutesCalled: false
|
||||||
function suggestedRoutes(amount) {
|
function suggestedRoutes(amount) {
|
||||||
|
|
|
@ -57,4 +57,20 @@ QtObject {
|
||||||
}
|
}
|
||||||
return hovered? WalletUtils.colorizedChainPrefix(chainShortNames) + Utils.richColorText(finalAddress, Theme.palette.directColor1) : chainShortNames + finalAddress
|
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
|
ManageTokensCommunityTag 1.0 ManageTokensCommunityTag.qml
|
||||||
ManageTokensDelegate 1.0 ManageTokensDelegate.qml
|
ManageTokensDelegate 1.0 ManageTokensDelegate.qml
|
||||||
ManageTokensGroupDelegate 1.0 ManageTokensGroupDelegate.qml
|
ManageTokensGroupDelegate 1.0 ManageTokensGroupDelegate.qml
|
||||||
|
MaxSendButton 1.0 MaxSendButton.qml
|
||||||
InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml
|
InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml
|
||||||
StatusNetworkListItemTag 1.0 StatusNetworkListItemTag.qml
|
StatusNetworkListItemTag 1.0 StatusNetworkListItemTag.qml
|
||||||
CollectibleBalanceTag 1.0 CollectibleBalanceTag.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.Utils 0.1 as SQUtils
|
||||||
import StatusQ.Core.Theme 0.1
|
import StatusQ.Core.Theme 0.1
|
||||||
|
|
||||||
|
import AppLayouts.Wallet.controls 1.0
|
||||||
|
|
||||||
import shared.popups.send.views 1.0
|
import shared.popups.send.views 1.0
|
||||||
import shared.popups.send.panels 1.0
|
import shared.popups.send.panels 1.0
|
||||||
|
|
||||||
|
@ -92,17 +94,6 @@ Control {
|
||||||
readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
|
readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
|
||||||
readonly property string inputSymbol: amountToSendInput.inputIsFiat ? root.currencyStore.currentCurrency :
|
readonly property string inputSymbol: amountToSendInput.inputIsFiat ? root.currencyStore.currentCurrency :
|
||||||
!!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: ""
|
!!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
|
property string searchText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,13 +188,15 @@ Control {
|
||||||
interactive: true
|
interactive: true
|
||||||
selectedHolding: d.selectedHolding
|
selectedHolding: d.selectedHolding
|
||||||
fiatInputInteractive: root.fiatInputInteractive
|
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
|
multiplierIndex: d.isSelectedHoldingValidAsset && !!holdingSelector.selectedItem && !!holdingSelector.selectedItem.decimals
|
||||||
? holdingSelector.selectedItem.decimals
|
? holdingSelector.selectedItem.decimals
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
maxInputBalance: (root.swapSide === SwapInputPanel.SwapSide.Receive || !d.isSelectedHoldingValidAsset) ? Number.POSITIVE_INFINITY
|
maxInputBalance: (root.swapSide === SwapInputPanel.SwapSide.Receive || !d.isSelectedHoldingValidAsset) ? Number.POSITIVE_INFINITY
|
||||||
: d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
|
: maxSendButton.maxSafeValue
|
||||||
currentCurrency: root.currencyStore.currentCurrency
|
currentCurrency: root.currencyStore.currentCurrency
|
||||||
formatCurrencyAmount: root.currencyStore.formatCurrencyAmount
|
formatCurrencyAmount: root.currencyStore.formatCurrencyAmount
|
||||||
loading: root.loading
|
loading: root.loading
|
||||||
|
@ -247,26 +240,24 @@ Control {
|
||||||
onSearchTextChanged: d.searchText = searchText
|
onSearchTextChanged: d.searchText = searchText
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillHeight: !itemTag.visible }
|
Item { Layout.fillHeight: !maxSendButton.visible }
|
||||||
|
|
||||||
StatusListItemTag {
|
MaxSendButton {
|
||||||
id: itemTag
|
id: maxSendButton
|
||||||
objectName: "maxTagButton"
|
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
Layout.maximumWidth: parent.width
|
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
|
visible: d.isSelectedHoldingValidAsset && root.swapSide === SwapInputPanel.SwapSide.Pay
|
||||||
title: d.maxInputBalance > 0 ? qsTr("Max: %1").arg(d.maxInputBalanceFormatted)
|
|
||||||
: qsTr("No balances active")
|
onClicked: {
|
||||||
tagClickable: true
|
if (maxSafeValue)
|
||||||
closeButtonVisible: false
|
amountToSendInput.input.text = maxSafeValueAsString
|
||||||
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)
|
|
||||||
else
|
else
|
||||||
amountToSendInput.input.input.edit.clear()
|
amountToSendInput.input.input.edit.clear()
|
||||||
amountToSendInput.input.forceActiveFocus()
|
amountToSendInput.input.forceActiveFocus()
|
||||||
|
|
|
@ -181,6 +181,10 @@ QObject {
|
||||||
roleName: "chainId"
|
roleName: "chainId"
|
||||||
value: root.swapFormData.selectedNetworkChainId
|
value: root.swapFormData.selectedNetworkChainId
|
||||||
enabled: root.swapFormData.selectedNetworkChainId !== -1
|
enabled: root.swapFormData.selectedNetworkChainId !== -1
|
||||||
|
},
|
||||||
|
ValueFilter {
|
||||||
|
roleName: "isTest"
|
||||||
|
value: root.swapStore.areTestNetworksEnabled
|
||||||
}/*,
|
}/*,
|
||||||
// TODO enable once AccountsModalHeader is reworked!!
|
// TODO enable once AccountsModalHeader is reworked!!
|
||||||
ValueFilter {
|
ValueFilter {
|
||||||
|
|
|
@ -17,6 +17,8 @@ import StatusQ.Core.Theme 0.1
|
||||||
import StatusQ.Core.Utils 0.1
|
import StatusQ.Core.Utils 0.1
|
||||||
import StatusQ.Popups.Dialog 0.1
|
import StatusQ.Popups.Dialog 0.1
|
||||||
|
|
||||||
|
import AppLayouts.Wallet.controls 1.0
|
||||||
|
|
||||||
import "./panels"
|
import "./panels"
|
||||||
import "./controls"
|
import "./controls"
|
||||||
import "./views"
|
import "./views"
|
||||||
|
@ -141,14 +143,6 @@ StatusDialog {
|
||||||
|
|
||||||
recalculateRoutesAndFees()
|
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
|
bottomPadding: 16
|
||||||
|
@ -277,36 +271,22 @@ StatusDialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusListItemTag {
|
MaxSendButton {
|
||||||
Layout.maximumWidth: 300
|
Layout.maximumWidth: 300
|
||||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||||
Layout.preferredHeight: 22
|
|
||||||
visible: d.isSelectedHoldingValidAsset || d.isHoveredHoldingValidAsset && !d.isCollectiblesTransfer
|
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)
|
value: d.maxInputBalance
|
||||||
return qsTr("Max: %1").arg(balance.toString())
|
symbol: d.inputSymbol
|
||||||
}
|
valid: amountToSendInput.input.valid || !amountToSendInput.input.text
|
||||||
tagClickable: true
|
formatCurrencyAmount: (amount, symbol) => d.currencyStore.formatCurrencyAmount(amount, symbol, {noSymbol: !amountToSendInput.inputIsFiat})
|
||||||
closeButtonVisible: false
|
|
||||||
titleText.font.pixelSize: 12
|
onClicked: {
|
||||||
bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
|
if (maxSafeValue > 0)
|
||||||
titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
|
amountToSendInput.input.text = maxSafeValueAsString
|
||||||
onTagClicked: {
|
else
|
||||||
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
|
amountToSendInput.input.input.edit.clear()
|
||||||
amountToSendInput.input.text = d.currencyStore.formatCurrencyAmount(max, d.inputSymbol, {noSymbol: true, rawAmount: true}, LocaleUtils.userInputLocale)
|
amountToSendInput.input.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import StatusQ.Core 0.1
|
||||||
import StatusQ.Core.Theme 0.1
|
import StatusQ.Core.Theme 0.1
|
||||||
import StatusQ.Controls 0.1
|
import StatusQ.Controls 0.1
|
||||||
import StatusQ.Core.Utils 0.1 as SQUtils
|
import StatusQ.Core.Utils 0.1 as SQUtils
|
||||||
import StatusQ.Controls 0.1
|
|
||||||
import StatusQ.Components 0.1
|
import StatusQ.Components 0.1
|
||||||
import StatusQ.Controls.Validators 0.1
|
import StatusQ.Controls.Validators 0.1
|
||||||
|
|
||||||
|
@ -128,10 +127,6 @@ ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
id: topItem
|
id: topItem
|
||||||
|
|
||||||
property double topAmountToSend: !inputIsFiat ? d.cryptoValueToSend
|
|
||||||
: d.fiatValueToSend
|
|
||||||
property string topAmountSymbol: !inputIsFiat ? d.selectedSymbol
|
|
||||||
: root.currentCurrency
|
|
||||||
AmountInputWithCursor {
|
AmountInputWithCursor {
|
||||||
id: topAmountToSendInput
|
id: topAmountToSendInput
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
Loading…
Reference in New Issue