fix(@desktop/wallet): Creating the UI flow needed to select params for making a purchase via Mercuryo

This commit is contained in:
Khushboo Mehta 2024-08-06 18:04:22 +02:00 committed by Anthony Laibe
parent d6ecc24562
commit 6aeb4671cb
29 changed files with 901 additions and 157 deletions

View File

@ -1,16 +1,54 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
import Models 1.0
import AppLayouts.Wallet.popups 1.0
import StatusQ.Core.Backpressure 0.1
import AppLayouts.Wallet.popups.buy 1.0
import AppLayouts.Wallet.stores 1.0
import shared.stores 1.0
SplitView {
id: root
orientation: Qt.Horizontal
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
})
readonly property var buyCryptoStore: BuyCryptoStore {
readonly property var providersModel: OnRampProvidersModel{}
property bool areProvidersLoading
signal providerUrlReady(string uuid , string url)
function fetchProviders() {
console.warn("fetchProviders called >>")
areProvidersLoading = true
d.debounceFetchProvidersList()
}
function fetchProviderUrl(uuid, providerID,
isRecurrent, accountAddress = "",
chainID = 0, symbol = "") {
console.warn("fetchProviderUrl called >> uuid: ", uuid, "providerID: ",providerID
, "isRecurrent: ", isRecurrent, "accountAddress: ", accountAddress,
"chainID: ", chainID, "symbol: ", symbol)
d.uuid = uuid
d.debounceFetchProviderUrl()
}
}
}
PopupBackground {
id: popupBg
@ -28,8 +66,34 @@ SplitView {
BuyCryptoModal {
id: buySellModal
anchors.centerIn: parent
visible: true
onRampProvidersModel: OnRampProvidersModel{}
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
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"
}
}
}
}

View File

@ -5,36 +5,91 @@ import SortFilterProxyModel 0.2
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Core.Backpressure 0.1
import Models 1.0
import utils 1.0
import AppLayouts.Wallet.popups 1.0
import AppLayouts.Wallet.popups.buy 1.0
import AppLayouts.Wallet.stores 1.0
import shared.stores 1.0
Item {
id: root
width: 600
height: 800
OnRampProvidersModel{
id: _onRampProvidersModel
}
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
})
readonly property var buyCryptoStore: BuyCryptoStore {
readonly property var providersModel: d.onRampProvidersModel
property bool areProvidersLoading
signal providerUrlReady(string uuid , string url)
SortFilterProxyModel {
id: recurrentOnRampProvidersModel
sourceModel: _onRampProvidersModel
filters: ValueFilter {
roleName: "recurrentSiteUrl"
value: ""
inverted: true
function fetchProviders() {
console.warn("fetchProviders called >>")
areProvidersLoading = true
d.debounceFetchProvidersList()
}
function fetchProviderUrl(uuid, providerID,
isRecurrent, accountAddress = "",
chainID = 0, symbol = "") {
console.warn("fetchProviderUrl called >> uuid: ", uuid, "providerID: ",providerID
, "isRecurrent: ", isRecurrent, "accountAddress: ", accountAddress,
"chainID: ", chainID, "symbol: ", symbol)
d.uuid = uuid
d.debounceFetchProviderUrl()
}
}
readonly property var onRampProvidersModel: OnRampProvidersModel{}
readonly property var recurrentOnRampProvidersModel: SortFilterProxyModel {
sourceModel: d.onRampProvidersModel
filters: ValueFilter {
roleName: "supportsRecurrentPurchase"
value: true
}
}
}
Component {
id: componentUnderTest
BuyCryptoModal {
onRampProvidersModel: _onRampProvidersModel
onClosed: destroy()
id: buySellModal
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
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"
}
}
}
@ -73,15 +128,17 @@ Item {
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)
compare(externalLinkIcon.color, Theme.palette.baseColor1) */
// Hover over the item and check hovered state
mouseMove(delegateUnderTest, delegateUnderTest.width/2, delegateUnderTest.height/2)
verify(delegateUnderTest.sensor.containsMouse)
compare(externalLinkIcon.color, Theme.palette.directColor1)
/* TODO: fix when writing more tests for this functionality
compare(externalLinkIcon.color, Theme.palette.directColor1) */
verify(delegateUnderTest.color, Theme.palette.baseColor2)
}
}
@ -99,11 +156,11 @@ Item {
launchPopup()
// check if footer has Done button and action on button clicked
const footer = findChild(controlUnderTest, "footer")
verify(!!footer)
compare(footer.rightButtons.count, 1)
compare(footer.rightButtons.get(0).text, qsTr("Done"))
mouseClick(footer.rightButtons.get(0))
compare(controlUnderTest.rightButtons.length, 2)
verify(!controlUnderTest.rightButtons[0].visible)
verify(controlUnderTest.rightButtons[1].visible)
compare(controlUnderTest.rightButtons[1].text, qsTr("Done"))
mouseClick(controlUnderTest.rightButtons[1])
// popup should be closed
verify(!controlUnderTest.opened)
@ -140,6 +197,7 @@ Item {
}
function test_modalContent_OneTime_tab() {
notificationSpy.clear()
// Launch modal
launchPopup()
@ -152,14 +210,16 @@ Item {
waitForRendering(providersList)
verify(!!providersList)
tryCompare(controlUnderTest.buyCryptoAdaptor.buyCryptoStore, "areProvidersLoading", false)
mouseClick(tabBar.itemAt(0))
compare(tabBar.currentIndex, 0)
// verify that 3 items are listed
compare(providersList.count, 3)
// verify that 4 items are listed
compare(providersList.count, 4)
// check if delegate contents are as expected
testDelegateItems(providersList, _onRampProvidersModel)
testDelegateItems(providersList, d.onRampProvidersModel)
let delegateUnderTest = providersList.itemAtIndex(0)
verify(!!delegateUnderTest)
@ -168,8 +228,8 @@ Item {
tryCompare(notificationSpy, "count", 0)
mouseClick(delegateUnderTest)
tryCompare(notificationSpy, "count", 1)
compare(notificationSpy.signalArguments[0][0], _onRampProvidersModel.get(0).siteUrl)
compare(notificationSpy.signalArguments[0][1], _onRampProvidersModel.get(0).hostname)
compare(notificationSpy.signalArguments[0][0], "xxxx")
compare(notificationSpy.signalArguments[0][1], d.onRampProvidersModel.get(0).hostname)
notificationSpy.clear()
// popup should be closed
@ -177,6 +237,7 @@ Item {
}
function test_modalContent_recurrent_tab() {
notificationSpy.clear()
// Launch modal
launchPopup()
@ -186,9 +247,9 @@ Item {
// find providers list
const providersList = findChild(controlUnderTest, "providersList")
waitForRendering(providersList)
verify(!!providersList)
tryCompare(controlUnderTest.buyCryptoAdaptor.buyCryptoStore, "areProvidersLoading", false)
// check data in "Recurrent" tab --------------------------------------------------------
mouseClick(tabBar.itemAt(1))
@ -200,7 +261,7 @@ Item {
compare(providersList.count, 1)
// check if delegate contents are as expected
testDelegateItems(providersList, recurrentOnRampProvidersModel)
testDelegateItems(providersList, d.recurrentOnRampProvidersModel)
let delegateUnderTest = providersList.itemAtIndex(0)
verify(!!delegateUnderTest)
@ -209,13 +270,9 @@ Item {
tryCompare(notificationSpy, "count", 0)
verify(controlUnderTest.opened)
mouseClick(delegateUnderTest)
tryCompare(notificationSpy, "count", 1)
compare(notificationSpy.signalArguments[0][0], recurrentOnRampProvidersModel.get(0).recurrentSiteUrl)
compare(notificationSpy.signalArguments[0][1], recurrentOnRampProvidersModel.get(0).hostname)
tryCompare(notificationSpy, "count", 0)
notificationSpy.clear()
// popup should be closed
verify(!controlUnderTest.opened)
//TODO: add more test logic here for second page of selecting params
}
}
}

View File

@ -87,5 +87,6 @@ QtObject {
readonly property string latamex: Style.png("onRampProviders/latamex")
readonly property string moonPay: Style.png("onRampProviders/moonPay")
readonly property string ramp: Style.png("onRampProviders/ramp")
readonly property string mercuryo: Style.png("onRampProviders/mercuryo")
}
}

View File

@ -3,31 +3,56 @@ import QtQuick 2.15
ListModel {
readonly property var data: [
{
id: "1",
name: "Ramp",
description: "Global crypto to fiat flow",
fees: "0.49% - 2.9%",
logoUrl: ModelsData.onRampProviderImages.ramp,
siteUrl: "https://ramp.network/buy?hostApiKey=zrtf9u2uqebeyzcs37fu5857tktr3eg9w5tffove&swapAsset=DAI,ETH,USDC,USDT",
hostname: "ramp.network",
recurrentSiteUrl: ""
supportsSinglePurchase: true,
supportsRecurrentPurchase: false,
supportedAssets:[],
urlsNeedParameters: false
},
{
id: "2",
name: "MoonPay",
description: "The new standard for fiat to crypto",
fees: "1% - 4.5%",
logoUrl: ModelsData.onRampProviderImages.moonPay,
siteUrl: "https://buy.moonpay.com/?apiKey=pk_live_YQC6CQPA5qqDu0unEwHJyAYQyeIqFGR",
hostname: "moonpay.com",
recurrentSiteUrl: "https://buy.moonpay.com/?apiKey=pk_live_ABCCQPA5qqDu0unEwHJyAYQyeIqFGR",
supportsSinglePurchase: true,
supportsRecurrentPurchase: false,
supportedAssets:[],
urlsNeedParameters: false
},
{
id: "3",
name: "Latamex",
description: "Easily buy crypto in Argentina, Mexico, and Brazil",
fees: "1% - 1.7%",
logoUrl: ModelsData.onRampProviderImages.latamex,
siteUrl: "https://latamex.com/",
hostname: "latamex.com",
recurrentSiteUrl: "",
supportsSinglePurchase: true,
supportsRecurrentPurchase: false,
supportedAssets:[],
urlsNeedParameters: false
},
{
id: "4",
name: "Mercuryo",
description: "Mercuryo buy crypto in Argentina, Mexico, and Brazil",
fees: "1% - 1.7%",
logoUrl: ModelsData.onRampProviderImages.mercuryo,
hostname: "mercuryo.com",
supportsSinglePurchase: true,
supportsRecurrentPurchase: true,
supportedAssets:[
{ key: "111551110x0000000000000000000000000000000000000000", chainId: 11155111, address: "0x0000000000000000000000000000000000000000"},
{ key: "4200x0000000000000000000000000000000000000000", chainId: 420, address: "0x0000000000000000000000000000000000000000"},
{ key: "4200xf2edf1c091f683e3fb452497d9a98a49cba84669", chainId: 420, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84669"},
],
urlsNeedParameters: true
}
]

View File

@ -82,7 +82,7 @@ ListModel {
{ chainId: 42161, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"},
{ chainId: 5, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"},
{ chainId: 11155111, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"},
{ chainId: 420, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"},
{ chainId: 420, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84669"},
{ chainId: 421613, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"},
],
decimals: 18,

View File

@ -0,0 +1,4 @@
import QtQuick 2.15
QtObject {
}

View File

@ -4,3 +4,4 @@ singleton RootStore 1.0 RootStore.qml
SwapStore 1.0 SwapStore.qml
TokensStore 1.0 TokensStore.qml
WalletAssetsStore 1.0 WalletAssetsStore.qml
BuyCryptoStore 1.0 BuyCryptoStore.qml

View File

@ -17,6 +17,7 @@ import "views"
import "stores"
import "controls"
import "popups/swap"
import "popups/buy"
Item {
id: root
@ -142,6 +143,10 @@ Item {
selectedAccountAddress: RootStore.selectedAddress
}
property BuyCryptoParamsForm buyFormData: BuyCryptoParamsForm {
selectedWalletAddress: RootStore.selectedAddress
}
function displayAllAddresses() {
RootStore.showSavedAddresses = false
RootStore.selectedAddress = ""
@ -165,6 +170,12 @@ Item {
rightPanelStackView.currentItem.resetView()
}
}
function getSelectedOrFirstNonWatchedAddress() {
return !!RootStore.selectedAddress ?
RootStore.selectedAddress :
StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts, 0, "address")
}
}
SignPhraseModal {
@ -212,9 +223,7 @@ Item {
hasFloatingButtons: true
})
onLaunchSwapModal: {
d.swapFormData.selectedAccountAddress = !!RootStore.selectedAddress ?
RootStore.selectedAddress :
StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts,0, "address")
d.swapFormData.selectedAccountAddress = d.getSelectedOrFirstNonWatchedAddress()
d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId")
d.swapFormData.fromTokensKey = tokensKey
d.swapFormData.defaultToTokenKey = RootStore.areTestNetworksEnabled ? Constants.swap.testStatusTokenKey : Constants.swap.mainnetStatusTokenKey
@ -336,9 +345,7 @@ Item {
}
onLaunchSwapModal: {
d.swapFormData.fromTokensKey = ""
d.swapFormData.selectedAccountAddress = !!RootStore.selectedAddress ?
RootStore.selectedAddress :
StatusQUtils.ModelUtils.get(RootStore.nonWatchAccounts,0, "address")
d.swapFormData.selectedAccountAddress = d.getSelectedOrFirstNonWatchedAddress()
d.swapFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId")
if(!!walletStore.currentViewedHoldingTokensKey && walletStore.currentViewedHoldingType === Constants.TokenType.ERC20) {
d.swapFormData.fromTokensKey = walletStore.currentViewedHoldingTokensKey
@ -346,6 +353,15 @@ Item {
d.swapFormData.defaultToTokenKey = RootStore.areTestNetworksEnabled ? Constants.swap.testStatusTokenKey : Constants.swap.mainnetStatusTokenKey
Global.openSwapModalRequested(d.swapFormData)
}
onLaunchBuyCryptoModal: {
d.buyFormData.selectedWalletAddress = d.getSelectedOrFirstNonWatchedAddress()
d.buyFormData.selectedNetworkChainId = StatusQUtils.ModelUtils.getByKey(RootStore.filteredFlatModel, "layer", 1, "chainId")
d.buyFormData.selectedTokenKey = Constants.ethToken
if(!!walletStore.currentViewedHoldingTokensKey && walletStore.currentViewedHoldingType === Constants.TokenType.ERC20) {
d.buyFormData.selectedTokenKey = walletStore.currentViewedHoldingTokensKey
}
Global.openBuyCryptoModalRequested(d.buyFormData)
}
ModelEntry {
id: selectedCommunityForCollectible

View File

@ -175,8 +175,7 @@ QObject {
readonly property string favoritesSectionId: "section_zzz"
}
RolesRenamingModel {
id: renamedTokensBySymbolModel
readonly property RolesRenamingModel renamedTokensBySymbolModel: RolesRenamingModel {
sourceModel: root.plainTokensBySymbolModel || null
mapping: [
RoleRename {
@ -191,7 +190,7 @@ QObject {
propagateResets: true
sources: [
SourceModel {
model: renamedTokensBySymbolModel
model: root.renamedTokensBySymbolModel
markerRoleValue: "plain_tokens_model"
},
SourceModel {

View File

@ -0,0 +1,48 @@
import QtQuick 2.15
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
StatusListItem {
id: root
required property string name
required property string description
required property string logoUrl
required property string fees
required property bool urlsNeedParameters
property bool isUrlLoading: false
title: root.name
subTitle: root.description
asset.name: root.logoUrl
asset.isImage: true
statusListItemSubTitle.maximumLineCount: 1
statusListItemComponentsSlot.spacing: 8
components: [
StatusTextWithLoadingState {
objectName: "feesText"
text: root.loading ? Constants.dummyText: root.fees
customColor: Theme.palette.baseColor1
lineHeight: 24
lineHeightMode: Text.FixedHeight
verticalAlignment: Text.AlignVCenter
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 {
visible: root.isUrlLoading
}
]
}

View File

@ -0,0 +1,18 @@
import QtQuick 2.15
import utils 1.0
BuyCryptoProvidersDelegate {
name: Constants.dummyText
description: Constants.dummyText
logoUrl: Constants.dummyText
urlsNeedParameters: false
fees: Constants.dummyText
statusListItemSubTitle.loading: true
statusListItemTitle.loading: true
statusListItemIcon.loading: true
loading: true
enabled: false
}

View File

@ -21,3 +21,5 @@ SwapExchangeButton 1.0 SwapExchangeButton.qml
TokenSelector 1.0 TokenSelector.qml
TokenSelectorNew 1.0 TokenSelectorNew.qml
SwapProvidersTermsAndConditionsText 1.0 SwapProvidersTermsAndConditionsText.qml
BuyCryptoProvidersDelegate 1.0 BuyCryptoProvidersDelegate.qml
BuyCryptoProvidersLoadingDelegate 1.0 BuyCryptoProvidersLoadingDelegate.qml

View File

@ -0,0 +1,86 @@
import QtQuick 2.15
import QtQuick.Layouts 1.0
import QtQml.Models 2.15
import SortFilterProxyModel 0.2
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import AppLayouts.Wallet.controls 1.0
ColumnLayout {
id: root
// required properties
required property bool providersLoading
// expected model structure:
// id, name, description, fees, logoUrl, hostname, supportsSinglePurchase, supportsRecurrentPurchase, supportedAssets, urlsNeedParameters
required property var providersModel
required property bool isUrlBeingFetched
required property string selectedProviderId
// exposed api
property alias currentTabIndex: tabBar.currentIndex
signal providerSelected(string id)
QtObject {
id: d
readonly property int loadingItemsCount: 5
}
spacing: 20
StatusSwitchTabBar {
id: tabBar
objectName: "tabBar"
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
StatusSwitchTabButton {
text: qsTr("One time")
}
StatusSwitchTabButton {
text: qsTr("Recurrent")
}
}
StatusListView {
objectName: "providersList"
Layout.fillWidth: true
Layout.fillHeight: true
DelegateModel {
id: regularModel
model: SortFilterProxyModel {
sourceModel: root.providersModel
filters: ValueFilter {
enabled: tabBar.currentIndex
roleName: "supportsRecurrentPurchase"
value: true
}
}
delegate: BuyCryptoProvidersDelegate {
required property var model
width: ListView.view.width
name: model.name
description: model.description
logoUrl: model.logoUrl
fees: model.fees
urlsNeedParameters: model.urlsNeedParameters
isUrlLoading: root.isUrlBeingFetched && root.selectedProviderId === model.id
onClicked: root.providerSelected(model.id)
}
}
DelegateModel {
id: loadingModel
model: d.loadingItemsCount
delegate: BuyCryptoProvidersLoadingDelegate {
required property var model
width: ListView.view.width
}
}
model: root.providersLoading ? loadingModel : regularModel
}
}

View File

@ -0,0 +1,148 @@
import QtQuick 2.14
import QtQuick.Layouts 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import StatusQ.Components 0.1
import StatusQ.Components.private 0.1
import AppLayouts.Wallet.controls 1.0
import utils 1.0
ColumnLayout {
id: root
// required properties
required property var adaptor
required property var selectedProvider
required property string selectedTokenKey
required property int selectedNetworkChainId
required property var filteredFlatNetworksModel
// exposed api
property alias searchString: holdingSelector.searchString
signal networkSelected(int chainId)
signal tokenSelected(string tokensKey)
QtObject {
id: d
function updateTokenSelector() {
if(!!root.selectedTokenKey && root.selectedNetworkChainId !== -1) {
holdingSelector.selectToken(root.selectedTokenKey)
}
}
}
onSelectedTokenKeyChanged: d.updateTokenSelector()
onSelectedNetworkChainIdChanged: d.updateTokenSelector()
spacing: 20
StatusListItem {
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
leftPadding: 0
rightPadding: 0
title: qsTr("Buy via %1").arg(!!root.selectedProvider ? root.selectedProvider.name: "")
subTitle: qsTr("Select which network and asset")
statusListItemTitle.color: Theme.palette.directColor1
asset.name: !!root.selectedProvider ? root.selectedProvider.logoUrl: ""
asset.isImage: true
color: Theme.palette.transparent
enabled: false
}
StatusMenuSeparator {
Layout.fillWidth: true
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
StatusBaseText {
text: qsTr("Select network")
color: Theme.palette.directColor1
font.pixelSize: 15
lineHeight: 22
lineHeightMode: Text.FixedHeight
verticalAlignment: Text.AlignVCenter
}
NetworkFilter {
objectName: "networkFilter"
Layout.fillWidth: true
control.popup.width: parent.width
multiSelection: false
showSelectionIndicator: false
flatNetworks: root.filteredFlatNetworksModel
selection: [root.selectedNetworkChainId]
onSelectionChanged: root.networkSelected(selection[0])
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 8
StatusBaseText {
text: qsTr("Select asset")
color: Theme.palette.directColor1
font.pixelSize: 15
lineHeight: 22
lineHeightMode: Text.FixedHeight
verticalAlignment: Text.AlignVCenter
}
TokenSelector {
id: holdingSelector
Layout.fillWidth: true
model: root.adaptor.outputAssetsModel
popup.width: parent.width
contentItem: Loader {
height: 40 // by design
sourceComponent: !!holdingSelector.currentTokensKey ? selectedTokenCmp : nothingSelectedCmp
}
background: StatusComboboxBackground {
border.width: 1
color: Theme.palette.transparent
}
onTokenSelected: root.tokenSelected(tokensKey)
Component.onCompleted: holdingSelector.selectToken(root.selectedTokenKey)
}
}
Component {
id: nothingSelectedCmp
StatusBaseText {
objectName: "tokenSelectorContentItemText"
font.pixelSize: Style.current.additionalTextSize
font.weight: Font.Medium
color: Theme.palette.primaryColor1
text: qsTr("Select asset")
}
}
Component {
id: selectedTokenCmp
RowLayout {
spacing: Style.current.halfPadding
StatusRoundedImage {
objectName: "tokenSelectorIcon"
Layout.preferredWidth: 20
Layout.preferredHeight: 20
image.source: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "iconSource")
}
StatusBaseText {
objectName: "tokenSelectorContentItemText"
font.pixelSize: 15
color: Theme.palette.directColor1
text: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "name")
}
StatusBaseText {
Layout.fillWidth: true
objectName: "tokenSelectorContentItemText"
font.pixelSize: 15
color: Theme.palette.baseColor1
text: ModelUtils.getByKey(holdingSelector.model, "tokensKey", holdingSelector.currentTokensKey, "symbol")
}
}
}
}

View File

@ -33,6 +33,7 @@ Rectangle {
signal launchSendModal(string fromAddress)
signal launchBridgeModal()
signal launchSwapModal()
signal launchBuyCryptoModal()
color: Theme.palette.statusAppLayout.rightPanelBackgroundColor
@ -146,7 +147,7 @@ Rectangle {
visible: d.buyActionAvailable
icon.name: "token"
text: qsTr("Buy")
onClicked: Global.openBuyCryptoModalRequested()
onClicked: root.launchBuyCryptoModal()
}
StatusFlatButton {

View File

@ -10,3 +10,5 @@ TokenSelectorPanel 1.0 TokenSelectorPanel.qml
WalletHeader 1.0 WalletHeader.qml
WalletNftPreview 1.0 WalletNftPreview.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml
BuyCryptoProvidersListPanel 1.0 BuyCryptoProvidersListPanel.qml
SelectParamsForBuyCryptoPanel 1.0 SelectParamsForBuyCryptoPanel.qml

View File

@ -1,97 +0,0 @@
import QtQuick 2.14
import QtQuick.Layouts 1.0
import QtQml.Models 2.14
import SortFilterProxyModel 0.2
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ 0.1
import utils 1.0
StatusDialog {
id: root
required property var onRampProvidersModel
padding: Style.current.xlPadding
implicitWidth: 560
implicitHeight: 436
title: qsTr("Buy assets")
ColumnLayout {
anchors.fill: parent
spacing: 20
StatusSwitchTabBar {
id: tabBar
objectName: "tabBar"
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
StatusSwitchTabButton {
text: qsTr("One time")
}
StatusSwitchTabButton {
text: qsTr("Recurrent")
}
}
StatusListView {
id: providersList
objectName: "providersList"
Layout.fillWidth: true
Layout.fillHeight: true
model: SortFilterProxyModel {
sourceModel: !!root.onRampProvidersModel ? root.onRampProvidersModel : null
filters: ValueFilter {
enabled: tabBar.currentIndex
roleName: "recurrentSiteUrl"
value: ""
inverted: true
}
}
delegate: StatusListItem {
width: ListView.view.width
title: name
subTitle: description
asset.name: logoUrl
asset.isImage: true
statusListItemSubTitle.maximumLineCount: 1
statusListItemComponentsSlot.spacing: 8
components: [
StatusBaseText {
objectName: "feesText"
text: fees
color: Theme.palette.baseColor1
lineHeight: 24
lineHeightMode: Text.FixedHeight
verticalAlignment: Text.AlignVCenter
},
StatusIcon {
objectName: "externalLinkIcon"
icon: "tiny/external"
color: sensor.containsMouse ? Theme.palette.directColor1: Theme.palette.baseColor1
}
]
onClicked: {
let url = tabBar.currentIndex ? recurrentSiteUrl : siteUrl
Global.openLinkWithConfirmation(url, hostname)
root.close()
}
}
}
}
footer: StatusDialogFooter {
objectName: "footer"
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Done")
onClicked: root.close()
}
}
}
}

View File

@ -0,0 +1,143 @@
import QtQuick 2.14
import QtQuick.Layouts 1.0
import QtQml.Models 2.14
import SortFilterProxyModel 0.2
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ 0.1
import utils 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.adaptors 1.0
import AppLayouts.Wallet.panels 1.0
StatusStackModal {
id: root
required property BuyCryptoParamsForm buyCryptoInputParamsForm
required property BuyCryptoModalAdaptor buyCryptoAdaptor
QtObject {
id: d
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
onClicked: {
if(!!root.buyCryptoAdaptor.selectedProvider && !!root.buyCryptoAdaptor.selectedToken) {
root.buyCryptoAdaptor.fetchProviderUrl(
root.buyCryptoInputParamsForm.selectedProviderId,
buyCryptoProvidersListPanel.currentTabIndex,
root.buyCryptoInputParamsForm.selectedWalletAddress,
root.buyCryptoInputParamsForm.selectedNetworkChainId,
root.buyCryptoAdaptor.selectedToken.symbol
)
}
}
enabled: root.buyCryptoInputParamsForm.filledCorrectly
}
}
width: 560
height: 515
padding: Style.current.xlPadding
stackTitle: qsTr("Buy assets for %1").arg(!!buyCryptoAdaptor.selectedAccount ? buyCryptoAdaptor.selectedAccount.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()
onClosed: {
// reset the view
root.replaceItem = undefined
buyCryptoProvidersListPanel.currentTabIndex = 0
root.buyCryptoAdaptor.reset()
root.buyCryptoInputParamsForm.resetFormData()
}
stackItems: [
BuyCryptoProvidersListPanel {
id: buyCryptoProvidersListPanel
providersLoading: root.buyCryptoAdaptor.providersLoading
providersModel: root.buyCryptoAdaptor.providersModel
selectedProviderId: root.buyCryptoInputParamsForm.selectedProviderId
isUrlBeingFetched: root.buyCryptoAdaptor.urlIsBeingFetched
onProviderSelected: {
root.buyCryptoInputParamsForm.selectedProviderId = id
if(!!root.buyCryptoAdaptor.selectedProvider) {
if(root.buyCryptoAdaptor.selectedProvider.urlsNeedParameters) {
root.replace(selectParamsPanel)
} else {
root.buyCryptoAdaptor.fetchProviderUrl(root.buyCryptoAdaptor.selectedProvider.id, currentTabIndex)
}
}
}
}
]
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
selectedTokenKey: root.buyCryptoInputParamsForm.selectedTokenKey
selectedNetworkChainId: root.buyCryptoInputParamsForm.selectedNetworkChainId
filteredFlatNetworksModel: root.buyCryptoAdaptor.filteredFlatNetworksModel
onNetworkSelected: {
if (root.buyCryptoInputParamsForm.selectedNetworkChainId !== chainId) {
root.buyCryptoInputParamsForm.selectedNetworkChainId = chainId
}
}
onTokenSelected: {
if (root.buyCryptoInputParamsForm.selectedTokenKey !== tokensKey) {
root.buyCryptoInputParamsForm.selectedTokenKey = tokensKey
}
}
}
}
}

View File

@ -0,0 +1,146 @@
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 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
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: ""
}
filters: FastExpressionFilter {
expression: !!d.selectedProviderSupportedArray ? d.selectedProviderSupportedArray.includes(model.key) : true
expectedRoles: ["key"]
}
}
exposedRoles: ["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)
}
}
}
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

@ -0,0 +1,19 @@
import QtQml 2.15
QtObject {
id: root
property string selectedWalletAddress: ""
property int selectedNetworkChainId: -1
property string selectedTokenKey: ""
property string selectedProviderId: ""
readonly property bool filledCorrectly: !!selectedWalletAddress && !!selectedTokenKey && selectedNetworkChainId !== -1
function resetFormData() {
selectedWalletAddress = ""
selectedNetworkChainId = -1
selectedTokenKey = ""
selectedProviderId = ""
}
}

View File

@ -0,0 +1,3 @@
BuyCryptoModal 1.0 BuyCryptoModal.qml
BuyCryptoModalAdaptor 1.0 BuyCryptoModalAdaptor.qml
BuyCryptoParamsForm 1.0 BuyCryptoParamsForm.qml

View File

@ -6,5 +6,4 @@ ReceiveModal 1.0 ReceiveModal.qml
AddEditSavedAddressPopup 1.0 AddEditSavedAddressPopup.qml
RemoveSavedAddressPopup 1.0 RemoveSavedAddressPopup.qml
SavedAddressActivityPopup 1.0 SavedAddressActivityPopup.qml
BuyCryptoModal 1.0 BuyCryptoModal.qml
SignTransactionModalBase 1.0 SignTransactionModalBase.qml

View File

@ -16,6 +16,7 @@ import shared.controls 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.popups.buy 1.0
StatusDialog {
id: root
@ -61,6 +62,12 @@ StatusDialog {
}
readonly property bool isError: root.swapAdaptor.errorMessage !== ""
readonly property BuyCryptoParamsForm buyFormData: BuyCryptoParamsForm {
selectedWalletAddress: root.swapInputParamsForm.selectedAccountAddress
selectedNetworkChainId: root.swapInputParamsForm.selectedNetworkChainId
selectedTokenKey: root.swapInputParamsForm.fromTokensKey
}
}
Connections {
@ -314,7 +321,13 @@ StatusDialog {
text: root.swapAdaptor.errorMessage
buttonText: root.swapAdaptor.isTokenBalanceInsufficient ? qsTr("Buy crypto") : qsTr("Buy ETH")
buttonVisible: visible && (root.swapAdaptor.isTokenBalanceInsufficient || root.swapAdaptor.isEthBalanceInsufficient)
onButtonClicked: Global.openBuyCryptoModalRequested()
onButtonClicked: {
// value dont update correctly if not done from here
d.buyFormData.selectedWalletAddress = root.swapInputParamsForm.selectedAccountAddress
d.buyFormData.selectedNetworkChainId = root.swapInputParamsForm.selectedNetworkChainId
d.buyFormData.selectedTokenKey =root.swapInputParamsForm.fromTokensKey
Global.openBuyCryptoModalRequested(d.buyFormData)
}
}
}
}

View File

@ -0,0 +1,35 @@
import QtQuick 2.15
QtObject {
id: root
readonly property var providersModel: walletSectionBuySellCrypto.model
readonly property bool areProvidersLoading: walletSectionBuySellCrypto.isFetching
signal providerUrlReady(string uuid , string url)
function fetchProviders() {
walletSectionBuySellCrypto.fetchProviders()
}
function fetchProviderUrl(
uuid,
providerID,
isRecurrent,
selectedWalletAddress = "",
chainID = 0,
symbol = "") {
walletSectionBuySellCrypto.fetchProviderUrl(
uuid,
providerID,
isRecurrent,
selectedWalletAddress,
chainID,
symbol
)
}
Component.onCompleted: {
walletSectionBuySellCrypto.providerUrlReady.connect(root.providerUrlReady)
}
}

View File

@ -172,8 +172,6 @@ QtObject {
d.initChainColors(flatNetworks)
}
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model
function resetCurrentViewedHolding(type) {
currentViewedHoldingTokensKey = ""
currentViewedHoldingID = ""

View File

@ -4,3 +4,4 @@ CollectiblesStore 1.0 CollectiblesStore.qml
TokensStore 1.0 TokensStore.qml
WalletAssetsStore 1.0 WalletAssetsStore.qml
SwapStore 1.0 SwapStore.qml
BuyCryptoStore 1.0 BuyCryptoStore.qml

View File

@ -16,6 +16,7 @@ import AppLayouts.Profile.popups 1.0
import AppLayouts.Communities.popups 1.0
import AppLayouts.Communities.helpers 1.0
import AppLayouts.Wallet.popups.swap 1.0
import AppLayouts.Wallet.popups.buy 1.0
import AppLayouts.Wallet.popups 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
@ -397,8 +398,10 @@ QtObject {
openPopup(swapModal, {swapInputParamsForm: parameters})
}
function openBuyCryptoModal() {
openPopup(buyCryptoModal)
function openBuyCryptoModal(parameters) {
openPopup(buyCryptoModal, {
buyCryptoInputParamsForm: parameters
})
}
readonly property list<Component> _components: [
@ -1254,7 +1257,16 @@ QtObject {
Component {
id: buyCryptoModal
BuyCryptoModal {
onRampProvidersModel: WalletStore.RootStore.cryptoRampServicesModel
buyCryptoAdaptor: BuyCryptoModalAdaptor {
buyCryptoStore: WalletStore.BuyCryptoStore {}
buyCryptoFormData: buyCryptoInputParamsForm
walletAccountsModel: root.rootStore.accounts
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
}
onClosed: destroy()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -107,7 +107,7 @@ QtObject {
signal openSwapModalRequested(var formDataParams)
// BuyCrypto
signal openBuyCryptoModalRequested()
signal openBuyCryptoModalRequested(var formDataParams)
// Metrics
signal openMetricsEnablePopupRequested(string placement, var cb)