From 9d8542c95d8cd7a06863292be82f950a9ea56dfb Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Wed, 15 May 2024 23:22:13 +0200 Subject: [PATCH] feat(@desktop/wallet): Add support for Account selection in Swap Modal using already existing AccountsModalHeader.qml fixes #14749 --- storybook/pages/SwapModalPage.qml | 59 +++- storybook/qmlTests/tests/tst_SwapModal.qml | 283 ++++++++++++++++++ storybook/src/Models/WalletAccountsModel.qml | 40 ++- .../AppLayouts/Wallet/stores/SwapStore.qml | 5 + .../stubs/AppLayouts/Wallet/stores/qmldir | 1 + ui/app/AppLayouts/Wallet/WalletLayout.qml | 10 +- ...apFormData.qml => SwapInputParamsForm.qml} | 0 .../Wallet/popups/swap/SwapModal.qml | 43 ++- .../Wallet/popups/swap/SwapModalAdaptor.qml | 98 ++++++ ui/app/AppLayouts/Wallet/popups/swap/qmldir | 3 +- ui/app/AppLayouts/Wallet/stores/SwapStore.qml | 11 + .../Wallet/stores/WalletAssetsStore.qml | 5 +- ui/app/AppLayouts/Wallet/stores/qmldir | 1 + ui/app/mainui/Popups.qml | 8 +- ui/imports/shared/controls/Padding.qml | 6 + ui/imports/shared/controls/qmldir | 1 + .../send/controls/AccountsModalHeader.qml | 23 +- .../send/controls/WalletAccountListItem.qml | 41 ++- .../popups/send/views/NetworkSelector.qml | 2 +- 19 files changed, 586 insertions(+), 54 deletions(-) create mode 100644 storybook/qmlTests/tests/tst_SwapModal.qml create mode 100644 storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml rename ui/app/AppLayouts/Wallet/popups/swap/{SwapFormData.qml => SwapInputParamsForm.qml} (100%) create mode 100644 ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml create mode 100644 ui/app/AppLayouts/Wallet/stores/SwapStore.qml create mode 100644 ui/imports/shared/controls/Padding.qml diff --git a/storybook/pages/SwapModalPage.qml b/storybook/pages/SwapModalPage.qml index 5917eb997e..a29c115e2d 100644 --- a/storybook/pages/SwapModalPage.qml +++ b/storybook/pages/SwapModalPage.qml @@ -1,7 +1,9 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import SortFilterProxyModel 0.2 +import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Utils 0.1 import StatusQ.Controls 0.1 @@ -10,6 +12,7 @@ import utils 1.0 import Storybook 1.0 import Models 1.0 +import shared.stores 1.0 import AppLayouts.Wallet.stores 1.0 import AppLayouts.Wallet.popups.swap 1.0 @@ -22,7 +25,13 @@ SplitView { QtObject { id: d + readonly property var accountsModel: WalletAccountsModel {} readonly property var tokenBySymbolModel: TokensBySymbolModel {} + readonly property var flatNetworksModel: NetworksModel.flatNetworks + readonly property var filteredNetworksModel: SortFilterProxyModel { + sourceModel: d.flatNetworksModel + filters: ValueFilter { roleName: "isTest"; value: areTestNetworksEnabledCheckbox.checked } + } } PopupBackground { @@ -45,28 +54,45 @@ SplitView { SwapModal { id: swapModal visible: true - formData: SwapFormData { + swapInputParamsForm: SwapInputParamsForm { selectedAccountIndex: accountComboBox.currentIndex selectedNetworkChainId: { - if (NetworksModel.flatNetworks.count > 0) { - return ModelUtils.get(NetworksModel.flatNetworks, networksComboBox.currentIndex).chainId + if (networksComboBox.model.count > 0 && networksComboBox.currentIndex >= 0) { + return ModelUtils.get(networksComboBox.model, networksComboBox.currentIndex, "chainId") } return -1 } fromTokensKey: { if (d.tokenBySymbolModel.count > 0) { - return ModelUtils.get(d.tokenBySymbolModel, fromTokenComboBox.currentIndex).key + return ModelUtils.get(d.tokenBySymbolModel, fromTokenComboBox.currentIndex, "key") } return "" } fromTokenAmount: swapInput.text toTokenKey: { if (d.tokenBySymbolModel.count > 0) { - return ModelUtils.get(d.tokenBySymbolModel, toTokenComboBox.currentIndex).key + return ModelUtils.get(d.tokenBySymbolModel, toTokenComboBox.currentIndex, "key") } return "" } } + swapAdaptor: SwapModalAdaptor { + swapStore: SwapStore { + readonly property var accounts: d.accountsModel + readonly property var flatNetworks: d.flatNetworksModel + readonly property bool areTestNetworksEnabled: areTestNetworksEnabledCheckbox.checked + } + walletAssetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + readonly property var plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + currencyStore: CurrenciesStore {} + swapFormData: swapModal.swapInputParamsForm + } } } @@ -79,14 +105,32 @@ SplitView { ColumnLayout { spacing: 10 + CheckBox { + id: areTestNetworksEnabledCheckbox + text: "areTestNetworksEnabled" + checked: true + onCheckedChanged: networksComboBox.currentIndex = 0 + } + StatusBaseText { text:"Selected Account" } ComboBox { id: accountComboBox textRole: "name" - model: WalletSendAccountsModel {} + model: SortFilterProxyModel { + sourceModel: d.accountsModel + filters: ValueFilter { + roleName: "walletType" + value: Constants.watchWalletType + inverted: true + } + sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } + } currentIndex: 0 + onCurrentIndexChanged: { + swapModal.swapInputParamsForm.selectedAccountIndex = currentIndex + } } StatusBaseText { @@ -95,8 +139,9 @@ SplitView { ComboBox { id: networksComboBox textRole: "chainName" - model: NetworksModel.flatNetworks + model: d.filteredNetworksModel currentIndex: 0 + onCountChanged: currentIndex = 0 } StatusBaseText { diff --git a/storybook/qmlTests/tests/tst_SwapModal.qml b/storybook/qmlTests/tests/tst_SwapModal.qml new file mode 100644 index 0000000000..e2d1bb1be5 --- /dev/null +++ b/storybook/qmlTests/tests/tst_SwapModal.qml @@ -0,0 +1,283 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import StatusQ 0.1 // See #10218 +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils +import StatusQ.Core.Theme 0.1 + +import QtQuick.Controls 2.15 + +import Models 1.0 +import Storybook 1.0 + +import utils 1.0 +import shared.stores 1.0 +import AppLayouts.Wallet.popups.swap 1.0 +import AppLayouts.Wallet.stores 1.0 +import AppLayouts.Wallet 1.0 + +Item { + id: root + width: 600 + height: 400 + + readonly property var swapStore: SwapStore { + readonly property var accounts: WalletAccountsModel {} + readonly property var flatNetworks: NetworksModel.flatNetworks + readonly property bool areTestNetworksEnabled: true + } + + readonly property var swapAdaptor: SwapModalAdaptor { + currencyStore: CurrenciesStore {} + walletAssetsStore: WalletAssetsStore { + id: thisWalletAssetStore + walletTokensStore: TokensStore { + readonly property var plainTokensBySymbolModel: TokensBySymbolModel {} + } + readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {} + assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel + } + swapStore: root.swapStore + swapFormData: root.swapFormData + } + + readonly property var swapFormData: SwapInputParamsForm {} + + Component { + id: componentUnderTest + SwapModal { + swapInputParamsForm: root.swapFormData + swapAdaptor: root.swapAdaptor + } + } + + TestCase { + name: "SwapModal" + when: windowShown + + property SwapModal controlUnderTest: null + + // helper functions ------------------------------------------------------------- + function launchAndVerfyModal() { + verify(!!controlUnderTest) + controlUnderTest.open() + verify(!!controlUnderTest.opened) + } + + function closeAndVerfyModal() { + verify(!!controlUnderTest) + controlUnderTest.close() + verify(!controlUnderTest.opened) + } + + function getAndVerifyAccountsModalHeader() { + const accountsModalHeader = findChild(controlUnderTest, "accountsModalHeader") + verify(!!accountsModalHeader) + return accountsModalHeader + } + + function launchAccountSelectionPopup(accountsModalHeader) { + // Launch account selection popup + verify(!accountsModalHeader.control.popup.opened) + mouseClick(accountsModalHeader, Qt.LeftButton) + waitForRendering(accountsModalHeader) + verify(!!accountsModalHeader.control.popup.opened) + return accountsModalHeader + } + // end helper functions ------------------------------------------------------------- + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + + function test_floating_header_default_account() { + verify(!!controlUnderTest) + /* using a for loop set different accounts as default index and + check if the correct values are displayed in the floating header*/ + for (let i = 0; i< swapAdaptor.nonWatchAccounts.count; i++) { + root.swapFormData.selectedAccountIndex = i + + // Launch popup + launchAndVerfyModal() + + const floatingHeaderBackground = findChild(controlUnderTest, "headerBackground") + verify(!!floatingHeaderBackground) + compare(floatingHeaderBackground.color.toString().toUpperCase(), Utils.getColorForId(swapAdaptor.nonWatchAccounts.get(i).colorId).toString().toUpperCase()) + + const headerContentItemText = findChild(controlUnderTest, "headerContentItemText") + verify(!!headerContentItemText) + compare(headerContentItemText.text, swapAdaptor.nonWatchAccounts.get(i).name) + + const headerContentItemEmoji = findChild(controlUnderTest, "headerContentItemEmoji") + verify(!!headerContentItemEmoji) + compare(headerContentItemEmoji.emojiId, SQUtils.Emoji.iconId(swapAdaptor.nonWatchAccounts.get(i).emoji)) + } + closeAndVerfyModal() + } + + function test_floating_header_doesnt_contain_watch_accounts() { + // main input list from store should contian watch accounts + let hasWatchAccount = false + for(let i =0; i< swapStore.accounts.count; i++) { + if(swapStore.accounts.get(i).walletType === Constants.watchWalletType) { + hasWatchAccount = true + break + } + } + verify(!!hasWatchAccount) + + // launch modal and get the account selection header + launchAndVerfyModal() + const accountsModalHeader = getAndVerifyAccountsModalHeader() + + // header model should not contain watch accounts + let floatingHeaderHasWatchAccount = false + for(let i =0; i< accountsModalHeader.model.count; i++) { + if(accountsModalHeader.model.get(i).walletType === Constants.watchWalletType) { + floatingHeaderHasWatchAccount = true + break + } + } + verify(!floatingHeaderHasWatchAccount) + + closeAndVerfyModal() + } + + function test_floating_header_list_items() { + // Launch popup and account selection modal + launchAndVerfyModal() + const accountsModalHeader = getAndVerifyAccountsModalHeader() + launchAccountSelectionPopup(accountsModalHeader) + + const comboBoxList = findChild(controlUnderTest, "accountSelectorList") + verify(!!comboBoxList) + + for(let i =0; i< comboBoxList.model.count; i++) { + let delegateUnderTest = comboBoxList.itemAtIndex(i) + // check if the items are organized as per the position role + if(!!delegateUnderTest && !!comboBoxList.itemAtIndex(i+1)) { + verify(comboBoxList.itemAtIndex(i+1).modelData.position > delegateUnderTest.modelData.position) + } + compare(delegateUnderTest.title, swapAdaptor.nonWatchAccounts.get(i).name) + compare(delegateUnderTest.subTitle, SQUtils.Utils.elideText(swapAdaptor.nonWatchAccounts.get(i).address, 6, 4)) + compare(delegateUnderTest.asset.color.toString().toUpperCase(), swapAdaptor.nonWatchAccounts.get(i).color.toString().toUpperCase()) + compare(delegateUnderTest.asset.emoji, swapAdaptor.nonWatchAccounts.get(i).emoji) + + const walletAccountCurrencyBalance = findChild(delegateUnderTest, "walletAccountCurrencyBalance") + verify(!!walletAccountCurrencyBalance) + verify(walletAccountCurrencyBalance.text, LocaleUtils.currencyAmountToLocaleString(swapAdaptor.nonWatchAccounts.get(i).currencyBalance)) + + // check if selected item in combo box is highlighted with the right color + if(comboBoxList.currentIndex === i) { + verify(delegateUnderTest.color, Theme.palette.statusListItem.highlightColor) + } + else { + verify(delegateUnderTest.color, Theme.palette.transparent) + } + + // TODO: always null not sure why + // const walletAccountTypeIcon = findChild(delegateUnderTest, "walletAccountTypeIcon") + // verify(!!walletAccountTypeIcon) + // compare(walletAccountTypeIcon.icon, swapAdaptor.nonWatchAccounts.get(i).walletType === Constants.watchWalletType ? "show" : delegateUnderTest.modelData.migratedToKeycard ? "keycard": "") + + // Hover over the item and check hovered state + mouseMove(delegateUnderTest, delegateUnderTest.width/2, delegateUnderTest.height/2) + verify(delegateUnderTest.sensor.containsMouse) + compare(delegateUnderTest.subTitle, WalletUtils.colorizedChainPrefix(root.swapAdaptor.getNetworkShortNames(swapAdaptor.nonWatchAccounts.get(i).preferredSharingChainIds))) + verify(delegateUnderTest.color, Theme.palette.baseColor2) + + } + controlUnderTest.close() + } + + function test_floating_header_after_setting_fromAsset() { + // Launch popup + launchAndVerfyModal() + + // launch account selection dropdown + const accountsModalHeader = getAndVerifyAccountsModalHeader() + launchAccountSelectionPopup(accountsModalHeader) + + const comboBoxList = findChild(accountsModalHeader, "accountSelectorList") + verify(!!comboBoxList) + + // before setting network chainId and fromTokensKey the header should not have balances + for(let i =0; i< comboBoxList.model.count; i++) { + let delegateUnderTest = comboBoxList.itemAtIndex(i) + verify(!delegateUnderTest.modelData.fromToken) + verify(!delegateUnderTest.modelData.accountBalance) + } + + // close account selection dropdown + accountsModalHeader.control.popup.close() + + // set network chainId and fromTokensKey and verify balances in account selection dropdown + root.swapFormData.selectedNetworkChainId = root.swapAdaptor.__filteredFlatNetworksModel.get(0).chainId + root.swapFormData.fromTokensKey = root.swapAdaptor.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel.get(0).key + compare(controlUnderTest.swapInputParamsForm.selectedNetworkChainId, root.swapFormData.selectedNetworkChainId) + compare(controlUnderTest.swapInputParamsForm.fromTokensKey, root.swapFormData.fromTokensKey) + + // launch account selection dropdown + launchAccountSelectionPopup(accountsModalHeader) + verify(!!comboBoxList) + + for(let i =0; i< comboBoxList.model.count; i++) { + let delegateUnderTest = comboBoxList.itemAtIndex(i) + verify(!!delegateUnderTest.modelData.fromToken) + verify(!!delegateUnderTest.modelData.accountBalance) + compare(delegateUnderTest.inlineTagModel, 1) + + const inlineTagDelegate_0 = findChild(delegateUnderTest, "inlineTagDelegate_0") + verify(!!inlineTagDelegate_0) + + compare(inlineTagDelegate_0.asset.name, Style.svg("tiny/%1".arg(delegateUnderTest.modelData.accountBalance.iconUrl))) + compare(inlineTagDelegate_0.asset.color.toString().toUpperCase(), delegateUnderTest.modelData.accountBalance.chainColor.toString().toUpperCase()) + compare(inlineTagDelegate_0.titleText.color, delegateUnderTest.modelData.accountBalance.balance === "0" ? Theme.palette.baseColor1 : Theme.palette.directColor1) + + let bigIntBalance = SQUtils.AmountsArithmetic.toNumber(delegateUnderTest.modelData.accountBalance.balance, delegateUnderTest.modelData.fromToken.decimals) + compare(inlineTagDelegate_0.title, root.swapAdaptor.formatCurrencyAmount(bigIntBalance, delegateUnderTest.modelData.fromToken.symbol)) + } + + closeAndVerfyModal() + } + + function test_floating_header_selection() { + // Launch popup + launchAndVerfyModal() + + for(let i =0; i< swapAdaptor.nonWatchAccounts.count; i++) { + + // launch account selection dropdown + const accountsModalHeader = getAndVerifyAccountsModalHeader() + launchAccountSelectionPopup(accountsModalHeader) + + const comboBoxList = findChild(accountsModalHeader, "accountSelectorList") + verify(!!comboBoxList) + + let delegateUnderTest = comboBoxList.itemAtIndex(i) + + mouseClick(delegateUnderTest, Qt.LeftButton) + waitForRendering(delegateUnderTest) + verify(accountsModalHeader.control.popup.closed) + + // The input params form's slected Index should be updated as per this selection + compare(root.swapFormData.selectedAccountIndex, i) + + // The comboBox item should reflect chosen account + const floatingHeaderBackground = findChild(accountsModalHeader, "headerBackground") + verify(!!floatingHeaderBackground) + compare(floatingHeaderBackground.color.toString().toUpperCase(), swapAdaptor.nonWatchAccounts.get(i).color.toString().toUpperCase()) + + const headerContentItemText = findChild(accountsModalHeader, "headerContentItemText") + verify(!!headerContentItemText) + compare(headerContentItemText.text, swapAdaptor.nonWatchAccounts.get(i).name) + + const headerContentItemEmoji = findChild(accountsModalHeader, "headerContentItemEmoji") + verify(!!headerContentItemEmoji) + compare(headerContentItemEmoji.emojiId, SQUtils.Emoji.iconId(swapAdaptor.nonWatchAccounts.get(i).emoji)) + } + closeAndVerfyModal() + } + } +} diff --git a/storybook/src/Models/WalletAccountsModel.qml b/storybook/src/Models/WalletAccountsModel.qml index f90bff4b26..0c8c13b547 100644 --- a/storybook/src/Models/WalletAccountsModel.qml +++ b/storybook/src/Models/WalletAccountsModel.qml @@ -40,7 +40,13 @@ ListModel { symbol: "ZRX" } } - ] + ], + preferredSharingChainIds: "5:420:421613", + currencyBalance: ({amount: 1.25, + symbol: "USD", + displayDecimals: 4, + stripTrailingZeroes: false}), + migratedToKeycard: true }, { name: "Hot wallet (generated)", @@ -60,7 +66,13 @@ ListModel { symbol: "DBF" } } - ] + ], + preferredSharingChainIds: "5:420:421613", + currencyBalance: ({amount: 10, + symbol: "USD", + displayDecimals: 4, + stripTrailingZeroes: false}), + migratedToKeycard: false }, { name: "Family (seed)", @@ -89,7 +101,13 @@ ListModel { symbol: "DAI" } } - ] + ], + preferredSharingChainIds: "5:420:421613", + currencyBalance: ({amount: 110.05, + symbol: "USD", + displayDecimals: 4, + stripTrailingZeroes: false}), + migratedToKeycard: false }, { name: "Tag Heuer (watch)", @@ -100,7 +118,13 @@ ListModel { walletType: Constants.watchWalletType, position: 2, assets: [ - ] + ], + preferredSharingChainIds: "5:420:421613", + currencyBalance: ({amount: 3, + symbol: "USD", + displayDecimals: 4, + stripTrailingZeroes: false}), + migratedToKeycard: false }, { name: "Fab (key)", @@ -120,7 +144,13 @@ ListModel { symbol: "SOX" } } - ] + ], + preferredSharingChainIds: "5:420:421613", + currencyBalance: ({amount: 999, + symbol: "USD", + displayDecimals: 4, + stripTrailingZeroes: false}), + migratedToKeycard: false } ] diff --git a/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml b/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml new file mode 100644 index 0000000000..e529905281 --- /dev/null +++ b/storybook/stubs/AppLayouts/Wallet/stores/SwapStore.qml @@ -0,0 +1,5 @@ +import QtQuick 2.15 + +QtObject { + id: root +} diff --git a/storybook/stubs/AppLayouts/Wallet/stores/qmldir b/storybook/stubs/AppLayouts/Wallet/stores/qmldir index fb74d82631..9be18ea7ec 100644 --- a/storybook/stubs/AppLayouts/Wallet/stores/qmldir +++ b/storybook/stubs/AppLayouts/Wallet/stores/qmldir @@ -2,3 +2,4 @@ CollectiblesStore 1.0 CollectiblesStore.qml WalletAssetsStore 1.0 WalletAssetsStore.qml TokensStore 1.0 TokensStore.qml ActivityFiltersStore 1.0 ActivityFiltersStore.qml +SwapStore 1.0 SwapStore.qml diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index ab515d5941..2187b4d708 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -135,12 +135,12 @@ Item { RootStore.backButtonName = "" } - property SwapFormData swapFormData: SwapFormData { - selectedAccountIndex: RootStore.showAllAccounts ? 0 : leftTab.currentAccountIndex + property SwapInputParamsForm swapFormData: SwapInputParamsForm { + selectedAccountIndex: d.selectedAccountIndex selectedNetworkChainId: { // Without this when we switch testnet mode, the correct network is not evaluated RootStore.areTestNetworksEnabled - return StatusQUtils.ModelUtils.get(RootStore.filteredFlatModel, 0).chainId + return StatusQUtils.ModelUtils.get(RootStore.filteredFlatModel, 0, "chainId") } } @@ -167,6 +167,8 @@ Item { rightPanelStackView.currentItem.resetView() } } + + readonly property int selectedAccountIndex: RootStore.showAllAccounts ? 0 : leftTab.currentAccountIndex } SignPhraseModal { @@ -214,6 +216,7 @@ Item { hasFloatingButtons: true }) onLaunchSwapModal: { + d.swapFormData.selectedAccountIndex = d.selectedAccountIndex d.swapFormData.fromTokensKey = tokensKey Global.openSwapModalRequested(d.swapFormData) } @@ -330,6 +333,7 @@ Item { } onLaunchSwapModal: { d.swapFormData.fromTokensKey = "" + d.swapFormData.selectedAccountIndex = d.selectedAccountIndex if(!!walletStore.currentViewedHoldingTokensKey && walletStore.currentViewedHoldingType === Constants.TokenType.ERC20) { d.swapFormData.fromTokensKey = walletStore.currentViewedHoldingTokensKey } diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapFormData.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml similarity index 100% rename from ui/app/AppLayouts/Wallet/popups/swap/SwapFormData.qml rename to ui/app/AppLayouts/Wallet/popups/swap/SwapInputParamsForm.qml diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml index 49d295f517..86608146a1 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModal.qml @@ -5,14 +5,21 @@ import utils 1.0 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 as SQUtils import StatusQ.Popups.Dialog 0.1 +import shared.popups.send.controls 1.0 + StatusDialog { id: root - title: qsTr("Swap") - // This should be the only property which should be used when being launched from elsewhere - property SwapFormData formData: SwapFormData {} + /* This should be the only property which should be used to input + parameters to the modal when being launched from elsewhere */ + required property SwapInputParamsForm swapInputParamsForm + required property SwapModalAdaptor swapAdaptor + + objectName: "swapModal" + title: qsTr("Swap") bottomPadding: 16 padding: 0 @@ -23,32 +30,44 @@ StatusDialog { color: Theme.palette.baseColor3 } + header: AccountsModalHeader { + anchors.top: parent.top + anchors.topMargin: -height - 18 + control.popup.width: 512 + model: root.swapAdaptor.nonWatchAccounts + getNetworkShortNames: root.swapAdaptor.getNetworkShortNames + formatCurrencyAmount: root.swapAdaptor.formatCurrencyAmount + /* TODO: once the Account Header is reworked we simply should be + able to use an index and not this logic of selectedAccount being set */ + selectedAccount: root.swapAdaptor.getSelectedAccount(root.swapInputParamsForm.selectedAccountIndex) + onSelectedIndexChanged: { + root.swapInputParamsForm.selectedAccountIndex = selectedIndex + } + } + + // This is a temporary placeholder while each of the components are being added. contentItem: Column { spacing: 5 StatusBaseText { Layout.alignment: Qt.AlignHCenter - text: "This area is a temporary placeholder" + text: qsTr("This area is a temporary placeholder") font.bold: true } StatusBaseText { Layout.alignment: Qt.AlignHCenter - text: "Selected account index: %1".arg(formData.selectedAccountIndex) + text: qsTr("Selected network: %1").arg(swapInputParamsForm.selectedNetworkChainId) } StatusBaseText { Layout.alignment: Qt.AlignHCenter - text: "Selected network: %1".arg(formData.selectedNetworkChainId) + text: qsTr("Selected from token: %1").arg(swapInputParamsForm.fromTokensKey) } StatusBaseText { Layout.alignment: Qt.AlignHCenter - text: "Selected from token: %1".arg(formData.fromTokensKey) + text: qsTr("from token amount: %1").arg(swapInputParamsForm.fromTokenAmount) } StatusBaseText { Layout.alignment: Qt.AlignHCenter - text: "from token amount: %1".arg(formData.fromTokenAmount) - } - StatusBaseText { - Layout.alignment: Qt.AlignHCenter - text: "Selected to token: %1".arg(formData.toTokenKey) + text: qsTr("Selected to token: %1").arg(swapInputParamsForm.toTokenKey) } } } diff --git a/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml new file mode 100644 index 0000000000..cca304def0 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/popups/swap/SwapModalAdaptor.qml @@ -0,0 +1,98 @@ +import QtQml 2.15 +import SortFilterProxyModel 0.2 + +import StatusQ 0.1 +import StatusQ.Core.Utils 0.1 + +import utils 1.0 + +import shared.stores 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStore + +QObject { + id: root + + required property CurrenciesStore currencyStore + required property WalletStore.WalletAssetsStore walletAssetsStore + required property WalletStore.SwapStore swapStore + required property SwapInputParamsForm swapFormData + + readonly property var nonWatchAccounts: SortFilterProxyModel { + sourceModel: root.swapStore.accounts + filters: ValueFilter { + roleName: "walletType" + value: Constants.watchWalletType + inverted: true + } + sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } + proxyRoles: [ + FastExpressionRole { + name: "accountBalance" + expression: __processAccountBalance(model.address) + expectedRoles: ["address"] + }, + FastExpressionRole { + name: "fromToken" + expression: root.__selectedFromToken + } + ] + } + + function getNetworkShortNames(chainIds) { + var networkString = "" + let chainIdsArray = chainIds.split(":") + for (let i = 0; i< chainIdsArray.length; i++) { + let nwShortName = ModelUtils.getByKey(root.__filteredFlatNetworksModel, "chainId", Number(chainIdsArray[i]), "shortName") + if(!!nwShortName) { + networkString = networkString + nwShortName + ':' + } + } + return networkString + } + + function formatCurrencyAmount(balance, symbol) { + return root.currencyStore.formatCurrencyAmount(balance, symbol) + } + + // TODO: remove once the AccountsModalHeader is reworked!! + function getSelectedAccount(index) { + if (root.nonWatchAccounts.count > 0 && index >= 0) { + return ModelUtils.get(nonWatchAccounts, index) + } + return null + } + + // Internal properties and functions ----------------------------------------------------------------------------------------------------------------------------- + readonly property SortFilterProxyModel __filteredFlatNetworksModel: SortFilterProxyModel { + sourceModel: root.swapStore.flatNetworks + filters: ValueFilter { roleName: "isTest"; value: root.swapStore.areTestNetworksEnabled } + } + + /* TODO: the below logic is only needed until https://github.com/status-im/status-desktop/issues/14550 + is implemented then we should use that helper to connect the balances model with a wallet account */ + readonly property var __selectedFromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey) + readonly property var __balancesModelForSelectedFromToken: ModelUtils.getByKey(root.walletAssetsStore.baseGroupedAccountAssetModel, "tokensKey", root.swapFormData.fromTokensKey, "balances") + readonly property LeftJoinModel __networkJointBalancesModelForSelectedFromToken: LeftJoinModel { + leftModel: root.__balancesModelForSelectedFromToken + rightModel: root.__filteredFlatNetworksModel + joinRole: "chainId" + } + readonly property SortFilterProxyModel __filteredBalancesModelForSelectedFromToken: SortFilterProxyModel { + sourceModel: __networkJointBalancesModelForSelectedFromToken + filters: ValueFilter { roleName: "chainId"; value: root.swapFormData.selectedNetworkChainId} + } + function __processAccountBalance(address) { + let network = ModelUtils.getByKey(root.__filteredFlatNetworksModel, "chainId", root.swapFormData.selectedNetworkChainId) + if(!!network) { + let accountBalances = ModelUtils.getByKey(root.__filteredBalancesModelForSelectedFromToken, "account", address) + if(accountBalances === null) { + return { + balance: "0", + iconUrl: network.iconUrl, + chainColor: network.chainColor} + } + return accountBalances + } + return null + } +} diff --git a/ui/app/AppLayouts/Wallet/popups/swap/qmldir b/ui/app/AppLayouts/Wallet/popups/swap/qmldir index 01dff1bee0..22c81e3942 100644 --- a/ui/app/AppLayouts/Wallet/popups/swap/qmldir +++ b/ui/app/AppLayouts/Wallet/popups/swap/qmldir @@ -1,2 +1,3 @@ SwapModal 1.0 SwapModal.qml -SwapFormData 1.0 SwapFormData.qml +SwapInputParamsForm 1.0 SwapInputParamsForm.qml +SwapModalAdaptor 1.0 SwapModalAdaptor.qml diff --git a/ui/app/AppLayouts/Wallet/stores/SwapStore.qml b/ui/app/AppLayouts/Wallet/stores/SwapStore.qml new file mode 100644 index 0000000000..44ab78acaf --- /dev/null +++ b/ui/app/AppLayouts/Wallet/stores/SwapStore.qml @@ -0,0 +1,11 @@ +import QtQuick 2.15 + +QtObject { + id: root + + /* TODO: all of these should come from their respective stores once the stores are reworked and + streamlined. This store should contain only swap specific properties/methods if any */ + readonly property var accounts: walletSectionAccounts.accounts + readonly property var flatNetworks: networksModule.flatNetworks + readonly property bool areTestNetworksEnabled: networksModule.areTestNetworksEnabled +} diff --git a/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml b/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml index b1fb2d3b9f..e0358779ad 100644 --- a/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/WalletAssetsStore.qml @@ -15,6 +15,9 @@ QtObject { property TokensStore walletTokensStore + /* this property represents the grouped_account_assets_model from backend*/ + readonly property var baseGroupedAccountAssetModel: walletSectionAssets.groupedAccountAssetsModel + readonly property var assetsController: ManageTokensController { sourceModel: groupedAccountAssetsModel settingsKey: "WalletAssets" @@ -92,7 +95,7 @@ QtObject { /* This model joins the "Tokens by symbol model combined with Community details" and "Grouped Account Assets Model" by tokenskey */ property LeftJoinModel groupedAccountAssetsModel: LeftJoinModel { - leftModel: walletSectionAssets.groupedAccountAssetsModel + leftModel: root.baseGroupedAccountAssetModel rightModel: _jointTokensBySymbolModel joinRole: "tokensKey" } diff --git a/ui/app/AppLayouts/Wallet/stores/qmldir b/ui/app/AppLayouts/Wallet/stores/qmldir index aad08def2b..61315b0082 100644 --- a/ui/app/AppLayouts/Wallet/stores/qmldir +++ b/ui/app/AppLayouts/Wallet/stores/qmldir @@ -3,3 +3,4 @@ ActivityFiltersStore 1.0 ActivityFiltersStore.qml CollectiblesStore 1.0 CollectiblesStore.qml TokensStore 1.0 TokensStore.qml WalletAssetsStore 1.0 WalletAssetsStore.qml +SwapStore 1.0 SwapStore.qml diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index e95e07c860..37c3d0b915 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -387,7 +387,7 @@ QtObject { } function openSwapModal(parameters) { - openPopup(swapModal, {formData: parameters}) + openPopup(swapModal, {swapInputParamsForm: parameters}) } readonly property list _components: [ @@ -1240,6 +1240,12 @@ QtObject { Component { id: swapModal SwapModal { + swapAdaptor: SwapModalAdaptor { + swapStore: WalletStore.SwapStore {} + walletAssetsStore: root.walletAssetsStore + currencyStore: root.currencyStore + swapFormData: swapInputParamsForm + } onClosed: destroy() } } diff --git a/ui/imports/shared/controls/Padding.qml b/ui/imports/shared/controls/Padding.qml new file mode 100644 index 0000000000..b116de6dde --- /dev/null +++ b/ui/imports/shared/controls/Padding.qml @@ -0,0 +1,6 @@ +import QtQuick 2.15 + +Item { + implicitWidth: 1 + implicitHeight: 16 +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index 3df8219596..8c23fb410d 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -52,3 +52,4 @@ MockedKeycardReaderStateSelector 1.0 MockedKeycardReaderStateSelector.qml MockedKeycardStateSelector 1.0 MockedKeycardStateSelector.qml AssetsSectionDelegate 1.0 AssetsSectionDelegate.qml ExpandableTag 1.0 ExpandableTag.qml +Padding 1.0 Padding.qml diff --git a/ui/imports/shared/popups/send/controls/AccountsModalHeader.qml b/ui/imports/shared/popups/send/controls/AccountsModalHeader.qml index 2d44d6f937..efc9ffe9c5 100644 --- a/ui/imports/shared/popups/send/controls/AccountsModalHeader.qml +++ b/ui/imports/shared/popups/send/controls/AccountsModalHeader.qml @@ -8,15 +8,19 @@ import StatusQ.Core.Utils 0.1 as StatusQUtils import utils 1.0 -import "../controls" +import shared.controls 1.0 StatusComboBox { id: root property var selectedAccount property var getNetworkShortNames: function(chainIds){} + property var formatCurrencyAmount: function(balance, symbol){} property int selectedIndex: -1 + objectName: "accountsModalHeader" + popupContentItemObjectName: "accountSelectorList" + control.padding: 0 control.spacing: 0 control.leftPadding: 8 @@ -27,6 +31,8 @@ StatusComboBox { control.indicator: null control.background: Rectangle { + objectName: "headerBackground" + width: contentItem.childrenRect.width + control.leftPadding + control.rightPadding height: 32 radius: 8 @@ -43,21 +49,19 @@ StatusComboBox { anchors.verticalCenter: parent.verticalCenter width: childrenRect.width spacing: 8 - component Padding: Item { - width: 12 - height: 16 - } Padding {} StatusEmoji { + objectName: "headerContentItemEmoji" anchors.verticalCenter: parent.verticalCenter width: 16 height: 16 - emojiId: StatusQUtils.Emoji.iconId(selectedAccount.emoji ?? "", StatusQUtils.Emoji.size.verySmall) || "" + emojiId: StatusQUtils.Emoji.iconId(!!selectedAccount && !!selectedAccount.emoji ? selectedAccount.emoji : "", StatusQUtils.Emoji.size.verySmall) || "" visible: !!emojiId } StatusBaseText { + objectName: "headerContentItemText" anchors.verticalCenter: parent.verticalCenter - text: selectedAccount.name ?? "" + text: !!selectedAccount && !!selectedAccount.name ? selectedAccount.name : "" font.pixelSize: 15 color: Theme.palette.indirectColor1 } @@ -76,15 +80,16 @@ StatusComboBox { width: ListView.view.width modelData: model getNetworkShortNames: root.getNetworkShortNames + formatCurrencyAmount: root.formatCurrencyAmount color: sensor.containsMouse || highlighted ? Theme.palette.baseColor2 : - selectedAccount.name === model.name ? Theme.palette.statusListItem.highlightColor : "transparent" + !!selectedAccount && selectedAccount.name === model.name ? Theme.palette.statusListItem.highlightColor : "transparent" onClicked: { selectedIndex = index control.popup.close() } Component.onCompleted:{ - if(selectedAccount.address === model.address) + if(!!selectedAccount && selectedAccount.address === model.address) selectedIndex = index } } diff --git a/ui/imports/shared/popups/send/controls/WalletAccountListItem.qml b/ui/imports/shared/popups/send/controls/WalletAccountListItem.qml index e8e2e7f891..00945fb924 100644 --- a/ui/imports/shared/popups/send/controls/WalletAccountListItem.qml +++ b/ui/imports/shared/popups/send/controls/WalletAccountListItem.qml @@ -1,5 +1,6 @@ import QtQuick 2.15 +import StatusQ 0.1 import StatusQ.Components 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core 0.1 @@ -16,6 +17,7 @@ StatusListItem { property var modelData property var getNetworkShortNames: function(chainIds){} property bool clearVisible: false + property var formatCurrencyAmount: function(balances, symbols){} signal cleared() objectName: !!modelData ? modelData.name: "" @@ -45,25 +47,19 @@ StatusListItem { Column { anchors.verticalCenter: parent.verticalCenter StatusTextWithLoadingState { + objectName: "walletAccountCurrencyBalance" anchors.right: parent.right font.pixelSize: 15 text: LocaleUtils.currencyAmountToLocaleString(!!modelData ? modelData.currencyBalance: "") } - Row { + StatusIcon { + objectName: "walletAccountTypeIcon" anchors.right: parent.right - spacing: 6 - StatusIcon { - width: !!icon ? 15: 0 - height: !!icon ? 15 : 0 - color: Theme.palette.directColor1 - icon: !!modelData && modelData.walletType === Constants.watchWalletType ? "show" : "" - } - StatusIcon { - width: !!icon ? 15: 0 - height: !!icon ? 15 : 0 - color: Theme.palette.directColor1 - icon: !!modelData && modelData.migratedToKeycard ? "keycard" : "" - } + width: !!icon ? 15: 0 + height: !!icon ? 15 : 0 + color: Theme.palette.directColor1 + icon: !!modelData ? modelData.walletType === Constants.watchWalletType ? "show" : + modelData.migratedToKeycard ? "keycard" : "" : "" } }, ClearButton { @@ -74,4 +70,21 @@ StatusListItem { onClicked: root.cleared() } ] + + inlineTagModel: !!root.modelData.fromToken && !!root.modelData.accountBalance ? 1 : 0 + inlineTagDelegate: StatusListItemTag { + objectName: "inlineTagDelegate_" + index + readonly property double balance: StatusQUtils.AmountsArithmetic.toNumber(root.modelData.accountBalance.balance, root.modelData.fromToken.decimals) + background: null + height: 16 + asset.height: 16 + asset.width: 16 + title: root.formatCurrencyAmount(balance, root.modelData.fromToken.symbol) + titleText.font.pixelSize: 12 + titleText.color: balance === 0 ? Theme.palette.baseColor1 : Theme.palette.directColor1 + asset.isImage: true + asset.name: Style.svg("tiny/%1".arg(root.modelData.accountBalance.iconUrl)) + asset.color: root.modelData.accountBalance.chainColor + closeButtonVisible: false + } } diff --git a/ui/imports/shared/popups/send/views/NetworkSelector.qml b/ui/imports/shared/popups/send/views/NetworkSelector.qml index 057abbac72..9f0e3cc2a5 100644 --- a/ui/imports/shared/popups/send/views/NetworkSelector.qml +++ b/ui/imports/shared/popups/send/views/NetworkSelector.qml @@ -10,7 +10,7 @@ import StatusQ.Components 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 -import "../controls" +import shared.popups.send.controls 1.0 Item { id: root