feat(@desktop/wallet): Buy Mercuryo flow - Improvements

fixes #16041
This commit is contained in:
Khushboo Mehta 2024-08-13 14:03:35 +02:00 committed by Khushboo-dev-cpp
parent 2c2f87f654
commit b8f41e35c4
8 changed files with 499 additions and 308 deletions

View File

@ -5,10 +5,12 @@ import QtQuick.Layouts 1.15
import Storybook 1.0
import Models 1.0
import StatusQ 0.1
import StatusQ.Core.Backpressure 0.1
import AppLayouts.Wallet.popups.buy 1.0
import AppLayouts.Wallet.stores 1.0
import AppLayouts.Wallet.adaptors 1.0
import shared.stores 1.0
@ -47,6 +49,22 @@ SplitView {
d.debounceFetchProviderUrl()
}
}
readonly property var currencyStore: CurrenciesStore {}
readonly property var assetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
readonly property BuyCryptoParamsForm buyCryptoInputParamsForm: BuyCryptoParamsForm{
selectedWalletAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
selectedNetworkChainId: 11155111
selectedTokenKey: "ETH"
}
}
PopupBackground {
@ -61,7 +79,12 @@ SplitView {
text: "Reopen"
enabled: !buySellModal.visible
onClicked: buySellModal.open()
onClicked: {
buySellModal.buyCryptoInputParamsForm.selectedWalletAddress = "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
buySellModal.buyCryptoInputParamsForm.selectedNetworkChainId = 11155111
buySellModal.buyCryptoInputParamsForm.selectedTokenKey = "ETH"
buySellModal.open()
}
}
BuyCryptoModal {
@ -70,29 +93,19 @@ SplitView {
visible: true
modal: false
closePolicy: Popup.CloseOnEscape
buyCryptoAdaptor: BuyCryptoModalAdaptor {
buyCryptoStore: d.buyCryptoStore
readonly property var currencyStore: CurrenciesStore {}
readonly property var assetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
buyCryptoFormData: buySellModal.buyCryptoInputParamsForm
buyProvidersModel: d.buyCryptoStore.providersModel
isBuyProvidersModelLoading: d.buyCryptoStore.areProvidersLoading
walletAccountsModel: WalletAccountsModel{}
networksModel: NetworksModel.flatNetworks
areTestNetworksEnabled: true
groupedAccountAssetsModel: assetsStore.groupedAccountAssetsModel
plainTokensBySymbolModel: assetsStore.walletTokensStore.plainTokensBySymbolModel
currentCurrency: currencyStore.currentCurrency
}
buyCryptoInputParamsForm: BuyCryptoParamsForm{
selectedWalletAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
selectedNetworkChainId: 11155111
selectedTokenKey: "ETH"
currentCurrency: d.currencyStore.currentCurrency
plainTokensBySymbolModel: d.assetsStore.walletTokensStore.plainTokensBySymbolModel
groupedAccountAssetsModel: d.assetsStore.groupedAccountAssetsModel
buyCryptoInputParamsForm: d.buyCryptoInputParamsForm
Component.onCompleted: {
fetchProviders.connect(d.buyCryptoStore.fetchProviders)
fetchProviderUrl.connect(d.buyCryptoStore.fetchProviderUrl)
d.buyCryptoStore.providerUrlReady.connect(providerUrlReady)
}
}
}

View File

@ -3,6 +3,8 @@ import QtTest 1.15
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Core.Backpressure 0.1
@ -22,22 +24,44 @@ Item {
QtObject {
id: d
property string uuid
property var debounceFetchProviderUrl: Backpressure.debounce(root, 500, function() {
d.buyCryptoStore.providerUrlReady(d.uuid, "xxxx")
})
property var debounceFetchProvidersList: Backpressure.debounce(root, 500, function() {
d.buyCryptoStore.areProvidersLoading = false
})
}
Component {
id: componentUnderTest
BuyCryptoModal {
id: buySellModal
buyProvidersModel: buyCryptoStore.providersModel
isBuyProvidersModelLoading: buyCryptoStore.areProvidersLoading
currentCurrency: currencyStore.currentCurrency
walletAccountsModel: WalletAccountsModel{}
networksModel: NetworksModel.flatNetworks
areTestNetworksEnabled: true
plainTokensBySymbolModel: assetsStore.walletTokensStore.plainTokensBySymbolModel
groupedAccountAssetsModel: assetsStore.groupedAccountAssetsModel
buyCryptoInputParamsForm: BuyCryptoParamsForm {
selectedWalletAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
selectedNetworkChainId: 11155111
selectedTokenKey: "ETH"
}
Component.onCompleted: {
fetchProviders.connect(buyCryptoStore.fetchProviders)
fetchProviderUrl.connect(buyCryptoStore.fetchProviderUrl)
buyCryptoStore.providerUrlReady.connect(buySellModal.providerUrlReady)
}
// Temporary assignments to make tests run independently
readonly property var currencyStore: CurrenciesStore {}
readonly property var buyCryptoStore: BuyCryptoStore {
readonly property var providersModel: _onRampProvidersModel
readonly property var providersModel: OnRampProvidersModel{}
property bool areProvidersLoading
signal providerUrlReady(string uuid ,string url)
function fetchProviders() {
console.warn("fetchProviders called >>")
areProvidersLoading = true
d.debounceFetchProvidersList()
debounceFetchProvidersList()
}
function fetchProviderUrl(uuid, providerID,
@ -46,32 +70,27 @@ Item {
console.warn("fetchProviderUrl called >> uuid: ", uuid, "providerID: ",providerID
, "isRecurrent: ", isRecurrent, "accountAddress: ", accountAddress,
"chainID: ", chainID, "symbol: ", symbol)
d.uuid = uuid
d.debounceFetchProviderUrl()
buySellModal.uuid = uuid
debounceFetchProviderUrl()
}
}
readonly property ModelEntry selectedAccountEntry: ModelEntry {
sourceModel: walletAccountsModel
key: "address"
value: buyCryptoInputParamsForm.selectedWalletAddress
}
OnRampProvidersModel{
id: _onRampProvidersModel
readonly property ModelEntry selectedProviderEntry: ModelEntry {
sourceModel: buyCryptoStore.providersModel
key: "id"
value: buyCryptoInputParamsForm.selectedProviderId
}
SortFilterProxyModel {
id: recurrentOnRampProvidersModel
sourceModel: _onRampProvidersModel
readonly property var recurrentOnRampProvidersModel: SortFilterProxyModel {
sourceModel: buyProvidersModel
filters: ValueFilter {
roleName: "supportsRecurrentPurchase"
value: true
}
}
Component {
id: componentUnderTest
BuyCryptoModal {
id: buySellModal
buyCryptoAdaptor: BuyCryptoModalAdaptor {
buyCryptoStore: d.buyCryptoStore
readonly property var currencyStore: CurrenciesStore {}
readonly property var assetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
@ -80,19 +99,13 @@ Item {
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
buyCryptoFormData: buySellModal.buyCryptoInputParamsForm
walletAccountsModel: WalletAccountsModel{}
networksModel: NetworksModel.flatNetworks
areTestNetworksEnabled: true
groupedAccountAssetsModel: assetsStore.groupedAccountAssetsModel
plainTokensBySymbolModel: assetsStore.walletTokensStore.plainTokensBySymbolModel
currentCurrency: currencyStore.currentCurrency
}
buyCryptoInputParamsForm: BuyCryptoParamsForm{
selectedWalletAddress: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
selectedNetworkChainId: 11155111
selectedTokenKey: "ETH"
}
property string uuid
property var debounceFetchProviderUrl: Backpressure.debounce(root, 500, function() {
buySellModal.buyCryptoStore.providerUrlReady(uuid, "xxxx")
})
property var debounceFetchProvidersList: Backpressure.debounce(root, 500, function() {
buySellModal.buyCryptoStore.areProvidersLoading = false
})
}
}
@ -109,11 +122,15 @@ Item {
property BuyCryptoModal controlUnderTest: null
function init() {
notificationSpy.clear()
controlUnderTest = createTemporaryObject(componentUnderTest, root)
}
function launchPopup() {
verify(!!controlUnderTest)
controlUnderTest.buyCryptoInputParamsForm.selectedWalletAddress = "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
controlUnderTest.buyCryptoInputParamsForm.selectedNetworkChainId = 11155111
controlUnderTest.buyCryptoInputParamsForm.selectedTokenKey = "ETH"
controlUnderTest.open()
verify(!!controlUnderTest.opened)
}
@ -126,26 +143,152 @@ Item {
compare(delegateUnderTest.title, modelToCompareAgainst.get(i).name)
compare(delegateUnderTest.subTitle, modelToCompareAgainst.get(i).description)
compare(delegateUnderTest.asset.name, modelToCompareAgainst.get(i).logoUrl)
compare(delegateUnderTest.isUrlLoading, false)
const feesText = findChild(delegateUnderTest, "feesText")
verify(!!feesText)
compare(feesText.text, modelToCompareAgainst.get(i).fees)
/* TODO: fix when writing more tests for this functionality
const externalLinkIcon = findChild(delegateUnderTest, "externalLinkIcon")
verify(!!externalLinkIcon)
compare(externalLinkIcon.icon, "tiny/external")
compare(externalLinkIcon.color, Theme.palette.baseColor1) */
const loadingIndicator = findChild(delegateUnderTest, "loadingIndicator")
verify(!!loadingIndicator)
verify(!loadingIndicator.visible)
var extraIcon = null
if (modelToCompareAgainst.get(i).urlsNeedParameters) {
extraIcon = findChild(delegateUnderTest, "chevron-down-icon")
verify(!!extraIcon)
compare(extraIcon.icon, "chevron-down")
compare(extraIcon.rotation, 270)
compare(extraIcon.color, Theme.palette.baseColor1)
} else {
extraIcon = findChild(delegateUnderTest, "tiny/external-icon")
verify(!!extraIcon)
compare(extraIcon.icon, "tiny/external")
compare(extraIcon.rotation, 0)
compare(extraIcon.color, Theme.palette.baseColor1)
}
// Hover over the item and check hovered state
mouseMove(delegateUnderTest, delegateUnderTest.width/2, delegateUnderTest.height/2)
verify(delegateUnderTest.sensor.containsMouse)
/* TODO: fix when writing more tests for this functionality
compare(externalLinkIcon.color, Theme.palette.directColor1) */
compare(extraIcon.color, Theme.palette.directColor1)
verify(delegateUnderTest.color, Theme.palette.baseColor2)
}
}
function testDelegateMouseClicksForProvidersThatNeedParams(delegateUnderTest, modelData) {
const loadingIndicator = findChild(delegateUnderTest, "loadingIndicator")
verify(!!loadingIndicator)
verify(!loadingIndicator.visible)
verify(!controlUnderTest.replaceItem)
// test mouse click
tryCompare(notificationSpy, "count", 0)
mouseClick(delegateUnderTest)
waitForRendering(controlUnderTest.replaceLoader)
verify(controlUnderTest.replaceItem)
const selectParamsPanel = findChild(controlUnderTest, "selectParamsPanel")
verify(!!selectParamsPanel)
// title should not change
verify(controlUnderTest.stackTitle, qsTr("Buy assets for %1").arg(!!controlUnderTest.selectedAccountEntry.item ? controlUnderTest.selectedAccountEntry.item.name: ""))
compare(controlUnderTest.rightButtons.length, 2)
verify(controlUnderTest.rightButtons[0].visible)
verify(controlUnderTest.rightButtons[1].enabled)
verify(controlUnderTest.rightButtons[0].text, qsTr("Buy via %1").arg(!!controlUnderTest.selectedProviderEntry.item ? controlUnderTest.selectedProviderEntry.item.name: ""))
verify(!controlUnderTest.rightButtons[1].visible)
verify(controlUnderTest.backButton.visible)
const selectParamsForBuyCryptoPanelHeader = findChild(selectParamsPanel, "selectParamsForBuyCryptoPanelHeader")
verify(!!selectParamsForBuyCryptoPanelHeader)
compare(selectParamsForBuyCryptoPanelHeader.title, qsTr("Buy via %1").arg(!!controlUnderTest.selectedProviderEntry.item ? controlUnderTest.selectedProviderEntry.item.name: ""))
compare(selectParamsForBuyCryptoPanelHeader.subTitle, qsTr("Select which network and asset"))
compare(selectParamsForBuyCryptoPanelHeader.statusListItemTitle.color, Theme.palette.directColor1)
compare(selectParamsForBuyCryptoPanelHeader.asset.name, !!controlUnderTest.selectedProviderEntry.item ? controlUnderTest.selectedProviderEntry.item .logoUrl: "")
compare(selectParamsForBuyCryptoPanelHeader.color, Theme.palette.transparent)
compare(selectParamsForBuyCryptoPanelHeader.enabled, false)
const networkFilter = findChild(selectParamsPanel, "networkFilter")
verify(!!networkFilter)
compare(networkFilter.selection, [controlUnderTest.buyCryptoInputParamsForm.selectedNetworkChainId])
const tokenSelector = findChild(selectParamsPanel, "tokenSelector")
verify(!!tokenSelector)
compare(tokenSelector.currentTokensKey, controlUnderTest.buyCryptoInputParamsForm.selectedTokenKey)
const selectedTokenItem = findChild(selectParamsPanel, "selectedTokenItem")
verify(!!selectedTokenItem)
const modelDataToTest = ModelUtils.getByKey(tokenSelector.model, "tokensKey", tokenSelector.currentTokensKey)
const tokenSelectorIcon = findChild(selectedTokenItem, "tokenSelectorIcon")
verify(!!tokenSelectorIcon)
compare(tokenSelectorIcon.image.source, modelDataToTest.iconSource)
const tokenSelectorContentItemName = findChild(selectedTokenItem, "tokenSelectorContentItemName")
verify(!!tokenSelectorContentItemName)
compare(tokenSelectorContentItemName.text, modelDataToTest.name)
const tokenSelectorContentItemSymbol = findChild(selectedTokenItem, "tokenSelectorContentItemSymbol")
verify(!!tokenSelectorContentItemSymbol)
compare(tokenSelectorContentItemSymbol.text, modelDataToTest.symbol)
//switch to a network that has no tokens and ensure its reset
controlUnderTest.buyCryptoInputParamsForm.selectedNetworkChainId = 421613
waitForRendering(selectParamsPanel)
const nothingSelectedContentItem = findChild(selectParamsPanel, "tokenSelectorContentItemText")
verify(!!nothingSelectedContentItem)
verify(!selectedTokenItem.visible)
verify(!controlUnderTest.rightButtons[0].enabled)
// switch back a network and token thats valid and check if clicking buy button works properly
controlUnderTest.buyCryptoInputParamsForm.selectedNetworkChainId = 11155111
controlUnderTest.buyCryptoInputParamsForm.selectedTokenKey = "ETH"
waitForRendering(selectParamsPanel)
verify(controlUnderTest.rightButtons[0].enabled)
mouseClick(controlUnderTest.rightButtons[0])
verify(controlUnderTest.rightButtons[0].loading)
tryCompare(notificationSpy, "count", 1)
compare(notificationSpy.signalArguments[0][0], "xxxx")
compare(notificationSpy.signalArguments[0][1], modelData.hostname)
notificationSpy.clear()
// popup should be closed
verify(!controlUnderTest.opened)
}
function testDelegateMouseClicksForProvidersThatNeedNoParams(delegateUnderTest, modelData) {
// test provider that need no parameters and we directly redirect to the site
const loadingIndicator = findChild(delegateUnderTest, "loadingIndicator")
verify(!!loadingIndicator)
verify(!loadingIndicator.visible)
const extraIcon = findChild(delegateUnderTest, "tiny/external-icon")
verify(!!extraIcon)
verify(!extraIcon.visble)
// test mouse click
tryCompare(notificationSpy, "count", 0)
mouseClick(delegateUnderTest)
verify(loadingIndicator.visible)
tryCompare(notificationSpy, "count", 1)
compare(notificationSpy.signalArguments[0][0], "xxxx")
compare(notificationSpy.signalArguments[0][1], modelData.hostname)
notificationSpy.clear()
// popup should be closed
verify(!controlUnderTest.opened)
}
function test_launchAndCloseModal() {
launchPopup()
@ -165,6 +308,8 @@ Item {
compare(controlUnderTest.rightButtons[1].text, qsTr("Done"))
mouseClick(controlUnderTest.rightButtons[1])
verify(!controlUnderTest.backButton.visible)
// popup should be closed
verify(!controlUnderTest.opened)
}
@ -173,6 +318,8 @@ Item {
// Launch modal
launchPopup()
verify(controlUnderTest.stackTitle, qsTr("Buy assets for %1").arg(!!controlUnderTest.selectedAccountEntry.item ? controlUnderTest.selectedAccountEntry.item.name: ""))
// find tab bar
const tabBar = findChild(controlUnderTest, "tabBar")
verify(!!tabBar)
@ -210,10 +357,9 @@ Item {
// find providers list
const providersList = findChild(controlUnderTest, "providersList")
waitForRendering(providersList)
verify(!!providersList)
tryCompare(controlUnderTest.buyCryptoAdaptor.buyCryptoStore, "areProvidersLoading", false)
tryCompare(controlUnderTest, "isBuyProvidersModelLoading", false)
mouseClick(tabBar.itemAt(0))
compare(tabBar.currentIndex, 0)
@ -222,21 +368,42 @@ Item {
compare(providersList.count, 4)
// check if delegate contents are as expected
testDelegateItems(providersList, _onRampProvidersModel)
testDelegateItems(providersList, controlUnderTest.buyProvidersModel)
let delegateUnderTest = providersList.itemAtIndex(0)
verify(!!delegateUnderTest)
controlUnderTest.close()
}
// test mouse click
tryCompare(notificationSpy, "count", 0)
mouseClick(delegateUnderTest)
tryCompare(notificationSpy, "count", 1)
compare(notificationSpy.signalArguments[0][0], "xxxx")
compare(notificationSpy.signalArguments[0][1], _onRampProvidersModel.get(0).hostname)
function test_modalContent_OneTime_tab_mouseClicks() {
notificationSpy.clear()
// Launch modal
launchPopup()
// popup should be closed
verify(!controlUnderTest.opened)
// find providers list
const providersList = findChild(controlUnderTest, "providersList")
verify(!!providersList)
for(let i =0; i< controlUnderTest.buyProvidersModel.count; i++) {
notificationSpy.clear()
launchPopup()
verify(controlUnderTest.opened)
tryCompare(controlUnderTest, "isBuyProvidersModelLoading", false)
let delegateUnderTest = providersList.itemAtIndex(i)
verify(!!delegateUnderTest)
waitForRendering(delegateUnderTest)
// test provider that need parameters like network and token to be selected
const modelData = controlUnderTest.buyProvidersModel.get(i)
verify(!!modelData)
if (modelData.urlsNeedParameters) {
testDelegateMouseClicksForProvidersThatNeedParams(delegateUnderTest, modelData)
} else {
testDelegateMouseClicksForProvidersThatNeedNoParams(delegateUnderTest, modelData)
}
}
controlUnderTest.close()
}
function test_modalContent_recurrent_tab() {
@ -252,7 +419,7 @@ Item {
const providersList = findChild(controlUnderTest, "providersList")
verify(!!providersList)
tryCompare(controlUnderTest.buyCryptoAdaptor.buyCryptoStore, "areProvidersLoading", false)
tryCompare(controlUnderTest, "isBuyProvidersModelLoading", false)
// check data in "Recurrent" tab --------------------------------------------------------
mouseClick(tabBar.itemAt(1))
@ -264,18 +431,48 @@ Item {
compare(providersList.count, 1)
// check if delegate contents are as expected
testDelegateItems(providersList, recurrentOnRampProvidersModel)
testDelegateItems(providersList, controlUnderTest.recurrentOnRampProvidersModel)
controlUnderTest.close()
}
let delegateUnderTest = providersList.itemAtIndex(0)
verify(!!delegateUnderTest)
// test mouse click
tryCompare(notificationSpy, "count", 0)
verify(controlUnderTest.opened)
mouseClick(delegateUnderTest)
tryCompare(notificationSpy, "count", 0)
function test_modalContent_Recurrent_tab_mouseClicks() {
notificationSpy.clear()
//TODO: add more test logic here for second page of selecting params
// Launch modal
launchPopup()
// find tab bar
const tabBar = findChild(controlUnderTest, "tabBar")
verify(!!tabBar)
// find providers list
const providersList = findChild(controlUnderTest, "providersList")
verify(!!providersList)
mouseClick(tabBar.itemAt(1))
compare(tabBar.currentIndex, 1)
waitForRendering(providersList)
verify(!!providersList)
for(let i =0; i< controlUnderTest.recurrentOnRampProvidersModel.count; i++) {
notificationSpy.clear()
launchPopup()
verify(controlUnderTest.opened)
tryCompare(controlUnderTest, "isBuyProvidersModelLoading", false)
let delegateUnderTest = providersList.itemAtIndex(i)
verify(!!delegateUnderTest)
waitForRendering(delegateUnderTest)
// test provider that need parameters like network and token to be selected
const modelData = controlUnderTest.recurrentOnRampProvidersModel.get(i)
verify(!!modelData)
if (modelData.urlsNeedParameters) {
testDelegateMouseClicksForProvidersThatNeedParams(delegateUnderTest, modelData)
} else {
testDelegateMouseClicksForProvidersThatNeedNoParams(delegateUnderTest, modelData)
}
}
}
}
}

View File

@ -35,13 +35,13 @@ StatusListItem {
loading: root.loading
},
StatusIcon {
objectName: "externalLinkIcon"
icon: root.urlsNeedParameters ? "chevron-down": "tiny/external"
rotation: root.urlsNeedParameters ? 270: 0
color: sensor.containsMouse ? Theme.palette.directColor1: Theme.palette.baseColor1
visible: !root.loading && !root.isUrlLoading
},
StatusLoadingIndicator {
objectName: "loadingIndicator"
visible: root.isUrlLoading
}
]

View File

@ -16,7 +16,7 @@ ColumnLayout {
id: root
// required properties
required property var adaptor
required property var assetsModel
required property var selectedProvider
required property string selectedTokenKey
required property int selectedNetworkChainId
@ -27,9 +27,24 @@ ColumnLayout {
signal networkSelected(int chainId)
signal tokenSelected(string tokensKey)
QtObject {
id: d
function updateTokenSelector() {
Qt.callLater(()=> {
if(!!holdingSelector.model && !!root.selectedTokenKey && root.selectedNetworkChainId !== -1) {
holdingSelector.selectToken(root.selectedTokenKey)
}
})
}
}
onSelectedTokenKeyChanged: d.updateTokenSelector()
onSelectedNetworkChainIdChanged: d.updateTokenSelector()
spacing: 20
StatusListItem {
objectName: "selectParamsForBuyCryptoPanelHeader"
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
leftPadding: 0
@ -81,8 +96,9 @@ ColumnLayout {
}
TokenSelector {
id: holdingSelector
objectName: "tokenSelector"
Layout.fillWidth: true
model: root.adaptor.outputAssetsModel
model: root.assetsModel
popup.width: parent.width
contentItem: Loader {
height: 40 // by design
@ -93,7 +109,6 @@ ColumnLayout {
color: Theme.palette.transparent
}
onTokenSelected: root.tokenSelected(tokensKey)
Component.onCompleted: holdingSelector.selectToken(root.selectedTokenKey)
}
}
@ -111,6 +126,7 @@ ColumnLayout {
Component {
id: selectedTokenCmp
RowLayout {
objectName: "selectedTokenItem"
spacing: Style.current.halfPadding
StatusRoundedImage {
objectName: "tokenSelectorIcon"
@ -119,14 +135,14 @@ ColumnLayout {
image.source: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "iconSource")
}
StatusBaseText {
objectName: "tokenSelectorContentItemText"
objectName: "tokenSelectorContentItemName"
font.pixelSize: 15
color: Theme.palette.directColor1
text: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "name")
}
StatusBaseText {
Layout.fillWidth: true
objectName: "tokenSelectorContentItemText"
objectName: "tokenSelectorContentItemSymbol"
font.pixelSize: 15
color: Theme.palette.baseColor1
text: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "symbol")

View File

@ -17,78 +17,161 @@ import utils 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.adaptors 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.stores 1.0
StatusStackModal {
id: root
// required data
required property var buyProvidersModel
required property var isBuyProvidersModelLoading
required property BuyCryptoParamsForm buyCryptoInputParamsForm
required property BuyCryptoModalAdaptor buyCryptoAdaptor
required property var plainTokensBySymbolModel
required property var groupedAccountAssetsModel
required property var walletAccountsModel
required property var networksModel
required property bool areTestNetworksEnabled
required property string currentCurrency
signal fetchProviders()
signal fetchProviderUrl(string uuid,
string providerID,
bool isRecurrent,
string selectedWalletAddress,
int chainID,
string symbol)
// FIXME handling error in case the response is not successful
function providerUrlReady(uuid, url) {
if(uuid === d.uuid) {
d.urlIsBeingFetched = false
if (!!d.selectedProviderEntry.item && !!url)
Global.openLinkWithConfirmation(url, d.selectedProviderEntry.item.hostname)
root.close()
}
}
QtObject {
id: d
// States to track requests
property string uuid
property bool urlIsBeingFetched
readonly property var buyButton: StatusButton {
height: root.finishButton.height
visible: !!root.replaceItem
borderColor: "transparent"
text: qsTr("Buy via %1").arg(!!root.buyCryptoAdaptor.selectedProvider ? root.buyCryptoAdaptor.selectedProvider.name: "")
loading: root.buyCryptoAdaptor.urlIsBeingFetched
text: qsTr("Buy via %1").arg(!!d.selectedProviderEntry.item ? d.selectedProviderEntry.item.name: "")
loading: d.urlIsBeingFetched
onClicked: {
if(!!root.buyCryptoAdaptor.selectedProvider && !!root.buyCryptoAdaptor.selectedToken) {
root.buyCryptoAdaptor.fetchProviderUrl(
if(!!d.selectedProviderEntry.item && !!d.selectedTokenEntry.item) {
d.fetchProviderUrl(
root.buyCryptoInputParamsForm.selectedProviderId,
buyCryptoProvidersListPanel.currentTabIndex,
root.buyCryptoInputParamsForm.selectedWalletAddress,
root.buyCryptoInputParamsForm.selectedNetworkChainId,
root.buyCryptoAdaptor.selectedToken.symbol
d.selectedTokenEntry.item.symbol
)
}
}
enabled: root.buyCryptoInputParamsForm.filledCorrectly
}
readonly property ModelEntry selectedAccountEntry: ModelEntry {
sourceModel: root.walletAccountsModel
key: "address"
value: root.buyCryptoInputParamsForm.selectedWalletAddress
}
readonly property ModelEntry selectedTokenEntry: ModelEntry {
sourceModel: root.plainTokensBySymbolModel
key: "key"
value: root.buyCryptoInputParamsForm.selectedTokenKey
}
readonly property ModelEntry selectedProviderEntry: ModelEntry {
id: selectedProviderEntry
sourceModel: root.buyProvidersModel
key: "id"
value: root.buyCryptoInputParamsForm.selectedProviderId
}
function fetchProviderUrl(
providerID,
isRecurrent,
accountAddress = "",
chainID = 0,
symbol = "") {
// Identify new search with a different uuid
d.uuid = Utils.uuid()
d.urlIsBeingFetched = true
root.fetchProviderUrl(d.uuid, providerID, isRecurrent,
accountAddress, chainID, symbol)
}
// used to filter items based on search string in the token selector
property string searchString
readonly property var tokenSelectorViewAdaptor: TokenSelectorViewAdaptor {
assetsModel: root.groupedAccountAssetsModel
plainTokensBySymbolModel: root.plainTokensBySymbolModel
flatNetworksModel: root.networksModel
currentCurrency: root.currentCurrency
showAllTokens: true
enabledChainIds: root.buyCryptoInputParamsForm.selectedNetworkChainId !== -1 ? [root.buyCryptoInputParamsForm.selectedNetworkChainId] : []
accountAddress: root.buyCryptoInputParamsForm.selectedWalletAddress
searchString: d.searchString
}
readonly property var buyCryptoAdaptor: BuyCryptoModalAdaptor {
networksModel: root.networksModel
areTestNetworksEnabled: root.areTestNetworksEnabled
processedTokenSelectorAssetsModel: d.tokenSelectorViewAdaptor.outputAssetsModel
selectedProviderSupportedAssetsArray: {
if (!!d.selectedProviderEntry.item && !!d.selectedProviderEntry.item.supportedAssets)
return ModelUtils.modelToFlatArray(d.selectedProviderEntry.item.supportedAssets, "key")
return null
}
selectedChainId: root.buyCryptoInputParamsForm.selectedNetworkChainId
}
}
width: 560
height: 515
padding: Style.current.xlPadding
stackTitle: qsTr("Buy assets for %1").arg(!!buyCryptoAdaptor.selectedAccount ? buyCryptoAdaptor.selectedAccount.name: "")
stackTitle: qsTr("Buy assets for %1").arg(!!d.selectedAccountEntry.item ? d.selectedAccountEntry.item.name: "")
rightButtons: [d.buyButton, finishButton]
finishButton: StatusButton {
text: qsTr("Done")
onClicked: root.close()
}
Connections {
target: root.buyCryptoAdaptor
function onProviderUrlReady(url) {
if (!!root.buyCryptoAdaptor.selectedProvider && !!url)
Global.openLinkWithConfirmation(url, root.buyCryptoAdaptor.selectedProvider.hostname)
root.close()
}
}
onOpened: root.buyCryptoAdaptor.fetchProviders()
onOpened: root.fetchProviders()
onClosed: {
// reset the view
d.uuid = ""
d.urlIsBeingFetched = false
root.replaceItem = undefined
buyCryptoProvidersListPanel.currentTabIndex = 0
root.buyCryptoAdaptor.reset()
root.buyCryptoInputParamsForm.resetFormData()
}
stackItems: [
BuyCryptoProvidersListPanel {
id: buyCryptoProvidersListPanel
providersLoading: root.buyCryptoAdaptor.providersLoading
providersModel: root.buyCryptoAdaptor.providersModel
providersLoading: root.isBuyProvidersModelLoading
providersModel: root.buyProvidersModel
selectedProviderId: root.buyCryptoInputParamsForm.selectedProviderId
isUrlBeingFetched: root.buyCryptoAdaptor.urlIsBeingFetched
isUrlBeingFetched: d.urlIsBeingFetched
onProviderSelected: {
root.buyCryptoInputParamsForm.selectedProviderId = id
if(!!root.buyCryptoAdaptor.selectedProvider) {
if(root.buyCryptoAdaptor.selectedProvider.urlsNeedParameters) {
if(!!d.selectedProviderEntry.item) {
if(d.selectedProviderEntry.item.urlsNeedParameters) {
root.replace(selectParamsPanel)
} else {
root.buyCryptoAdaptor.fetchProviderUrl(root.buyCryptoAdaptor.selectedProvider.id, currentTabIndex)
d.fetchProviderUrl(d.selectedProviderEntry.item.id, currentTabIndex)
}
}
}
@ -98,36 +181,12 @@ StatusStackModal {
Component {
id: selectParamsPanel
SelectParamsForBuyCryptoPanel {
id: selectParamsPanelInst
adaptor: TokenSelectorViewAdaptor {
/* TODO these should be hadbled and perhaps improved under
https://github.com/status-im/status-desktop/issues/16025 */
assetsModel: SortFilterProxyModel {
sourceModel: root.buyCryptoAdaptor.groupedAccountAssetsModelWithKey
filters: FastExpressionFilter {
expression: model.addressPerChain.rowCount() > 0
expectedRoles: ["addressPerChain"]
}
}
plainTokensBySymbolModel: SortFilterProxyModel {
sourceModel: root.buyCryptoAdaptor.plainTokensBySymbolModelWithKey
filters: FastExpressionFilter {
expression: model.addressPerChain.rowCount() > 0
expectedRoles: ["addressPerChain"]
}
}
flatNetworksModel: root.buyCryptoAdaptor.networksModel
currentCurrency: root.buyCryptoAdaptor.currentCurrency
showAllTokens: true
enabledChainIds: root.buyCryptoInputParamsForm.selectedNetworkChainId !== -1 ? [root.buyCryptoInputParamsForm.selectedNetworkChainId] : []
accountAddress: root.buyCryptoInputParamsForm.selectedWalletAddress
searchString: selectParamsPanelInst.searchString
}
selectedProvider: root.buyCryptoAdaptor.selectedProvider
objectName: "selectParamsPanel"
assetsModel: d.buyCryptoAdaptor.filteredAssetsModel
selectedProvider: d.selectedProviderEntry.item
selectedTokenKey: root.buyCryptoInputParamsForm.selectedTokenKey
selectedNetworkChainId: root.buyCryptoInputParamsForm.selectedNetworkChainId
filteredFlatNetworksModel: root.buyCryptoAdaptor.filteredFlatNetworksModel
filteredFlatNetworksModel: d.buyCryptoAdaptor.filteredFlatNetworksModel
onNetworkSelected: {
if (root.buyCryptoInputParamsForm.selectedNetworkChainId !== chainId) {
root.buyCryptoInputParamsForm.selectedNetworkChainId = chainId
@ -138,6 +197,11 @@ StatusStackModal {
root.buyCryptoInputParamsForm.selectedTokenKey = tokensKey
}
}
Binding {
target: d
property: "searchString"
value: searchString
}
}
}
}

View File

@ -4,143 +4,37 @@ 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 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
QObject {
id: root
required property WalletStore.BuyCryptoStore buyCryptoStore
required property BuyCryptoParamsForm buyCryptoFormData
required property var walletAccountsModel
required property var networksModel
required property bool areTestNetworksEnabled
required property var groupedAccountAssetsModel
required property var plainTokensBySymbolModel
required property string currentCurrency
QtObject {
id: d
property string uuid
property bool urlIsBeingFetched
readonly property var selectedProviderSupportedArray: !!selectedProvider && !!selectedProvider.supportedAssets ? ModelUtils.modelToFlatArray(selectedProvider.supportedAssets, "key"): null
}
signal providerUrlReady(string url)
readonly property bool providersLoading: root.buyCryptoStore.areProvidersLoading
readonly property var providersModel: root.buyCryptoStore.providersModel
readonly property bool urlIsBeingFetched: d.urlIsBeingFetched
readonly property var selectedAccount: selectedAccountEntry.item
readonly property var selectedToken: selectedTokenEntry.item
readonly property var selectedProvider: selectedProviderEntry.item
required property var processedTokenSelectorAssetsModel
required property var selectedProviderSupportedAssetsArray
required property int selectedChainId
readonly property SortFilterProxyModel filteredFlatNetworksModel: SortFilterProxyModel {
sourceModel: root.networksModel
filters: ValueFilter { roleName: "isTest"; value: root.areTestNetworksEnabled }
}
/* TODO evaluate if this is still needed after
https://github.com/status-im/status-desktop/issues/16025 */
readonly property ObjectProxyModel plainTokensBySymbolModelWithKey: ObjectProxyModel {
sourceModel: root.plainTokensBySymbolModel
delegate: SortFilterProxyModel {
id: delegateRoot
readonly property var addressPerChain: this
sourceModel: model.addressPerChain
proxyRoles: JoinRole {
name: "key"
roleNames: ["chainId", "address"]
separator: ""
}
// this proxy removes tokens not supported by selected on-ramp provider
readonly property SortFilterProxyModel filteredAssetsModel: SortFilterProxyModel {
sourceModel: root.processedTokenSelectorAssetsModel.count ? root.processedTokenSelectorAssetsModel: null
filters: FastExpressionFilter {
expression: !!d.selectedProviderSupportedArray ? d.selectedProviderSupportedArray.includes(model.key) : true
expectedRoles: ["key"]
function isSupportedByProvider(addressPerChain) {
if(!addressPerChain)
return true
return !!ModelUtils.getFirstModelEntryIf(
addressPerChain,
(addPerChain) => {
return root.selectedChainId === addPerChain.chainId &&
root.selectedProviderSupportedAssetsArray.includes(addPerChain.chainId+addPerChain.address)
})
}
}
exposedRoles: ["addressPerChain"]
expression: isSupportedByProvider(model.addressPerChain)
expectedRoles: ["addressPerChain"]
}
/* TODO evaluate if this is still needed after
https://github.com/status-im/status-desktop/issues/16025 */
readonly property ObjectProxyModel groupedAccountAssetsModelWithKey: ObjectProxyModel {
sourceModel: root.groupedAccountAssetsModel
delegate: SortFilterProxyModel {
id: delegateRoot1
readonly property var addressPerChain: this
sourceModel: model.addressPerChain
proxyRoles: JoinRole {
name: "key"
roleNames: ["chainId", "address"]
separator: ""
}
filters: FastExpressionFilter {
expression: !!d.selectedProviderSupportedArray ? d.selectedProviderSupportedArray.includes(model.key) : true
expectedRoles: ["key"]
}
}
exposedRoles: ["addressPerChain"]
expectedRoles: ["addressPerChain"]
}
function reset() {
d.uuid = ""
d.urlIsBeingFetched = false
}
function fetchProviders() {
root.buyCryptoStore.fetchProviders()
}
function fetchProviderUrl(
providerID,
isRecurrent,
accountAddress = "",
chainID = 0,
symbol = "") {
// Identify new search with a different uuid
d.uuid = Utils.uuid()
d.urlIsBeingFetched = true
buyCryptoStore.fetchProviderUrl(d.uuid, providerID, isRecurrent,
accountAddress, chainID,symbol)
}
Connections {
target: root.buyCryptoStore
function onProviderUrlReady(uuid, url) {
if(uuid === d.uuid) {
d.urlIsBeingFetched = false
root.providerUrlReady(url)
enabled: !!root.selectedProviderSupportedAssetsArray && root.selectedProviderSupportedAssetsArray.length > 0 && root.selectedChainId !== -1
}
}
}
ModelEntry {
id: selectedAccountEntry
sourceModel: root.walletAccountsModel
key: "address"
value: root.buyCryptoFormData.selectedWalletAddress
}
ModelEntry {
id: selectedTokenEntry
sourceModel: root.plainTokensBySymbolModel
key: "key"
value: root.buyCryptoFormData.selectedTokenKey
}
ModelEntry {
id: selectedProviderEntry
sourceModel: root.providersModel
key: "id"
value: root.buyCryptoFormData.selectedProviderId
}
}

View File

@ -79,6 +79,8 @@ Item {
tokensStore: appMain.tokensStore
currencyStore: appMain.currencyStore
}
readonly property WalletStores.BuyCryptoStore buyCryptoStore: WalletStores.BuyCryptoStore {}
readonly property FeatureFlagsStore featureFlagsStore: FeatureFlagsStore {
readonly property var featureFlags: typeof featureFlagsRootContextProperty !== undefined ? featureFlagsRootContextProperty : null
@ -402,6 +404,7 @@ Item {
currencyStore: appMain.currencyStore
walletAssetsStore: appMain.walletAssetsStore
walletCollectiblesStore: appMain.walletCollectiblesStore
buyCryptoStore: appMain.buyCryptoStore
networkConnectionStore: appMain.networkConnectionStore
isDevBuild: !production

View File

@ -45,6 +45,7 @@ QtObject {
property WalletStore.WalletAssetsStore walletAssetsStore
property WalletStore.CollectiblesStore walletCollectiblesStore
property NetworkConnectionStore networkConnectionStore
property WalletStore.BuyCryptoStore buyCryptoStore
property bool isDevBuild
signal openExternalLink(string link)
@ -1249,15 +1250,18 @@ QtObject {
Component {
id: buyCryptoModal
BuyCryptoModal {
buyCryptoAdaptor: BuyCryptoModalAdaptor {
buyCryptoStore: WalletStore.BuyCryptoStore {}
buyCryptoFormData: buyCryptoInputParamsForm
buyProvidersModel: root.buyCryptoStore.providersModel
isBuyProvidersModelLoading: root.buyCryptoStore.areProvidersLoading
currentCurrency: root.currencyStore.currentCurrency
walletAccountsModel: root.rootStore.accounts
plainTokensBySymbolModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
groupedAccountAssetsModel: root.walletAssetsStore.groupedAccountAssetsModel
networksModel: root.rootStore.profileSectionStore.walletStore.flatNetworks
areTestNetworksEnabled: root.rootStore.profileSectionStore.walletStore.areTestNetworksEnabled
groupedAccountAssetsModel: root.walletAssetsStore.groupedAccountAssetsModel
plainTokensBySymbolModel: root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel
currentCurrency: root.currencyStore.currentCurrency
Component.onCompleted: {
fetchProviders.connect(root.buyCryptoStore.fetchProviders)
fetchProviderUrl.connect(root.buyCryptoStore.fetchProviderUrl)
root.buyCryptoStore.providerUrlReady.connect(providerUrlReady)
}
onClosed: destroy()
}