diff --git a/storybook/pages/BuyCryptoModalPage.qml b/storybook/pages/BuyCryptoModalPage.qml new file mode 100644 index 0000000000..55499ca1b8 --- /dev/null +++ b/storybook/pages/BuyCryptoModalPage.qml @@ -0,0 +1,37 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import Storybook 1.0 +import Models 1.0 + +import AppLayouts.Wallet.popups 1.0 + +SplitView { + id: root + + orientation: Qt.Horizontal + + PopupBackground { + id: popupBg + + SplitView.fillWidth: true + SplitView.fillHeight: true + + Button { + id: reopenButton + anchors.centerIn: parent + text: "Reopen" + enabled: !buySellModal.visible + + onClicked: buySellModal.open() + } + + BuyCryptoModal { + id: buySellModal + visible: true + onRampProvidersModel: OnRampProvidersModel{} + } + } +} + +// category: Popups diff --git a/storybook/qmlTests/tests/tst_BuyCryptoModal.qml b/storybook/qmlTests/tests/tst_BuyCryptoModal.qml new file mode 100644 index 0000000000..4408d47152 --- /dev/null +++ b/storybook/qmlTests/tests/tst_BuyCryptoModal.qml @@ -0,0 +1,146 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import StatusQ.Core.Theme 0.1 + +import Models 1.0 +import utils 1.0 + +import AppLayouts.Wallet.popups 1.0 + +Item { + id: root + width: 600 + height: 800 + + OnRampProvidersModel{ + id: onRampProvidersModal + } + + Component { + id: componentUnderTest + BuyCryptoModal { + onRampProvidersModel: onRampProvidersModal + onClosed: destroy() + } + } + + SignalSpy { + id: notificationSpy + target: Global + signalName: "openLinkWithConfirmation" + } + + TestCase { + name: "BuyCryptoModal" + when: windowShown + + property BuyCryptoModal controlUnderTest: null + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + + function launchPopup() { + verify(!!controlUnderTest) + controlUnderTest.open() + verify(!!controlUnderTest.opened) + } + + function test_launchAndCloseModal() { + launchPopup() + + // close popup + controlUnderTest.close() + verify(!controlUnderTest.opened) + } + + function test_ModalFooter() { + // Launch modal + 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), Qt.LeftButton) + + // popup should be closed + verify(!controlUnderTest.opened) + } + + function test_modalContent() { + // Launch modal + launchPopup() + + // find tab bar + const tabBar = findChild(controlUnderTest, "tabBar") + verify(!!tabBar) + + // should have 2 items + compare(tabBar.count, 2) + + // current index set should be to 0 + compare(tabBar.currentIndex, 0) + + // item 0 should have text "One time" + compare(tabBar.itemAt(0).text, qsTr("One time")) + + // item 1 should have text "Recurrent" + compare(tabBar.itemAt(1).text, qsTr("Recurrent")) + + // TODO: this will be implemnted under https://github.com/status-im/status-desktop/issues/14820 + // until then this list will be empty + mouseClick(tabBar.itemAt(1), Qt.LeftButton) + compare(tabBar.currentIndex, 1) + + const providersList = findChild(controlUnderTest, "providersList") + waitForRendering(providersList) + verify(!!providersList) + compare(providersList.count, 0) + + // check data on 1st tab -------------------------------------------------------- + mouseClick(tabBar.itemAt(0), Qt.LeftButton) + compare(tabBar.currentIndex, 0) + + waitForRendering(providersList) + verify(!!providersList) + + // verify that 3 items are listed + compare(providersList.count, 3) + + // check if delegate contents are as expected + for(let i =0; i< providersList.count; i++) { + let delegateUnderTest = providersList.itemAtIndex(i) + verify(!!delegateUnderTest) + + compare(delegateUnderTest.title, onRampProvidersModal.get(i).name) + compare(delegateUnderTest.subTitle, onRampProvidersModal.get(i).description) + compare(delegateUnderTest.asset.name, onRampProvidersModal.get(i).logoUrl) + + const feesText = findChild(delegateUnderTest, "feesText") + verify(!!feesText) + compare(feesText.text, onRampProvidersModal.get(i).fees) + + const externalLinkIcon = findChild(delegateUnderTest, "externalLinkIcon") + verify(!!externalLinkIcon) + compare(externalLinkIcon.icon, "tiny/external") + 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) + verify(delegateUnderTest.color, Theme.palette.baseColor2) + } + + // test mouse click + tryCompare(notificationSpy, "count", 0) + mouseClick(providersList.itemAtIndex(0)) + tryCompare(notificationSpy, "count", 1) + compare(notificationSpy.signalArguments[0][0],onRampProvidersModal.get(0).siteUrl) + compare(notificationSpy.signalArguments[0][1],onRampProvidersModal.get(0).hostname) + } + } +} diff --git a/storybook/src/Models/ModelsData.qml b/storybook/src/Models/ModelsData.qml index 0c87b7a33e..c4a93e3776 100644 --- a/storybook/src/Models/ModelsData.qml +++ b/storybook/src/Models/ModelsData.qml @@ -82,4 +82,10 @@ QtObject { readonly property string ownerTokenInfo: "In order to Mint, Import and Airdrop community tokens, you first need to mint your Owner token which will give you permissions to access the token management features for your community." readonly property string airdropInfo: "You can Airdrop tokens to deserving Community members or to give individuals token-based permissions." } + + readonly property QtObject onRampProviderImages: 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") + } } diff --git a/storybook/src/Models/OnRampProvidersModel.qml b/storybook/src/Models/OnRampProvidersModel.qml new file mode 100644 index 0000000000..5404f0a533 --- /dev/null +++ b/storybook/src/Models/OnRampProvidersModel.qml @@ -0,0 +1,35 @@ +import QtQuick 2.15 + +ListModel { + readonly property var data: [ + { + 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: "" + }, + { + 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_YQC6CQPA5qqDu0unEwHJyAYQyeIqFGR", + }, + { + 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: "", + } + ] + + Component.onCompleted: append(data) +} diff --git a/storybook/src/Models/qmldir b/storybook/src/Models/qmldir index 062845538f..95f7929692 100644 --- a/storybook/src/Models/qmldir +++ b/storybook/src/Models/qmldir @@ -22,6 +22,7 @@ WalletTransactionsModel 1.0 WalletTransactionsModel.qml GroupedAccountsAssetsModel 1.0 GroupedAccountsAssetsModel.qml TokensBySymbolModel 1.0 TokensBySymbolModel.qml CommunitiesModel 1.0 CommunitiesModel.qml +OnRampProvidersModel 1.0 OnRampProvidersModel.qml singleton ModelsData 1.0 ModelsData.qml singleton NetworksModel 1.0 NetworksModel.qml diff --git a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml index 5d150893e8..bfdbb0c721 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletFooter.qml @@ -81,9 +81,7 @@ Rectangle { visible: !root.isCommunityOwnershipTransfer && !root.walletStore.showAllAccounts icon.name: "token" text: qsTr("Buy") - onClicked: function () { - Global.openPopup(buySellModal); - } + onClicked: Global.openBuyCryptoModalRequested() } StatusFlatButton { @@ -97,11 +95,6 @@ Rectangle { onClicked: root.launchSwapModal() } } - - Component { - id: buySellModal - CryptoServicesModal {} - } } diff --git a/ui/app/AppLayouts/Wallet/popups/BuyCryptoModal.qml b/ui/app/AppLayouts/Wallet/popups/BuyCryptoModal.qml new file mode 100644 index 0000000000..18eb113f41 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/popups/BuyCryptoModal.qml @@ -0,0 +1,98 @@ +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 + // TODO: this temporary and changed under https://github.com/status-im/status-desktop/issues/14820 + // when recurrentSiteURL is available + filters: ValueFilter { + enabled: tabBar.currentIndex + roleName: "name" + value: "" + } + } + 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() + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/popups/CryptoServicesModal.qml b/ui/app/AppLayouts/Wallet/popups/CryptoServicesModal.qml deleted file mode 100644 index 872adf0ec0..0000000000 --- a/ui/app/AppLayouts/Wallet/popups/CryptoServicesModal.qml +++ /dev/null @@ -1,76 +0,0 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 - -import utils 1.0 -import shared.panels 1.0 - -import StatusQ.Popups 0.1 -import StatusQ.Components 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Core 0.1 - -import "../controls" -import "../stores" - -StatusModal { - id: cryptoServicesPopupRoot - - height: 400 - headerSettings.title: qsTr("Buy crypto") - anchors.centerIn: parent - - Loader { - id: loader - anchors.fill: parent - sourceComponent: servicesComponent - - Component { - id: servicesComponent - Item { - anchors.fill: parent - anchors.topMargin: Style.current.padding - anchors.bottomMargin: Style.current.padding - anchors.leftMargin: Style.current.padding - anchors.rightMargin: Style.current.padding - StyledText { - id: note - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - color: Style.current.secondaryText - text: qsTr("Choose a service you'd like to use to buy crypto") - } - - StatusListView { - anchors.top: note.bottom - anchors.bottom: parent.bottom - anchors.topMargin: Style.current.padding - anchors.horizontalCenter: parent.horizontalCenter - width: 394 - model: RootStore.cryptoRampServicesModel - focus: true - spacing: Style.current.padding - - delegate: StatusListItem { - width: parent.width - title: name - subTitle: description - asset.name: logoUrl - asset.isImage: true - label: fees - statusListItemSubTitle.maximumLineCount: 1 - components: [ - StatusIcon { - icon: "next" - color: Theme.palette.baseColor1 - } - ] - onClicked: { - Global.openLink(siteUrl); - cryptoServicesPopupRoot.close(); - } - } - } - } - } - } -} diff --git a/ui/app/AppLayouts/Wallet/popups/qmldir b/ui/app/AppLayouts/Wallet/popups/qmldir index 4aacaa2a38..52552b4151 100644 --- a/ui/app/AppLayouts/Wallet/popups/qmldir +++ b/ui/app/AppLayouts/Wallet/popups/qmldir @@ -6,3 +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 diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 37c3d0b915..1736b6aab0 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -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 1.0 import AppLayouts.Wallet.stores 1.0 as WalletStore import AppLayouts.Chat.stores 1.0 as ChatStore @@ -91,6 +92,7 @@ QtObject { Global.openConfirmHideCollectiblePopup.connect(openConfirmHideCollectiblePopup) Global.openCommunityMemberMessagesPopupRequested.connect(openCommunityMemberMessagesPopup) Global.openSwapModalRequested.connect(openSwapModal) + Global.openBuyCryptoModalRequested.connect(openBuyCryptoModal) } property var currentPopup @@ -390,6 +392,10 @@ QtObject { openPopup(swapModal, {swapInputParamsForm: parameters}) } + function openBuyCryptoModal() { + openPopup(buyCryptoModal) + } + readonly property list _components: [ Component { id: removeContactConfirmationDialog @@ -1248,6 +1254,13 @@ QtObject { } onClosed: destroy() } + }, + Component { + id: buyCryptoModal + BuyCryptoModal { + onRampProvidersModel: WalletStore.RootStore.cryptoRampServicesModel + onClosed: destroy() + } } ] } diff --git a/ui/imports/assets/png/onRampProviders/latamex.png b/ui/imports/assets/png/onRampProviders/latamex.png new file mode 100644 index 0000000000..891e1510d6 Binary files /dev/null and b/ui/imports/assets/png/onRampProviders/latamex.png differ diff --git a/ui/imports/assets/png/onRampProviders/moonPay.png b/ui/imports/assets/png/onRampProviders/moonPay.png new file mode 100644 index 0000000000..1be535ccf2 Binary files /dev/null and b/ui/imports/assets/png/onRampProviders/moonPay.png differ diff --git a/ui/imports/assets/png/onRampProviders/ramp.png b/ui/imports/assets/png/onRampProviders/ramp.png new file mode 100644 index 0000000000..3840202aa2 Binary files /dev/null and b/ui/imports/assets/png/onRampProviders/ramp.png differ diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index 6d7477396c..81d456c76d 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -107,6 +107,9 @@ QtObject { // Swap signal openSwapModalRequested(var formDataParams) + // BuyCrypto + signal openBuyCryptoModalRequested() + ///////////////////////////////////////////////////// // WalletConnect POC - to remove signal popupWalletConnect()