diff --git a/storybook/pages/SendModalPage.qml b/storybook/pages/SendModalPage.qml index bfe6470786..e7fd525bf0 100644 --- a/storybook/pages/SendModalPage.qml +++ b/storybook/pages/SendModalPage.qml @@ -4,9 +4,11 @@ import QtQuick.Layouts 1.15 import StatusQ 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 import StatusQ.Controls 0.1 import Storybook 1.0 +import Models 1.0 import utils 1.0 import shared.popups.send 1.0 @@ -22,18 +24,68 @@ SplitView { orientation: Qt.Horizontal property WalletAssetsStore walletAssetStore: WalletAssetsStore { - assetsWithFilteredBalances: root.assetsWithFilteredBalances - } + // Workaround to satisfy stub which is not empty (but should be) + assetsWithFilteredBalances: ListModel {} - property SubmodelProxyModel assetsWithFilteredBalances: SubmodelProxyModel { - sourceModel: root.walletAssetStore.groupedAccountsAssetsModel - submodelRoleName: "balances" - delegateModel: SortFilterProxyModel { - sourceModel: submodel - filters: FastExpressionFilter { - expression: txStore.selectedSenderAccountAddress === model.account - expectedRoles: ["account"] + property var groupedAccountAssetsModel: ListModel { + Component.onCompleted: { + const data = [ + { + tokensKey: "key_eth", + name: "Ethereum", + symbol: "ETH", + decimals: 18, + communityId: "", + balances: [ + { + chainId: "1", + balance: "122082928968121891", + account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240", + }, + { + chainId: "420", + balance: "559133758939097000", + account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + } + ], + currentCurrencyBalance: 234.234, + marketDetails: { + currencyPrice: { + amount: 12234.23, + displayDecimals: true + } + } + }, + { + tokensKey: "key_dai", + name: "DAI", + symbol: "DAI", + decimals: 18, + communityId: "", + balances: [ + { + chainId: "420", + balance: "1142155111", + account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + }, + { + chainId: "1", + balance: "4411211243121551121", + account: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + } + ], + currentCurrencyBalance: 234.234, + marketDetails: { + currencyPrice: { + amount: 234.23, + displayDecimals: true + } + } + } + ] + + append(data) } } } diff --git a/storybook/stubs/shared/stores/send/TransactionStore.qml b/storybook/stubs/shared/stores/send/TransactionStore.qml index 70fdfe0e76..67e786bdbb 100644 --- a/storybook/stubs/shared/stores/send/TransactionStore.qml +++ b/storybook/stubs/shared/stores/send/TransactionStore.qml @@ -9,6 +9,16 @@ import SortFilterProxyModel 0.2 import AppLayouts.Wallet.stores 1.0 +// TODO: This store, as all other stores should be empty QtObject {}. +// All mocking should be done in place in Storybook pages and unit tests. +// If it's necessary to share mocks between tests/pages, such mock can be +// created by deriving from empty stub and putting in mocks dir. +// Stores itself should be simple, thin layers over functionality exposed from +// the backend. No additional logic should there. Data transformation logic +// should be delegated to adaptors, stateles helpers to proper utility singletons. +// +// PLEASE DO NOT ADD ANY NEW CONTENT HERE + QtObject { id: root diff --git a/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml index 1c055d1f2c..a4f21463f5 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/adaptors/TokenSelectorViewAdaptor.qml @@ -53,6 +53,9 @@ QObject { // output model readonly property SortFilterProxyModel outputAssetsModel: SortFilterProxyModel { + + objectName: "TokenSelectorViewAdaptor_outputAssetsModel" + sourceModel: showAllTokens && !!plainTokensBySymbolModel ? concatModel : assetsObjectProxyModel proxyRoles: [ @@ -135,6 +138,7 @@ QObject { } expression: isPresentOnEnabledNetworks(model.addressPerChain) expectedRoles: ["addressPerChain"] + enabled: root.enabledChainIds.length } ] @@ -203,6 +207,8 @@ QObject { id: assetsObjectProxyModel sourceModel: root.assetsModel + objectName: "TokenSelectorViewAdaptor_assetsObjectProxyModel" + delegate: SortFilterProxyModel { id: delegateRoot diff --git a/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml b/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml index 09e0675080..aae6ac1b24 100644 --- a/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/CollectiblesStore.qml @@ -31,7 +31,7 @@ QtObject { } readonly property var collectiblesController: ManageTokensController { - sourceModel: _jointCollectiblesBySymbolModel + sourceModel: root.jointCollectiblesBySymbolModel settingsKey: "WalletCollectibles" serializeAsCollectibles: true @@ -85,8 +85,8 @@ QtObject { ] } - /* PRIVATE: This model joins the "Tokens By Symbol Model" and "Communities Model" by communityId */ - property LeftJoinModel _jointCollectiblesBySymbolModel: LeftJoinModel { + /* TODO: move all transformations to a dedicated adaptors */ + readonly property LeftJoinModel jointCollectiblesBySymbolModel: LeftJoinModel { objectName: "jointCollectiblesBySymbolModel" leftModel: allCollectiblesModel diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 57d86dd469..ce036ff5c4 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -1522,8 +1522,12 @@ Item { sourceComponent: SendPopups.SendModal { onlyAssets: sendModal.onlyAssets - store: appMain.transactionStore + loginType: appMain.rootStore.loginType + + store: appMain.transactionStore + collectiblesStore: appMain.walletCollectiblesStore + onClosed: { sendModal.closed() sendModal.preSelectedSendType = Constants.SendType.Unknown diff --git a/ui/imports/shared/popups/send/SendModal.qml b/ui/imports/shared/popups/send/SendModal.qml index 693ce5024f..482b803830 100644 --- a/ui/imports/shared/popups/send/SendModal.qml +++ b/ui/imports/shared/popups/send/SendModal.qml @@ -1,6 +1,6 @@ -import QtQuick 2.13 -import QtQuick.Controls 2.13 -import QtQuick.Layouts 1.13 +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import QtQuick.Dialogs 1.3 import QtGraphicalEffects 1.0 import SortFilterProxyModel 0.2 @@ -20,8 +20,10 @@ 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 AppLayouts.Wallet.adaptors 1.0 +import AppLayouts.Wallet.controls 1.0 +import AppLayouts.Wallet.panels 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores import shared.popups.send.panels 1.0 import "./controls" @@ -48,7 +50,8 @@ StatusDialog { property alias modalHeader: modalHeader.text required property TransactionStore store - property var nestedCollectiblesModel: store.nestedCollectiblesModel + property WalletStores.CollectiblesStore collectiblesStore + property var bestRoutes property bool isLoading: false property int loginType @@ -117,40 +120,6 @@ StatusDialog { property var hoveredHoldingType: Constants.TokenType.Unknown readonly property bool isHoveredHoldingValidAsset: !!hoveredHolding && hoveredHoldingType === Constants.TokenType.ERC20 - function getHolding(holdingId, holdingType) { - if (holdingType === Constants.TokenType.ERC20) { - return store.getAsset(assetsAdaptor.model, holdingId) - } else if (holdingType === Constants.TokenType.ERC721 || holdingType === Constants.TokenType.ERC1155) { - return store.getCollectible(holdingId) - } else { - return {} - } - } - - function setSelectedHoldingId(holdingId, holdingType) { - let holding = getHolding(holdingId, holdingType) - setSelectedHolding(holding, holdingType) - } - - function setSelectedHolding(holding, holdingType) { - d.selectedHoldingType = holdingType - d.selectedHolding = holding - let selectorHolding = store.holdingToSelectorHolding(holding, holdingType) - holdingSelector.setSelectedItem(selectorHolding, holdingType) - } - - function setHoveredHoldingId(holdingId, holdingType) { - let holding = getHolding(holdingId, holdingType) - setHoveredHolding(holding, holdingType) - } - - function setHoveredHolding(holding, holdingType) { - d.hoveredHoldingType = holdingType - d.hoveredHolding = holding - let selectorHolding = store.holdingToSelectorHolding(holding, holdingType) - holdingSelector.setHoveredItem(selectorHolding, holdingType) - } - onSelectedHoldingChanged: { if (d.selectedHoldingType === Constants.TokenType.ERC20) { if(!d.ensOrStickersPurpose && store.sendType !== Constants.SendType.Bridge) @@ -173,19 +142,6 @@ StatusDialog { } } - SendModalAssetsAdaptor { - id: assetsAdaptor - - controller: popup.store.walletAssetStore.assetsController - showCommunityAssets: popup.store.tokensStore.showCommunityAssetsInSend - tokensModel: popup.store.walletAssetStore.groupedAccountAssetsModel - account: popup.store.selectedSenderAccountAddress - marketValueThreshold: - popup.store.tokensStore.displayAssetsBelowBalance - ? popup.store.tokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount() - : 0 - } - LeftJoinModel { id: fromNetworksRouteModel leftModel: popup.store.fromNetworksRouteModel @@ -219,11 +175,35 @@ StatusDialog { if(popup.preSelectedSendType !== Constants.SendType.Unknown) { store.setSendType(popup.preSelectedSendType) } - if ((popup.preSelectedHoldingType > Constants.TokenType.Native) && - (popup.preSelectedHoldingType < Constants.TokenType.Unknown)) { - tokenListRect.browsingHoldingType = popup.preSelectedHoldingType - if (!!popup.preSelectedHoldingID) { - d.setSelectedHoldingId(popup.preSelectedHoldingID, popup.preSelectedHoldingType) + if (!!popup.preSelectedHoldingID + && popup.preSelectedHoldingType > Constants.TokenType.Native + && popup.preSelectedHoldingType < Constants.TokenType.Unknown) { + + if (popup.preSelectedHoldingType === Constants.TokenType.ERC20) { + const entry = ModelUtils.getByKey( + assetsAdaptor.outputAssetsModel, "tokensKey", + popup.preSelectedHoldingID) + d.selectedHoldingType = Constants.TokenType.ERC20 + d.selectedHolding = entry + + holdingSelector.setCustom(entry.symbol, entry.iconSource, + popup.preSelectedHoldingID) + holdingSelector.selectedItem = entry + } else { + const entry = ModelUtils.getByKey( + popup.store.collectiblesModel, + "uid", popup.preSelectedHoldingID) + + d.selectedHoldingType = entry.tokenType + d.selectedHolding = entry + + const id = entry.communityId ? entry.collectionUid : entry.uid + + holdingSelector.setCustom(entry.name, + entry.imageUrl || entry.mediaUrl, + id) + holdingSelector.selectedItem = entry + holdingSelector.currentTab = TokenSelectorPanel.Tabs.Collectibles } } @@ -267,9 +247,13 @@ StatusDialog { selectedAddress: !!popup.preSelectedAccount && !!popup.preSelectedAccount.address ? popup.preSelectedAccount.address : "" onCurrentAccountAddressChanged: { store.setSenderAccount(currentAccountAddress) + if (d.isSelectedHoldingValidAsset) { - d.setSelectedHoldingId(d.selectedHolding.symbol, d.selectedHoldingType) + d.selectedHolding = ModelUtils.getByKey( + holdingSelector.assetsModel, "tokensKey", + d.selectedHolding.tokensKey) } + popup.recalculateRoutesAndFees() } } @@ -316,25 +300,69 @@ StatusDialog { text: d.isBridgeTx ? qsTr("Bridge") : qsTr("Send") } - HoldingSelector { + TokenSelectorNew { id: holdingSelector + + property var selectedItem + property bool onlyAssets: false + + assetsModel: assetsAdaptor.outputAssetsModel + collectiblesModel: collectiblesAdaptorLoader.active + ? collectiblesAdaptorLoader.item.model : null + + TokenSelectorViewAdaptor { + id: assetsAdaptor + + assetsModel: popup.store.walletAssetStore.groupedAccountAssetsModel + + flatNetworksModel: popup.store.flatNetworksModel + currentCurrency: popup.store.currencyStore.currentCurrency + accountAddress: popup.preSelectedAccount ? popup.preSelectedAccount.address : "" + showCommunityAssets: popup.store.tokensStore.showCommunityAssetsInSend + } + + Loader { + id: collectiblesAdaptorLoader + + active: !d.isBridgeTx + + sourceComponent: CollectiblesSelectionAdaptor { + accountKey: popup.preSelectedAccount ? popup.preSelectedAccount.address : "" + collectiblesModel: collectiblesStore + ? collectiblesStore.jointCollectiblesBySymbolModel + : null + } + } + + onAssetSelected: { + const entry = ModelUtils.getByKey( + assetsModel, "tokensKey", key) + d.selectedHoldingType = Constants.TokenType.ERC20 + d.selectedHolding = entry + selectedItem = entry + } + + onCollectibleSelected: { + const entry = ModelUtils.getByKey( + popup.store.collectiblesModel, + "uid", key) + d.selectedHoldingType = entry.tokenType + d.selectedHolding = entry + selectedItem = entry + } + + onCollectionSelected: { + const entry = ModelUtils.getByKey( + popup.store.collectiblesModel, + "collectionUid", key) + d.selectedHoldingType = entry.tokenType + d.selectedHolding = entry + selectedItem = entry + } + } + + Item { Layout.fillWidth: true - Layout.fillHeight: true - assetsModel: assetsAdaptor.model - collectiblesModel: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null - networksModel: popup.store.flatNetworksModel - visible: (!!d.selectedHolding && d.selectedHoldingType !== Constants.TokenType.Unknown) || - (!!d.hoveredHolding && d.hoveredHoldingType !== Constants.TokenType.Unknown) - onItemSelected: { - d.setSelectedHoldingId(holdingId, holdingType) - } - onSearchTextChanged: assetsAdaptor.assetSearchString = assetSearchString - formatCurrentCurrencyAmount: function(balance){ - return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency) - } - formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){ - return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true}) - } } MaxSendButton { @@ -404,7 +432,7 @@ StatusDialog { ColumnLayout { spacing: 8 Layout.fillWidth: true - visible: !d.isBridgeTx && !!d.selectedHolding + visible: !d.isBridgeTx StatusBaseText { id: label elide: Text.ElideRight @@ -429,39 +457,6 @@ StatusDialog { } } - TokenListView { - id: tokenListRect - Layout.fillHeight: true - Layout.fillWidth: true - Layout.topMargin: Style.current.padding - Layout.leftMargin: Style.current.xlPadding - Layout.rightMargin: Style.current.xlPadding - Layout.bottomMargin: Style.current.xlPadding + Style.current.padding - visible: !d.selectedHolding - - assets: assetsAdaptor.model - collectibles: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null - networksModel: popup.store.flatNetworksModel - onlyAssets: holdingSelector.onlyAssets - onTokenSelected: function (symbolOrTokenKey, holdingType) { - d.setSelectedHoldingId(symbolOrTokenKey, holdingType) - } - onTokenHovered: { - if(hovered) { - d.setHoveredHoldingId(symbol, holdingType) - } else { - d.setHoveredHoldingId("", Constants.TokenType.Unknown) - } - } - onAssetSearchStringChanged: assetsAdaptor.assetSearchString = assetSearchString - formatCurrentCurrencyAmount: function(balance){ - return popup.store.currencyStore.formatCurrencyAmount(balance, popup.store.currencyStore.currentCurrency) - } - formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) { - return popup.store.formatCurrencyAmountFromBigInt(balance, symbol, decimals, {noSymbol: true}) - } - } - RecipientSelectorPanel { id: recipientsPanel @@ -472,9 +467,7 @@ StatusDialog { Layout.rightMargin: Style.current.xlPadding Layout.bottomMargin: Style.current.padding - // TODO: To be removed after all other refactors done (initial tokens selector page removed, bridge modal separated) - // This panel must be shown by default if no recipient already selected, otherwise, hidden - visible: !recipientInputLoader.ready && !d.isBridgeTx && !!d.selectedHolding + visible: !recipientInputLoader.ready && !d.isBridgeTx savedAddressesModel: popup.store.savedAddressesModel myAccountsModel: d.accountsAdaptor.model @@ -513,7 +506,8 @@ StatusDialog { contentWidth: availableWidth - visible: recipientInputLoader.ready && !!d.selectedHolding && (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) + visible: recipientInputLoader.ready && + (amountToSendInput.inputNumberValid || d.isCollectiblesTransfer) objectName: "sendModalScroll" diff --git a/ui/imports/shared/popups/send/models/SendModalAssetsAdaptor.qml b/ui/imports/shared/popups/send/models/SendModalAssetsAdaptor.qml index 166515015b..063446bd0f 100644 --- a/ui/imports/shared/popups/send/models/SendModalAssetsAdaptor.qml +++ b/ui/imports/shared/popups/send/models/SendModalAssetsAdaptor.qml @@ -83,6 +83,8 @@ QObject { ObjectProxyModel { id: proxyModel + objectName: "sendModalAssetsAdaptor_proxyModel" + sourceModel: root.tokensModel ?? null delegate: QObject { diff --git a/ui/imports/shared/popups/send/views/RecipientView.qml b/ui/imports/shared/popups/send/views/RecipientView.qml index 79deac2c50..862faebbaa 100644 --- a/ui/imports/shared/popups/send/views/RecipientView.qml +++ b/ui/imports/shared/popups/send/views/RecipientView.qml @@ -180,7 +180,7 @@ Loader { SendRecipientInput { width: parent.width height: visible ? implicitHeight: 0 - visible: !root.isBridgeTx && !!root.selectedAsset + visible: !root.isBridgeTx text: root.addressText function validateInput() {