From 2f050a025f8dbf4ea4989bf5fae32e7c6fb228b7 Mon Sep 17 00:00:00 2001 From: Alex Jbanca Date: Sat, 29 Jun 2024 23:24:05 +0300 Subject: [PATCH] feat(WalletConnect): Fine-tune connect dApp modal Changes: 1. Align dialog with Figma design 2. Add new components for round image with badge and connection status tag 3. Add tests 4. Dapps service will now receive wallet `RootStore` as input and reuse existing models with account balance and other necessary info for account selection and chain selection 5. Minor updates in stores 6. Minor updates in WC toast messages to display app domain instead of app url --- storybook/pages/ConnectDAppModalPage.qml | 78 ++--- storybook/pages/ConnectionStatusTagPage.qml | 25 ++ storybook/pages/DAppsWorkflowPage.qml | 12 +- storybook/pages/RoundImageWithBadgePage.qml | 52 +++ .../qmlTests/tests/tst_ConnectDAppModal.qml | 319 ++++++++++++++++++ .../qmlTests/tests/tst_DAppsWorkflow.qml | 49 +-- .../AppLayouts/Wallet/stores/RootStore.qml | 5 + .../stubs/AppLayouts/Wallet/stores/qmldir | 7 +- ui/StatusQ/src/assets.qrc | 1 + ui/StatusQ/src/assets/img/icons/dapp.svg | 2 +- .../src/assets/img/icons/walletConnect.svg | 1 + .../AppLayouts/Profile/stores/WalletStore.qml | 26 +- .../Wallet/panels/DAppsWorkflow.qml | 27 +- .../services/dapps/DAppsRequestHandler.qml | 15 +- .../services/dapps/WalletConnectService.qml | 38 ++- ui/app/AppLayouts/Wallet/stores/RootStore.qml | 8 +- .../Wallet/views/NetworkSelectorView.qml | 4 +- ui/app/mainui/AppMain.qml | 2 +- .../shared/controls/AccountSelector.qml | 9 +- ui/imports/shared/controls/InformationTag.qml | 6 + .../popups/walletconnect/ConnectDAppModal.qml | 167 ++++----- .../walletconnect/ConnectionStatusTag.qml | 57 ++++ .../walletconnect/RoundImageWithBadge.qml | 86 +++++ .../walletconnect/controls/DAppCard.qml | 137 -------- .../{controls => private}/ContextCard.qml | 176 +++++----- .../popups/walletconnect/private/DAppCard.qml | 74 ++++ .../{controls => private}/PermissionsCard.qml | 62 ++-- .../popups/walletconnect/private/qmldir | 3 + ui/imports/shared/popups/walletconnect/qmldir | 2 + 29 files changed, 1001 insertions(+), 449 deletions(-) create mode 100644 storybook/pages/ConnectionStatusTagPage.qml create mode 100644 storybook/pages/RoundImageWithBadgePage.qml create mode 100644 storybook/qmlTests/tests/tst_ConnectDAppModal.qml create mode 100644 storybook/stubs/AppLayouts/Wallet/stores/RootStore.qml create mode 100644 ui/StatusQ/src/assets/img/icons/walletConnect.svg create mode 100644 ui/imports/shared/popups/walletconnect/ConnectionStatusTag.qml create mode 100644 ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml delete mode 100644 ui/imports/shared/popups/walletconnect/controls/DAppCard.qml rename ui/imports/shared/popups/walletconnect/{controls => private}/ContextCard.qml (61%) create mode 100644 ui/imports/shared/popups/walletconnect/private/DAppCard.qml rename ui/imports/shared/popups/walletconnect/{controls => private}/PermissionsCard.qml (64%) create mode 100644 ui/imports/shared/popups/walletconnect/private/qmldir diff --git a/storybook/pages/ConnectDAppModalPage.qml b/storybook/pages/ConnectDAppModalPage.qml index 248ebf13ec..ed58d7dbb9 100644 --- a/storybook/pages/ConnectDAppModalPage.qml +++ b/storybook/pages/ConnectDAppModalPage.qml @@ -27,42 +27,17 @@ import shared.stores 1.0 Item { id: root - function openModal() { - modal.openWithFilter([1, 42161], JSON.parse(`{ - "metadata": { - "description": "React App for WalletConnect", - "icons": [ - "https://avatars.githubusercontent.com/u/37784886" - ], - "name": "React App", - "url": "https://react-app.walletconnect.com", - "verifyUrl": "https://verify.walletconnect.com" - }, - "publicKey": "300a6a1df4cb0cd73eb652f11845f35a318541eb18ab369860be85c0c2ada54a" - }`)) - if (pairedCheckbox.checked) { - pairedResultTimer.restart() - } - } - // qml Splitter SplitView { anchors.fill: parent - ColumnLayout { + PopupBackground { SplitView.fillWidth: true - Component.onCompleted: root.openModal() - - StatusButton { - id: openButton - - Layout.alignment: Qt.AlignHCenter - Layout.margins: 20 - - text: "Open ConnectDAppModal" - - onClicked: root.openModal() + Button { + text: "Open" + onClicked: modal.open() + anchors.centerIn: parent } ConnectDAppModal { @@ -70,6 +45,11 @@ Item { anchors.centerIn: parent + modal: false + closePolicy: Popup.NoAutoClose + + visible: true + spacing: 8 accounts: WalletAccountsModel {} @@ -78,9 +58,12 @@ Item { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: false; } } - } - ColumnLayout {} + dAppUrl: dAppUrlField.text + dAppName: dAppNameField.text + dAppIconUrl: hasIconCheckbox.checked ? "https://avatars.githubusercontent.com/u/37784886" : "" + connectionStatus: pairedCheckbox.checked ? pairedStatusCheckbox.checked ? connectionSuccessfulStatus : connectionFailedStatus : notConnectedStatus + } } ColumnLayout { @@ -100,6 +83,29 @@ Item { checked: true } + CheckBox { + id: hasIconCheckbox + + text: "Has Icon" + + checked: true + } + Label { + text: "DappName" + } + TextField { + id: dAppNameField + + text: "React App" + } + Label { + text: "DApp URL" + } + TextField { + id: dAppUrlField + + text: "https://react-app.walletconnect.com" + } Item { Layout.fillHeight: true } } } @@ -111,13 +117,7 @@ Item { running: false repeat: false onTriggered: { - if (pairedCheckbox.checked) { - if (pairedStatusCheckbox.checked) { - modal.pairSuccessful(null) - } else { - modal.pairFailed(null, "Pairing failed") - } - } + } } } diff --git a/storybook/pages/ConnectionStatusTagPage.qml b/storybook/pages/ConnectionStatusTagPage.qml new file mode 100644 index 0000000000..7123df2656 --- /dev/null +++ b/storybook/pages/ConnectionStatusTagPage.qml @@ -0,0 +1,25 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import shared.popups.walletconnect 1.0 + +Item { + id: root + + ConnectionStatusTag { + id: connectionTag + anchors.centerIn: parent + success: checkbox.checked + } + + CheckBox { + id: checkbox + anchors.bottom: connectionTag.top + anchors.horizontalCenter: connectionTag.horizontalCenter + text: "success" + checked: false + } +} + +// category: Components +// https://www.figma.com/design/HrmZp1y4S77QJezRFRl6ku/dApp-Interactions---Milestone-1?node-id=481-165960&t=oshb3aHNPCiUcQdH-0 \ No newline at end of file diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index 63b54f2199..212b79b637 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -18,6 +18,7 @@ import Models 1.0 import Storybook 1.0 import AppLayouts.Wallet.controls 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores import AppLayouts.Wallet.services.dapps 1.0 import SortFilterProxyModel 0.2 @@ -314,13 +315,18 @@ Item { } } - walletStore: WalletStore { - property var flatNetworks: SortFilterProxyModel { + walletRootStore: WalletStores.RootStore { + property string selectedAddress: "" + property var filteredFlatModel: SortFilterProxyModel { sourceModel: NetworksModel.flatNetworks filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; } } property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel - readonly property ListModel ownAccounts: accounts + readonly property ListModel nonWatchAccounts: accounts + + function getNetworkShortNames(chainIds) { + return "eth:oeth:arb" + } } onDisplayToastMessage: (message, isErr) => { diff --git a/storybook/pages/RoundImageWithBadgePage.qml b/storybook/pages/RoundImageWithBadgePage.qml new file mode 100644 index 0000000000..0fc24fd9f4 --- /dev/null +++ b/storybook/pages/RoundImageWithBadgePage.qml @@ -0,0 +1,52 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import shared.popups.walletconnect 1.0 + +SplitView { + Pane { + SplitView.fillWidth: true + SplitView.fillHeight: true + + RoundImageWithBadge { + id: roundImageWithBadge + + width: parent.width + height: width + imageUrl: addressField.text + badgeIcon: badgeField.text + fallbackIcon: fallbackIconField.text + } + } + + Pane { + id: controlsPane + SplitView.fillHeight: true + SplitView.preferredWidth: 300 + ColumnLayout { + Label { text: "Image url" } + TextField { + id: addressField + text: "https://picsum.photos/200/200" + Layout.fillWidth: true + } + Label { text: "Badge name" } + TextField { + id: badgeField + text: "walletConnect" + Layout.fillWidth: true + } + Label { text: "Fallback icon name" } + TextField { + id: fallbackIconField + text: "dapp" + Layout.fillWidth: true + } + } + } +} + +// category: Components + +// https://www.figma.com/design/HrmZp1y4S77QJezRFRl6ku/dApp-Interactions---Milestone-1?node-id=481-160233&t=xyix3QX5I3jxrDir-0 \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_ConnectDAppModal.qml b/storybook/qmlTests/tests/tst_ConnectDAppModal.qml new file mode 100644 index 0000000000..4affea393c --- /dev/null +++ b/storybook/qmlTests/tests/tst_ConnectDAppModal.qml @@ -0,0 +1,319 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import shared.popups.walletconnect 1.0 + +import Models 1.0 + +Item { + id: root + width: 600 + height: 800 + + Component { + id: accountsModelComponent + WalletAccountsModel {} + } + + Component { + id: componentUnderTest + ConnectDAppModal { + id: controlUnderTest + modal: false + anchors.centerIn: parent + accounts: WalletAccountsModel {} + flatNetworks: NetworksModel.flatNetworks + } + } + + TestCase { + id: testConnectDappModal + name: "ConnectDappModalTest" + when: windowShown + + SignalSpy { + id: connectSignalSpy + target: testConnectDappModal.dappModal + signalName: "connect" + } + + SignalSpy { + id: declineSignalSpy + target: testConnectDappModal.dappModal + signalName: "decline" + } + + SignalSpy { + id: disconnectSignalSpy + target: testConnectDappModal.dappModal + signalName: "disconnect" + } + + property ConnectDAppModal dappModal: null + + function cleanup() { + connectSignalSpy.clear() + declineSignalSpy.clear() + disconnectSignalSpy.clear() + } + + function test_initialState() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true}) + + verify(dappModal.visible, "ConnectDAppModal should be visible") + verify(dappModal.accounts, "ConnectDAppModal should have accounts") + verify(dappModal.flatNetworks, "ConnectDAppModal should have networks") + + compare(dappModal.width, 480) + compare(dappModal.height, 633) + compare(dappModal.dAppName, "") + compare(dappModal.dAppUrl, "") + compare(dappModal.dAppIconUrl, "") + compare(dappModal.connectionStatus, dappModal.notConnectedStatus) + } + + function test_notConnectedState() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true}) + + compare(dappModal.connectionStatus, dappModal.notConnectedStatus) + + // Reject button should be enabled + const rejectButton = findChild(dappModal, "rejectButton") + verify(rejectButton, "Reject button should be present") + compare(rejectButton.text, "Reject") + compare(rejectButton.enabled, true) + mouseClick(rejectButton) + compare(declineSignalSpy.count, 1) + + // Connect button should be enabled + const connectButton = findChild(dappModal, "primaryActionButton") + verify(connectButton, "Connect button should be present") + compare(connectButton.text, "Connect") + compare(connectButton.enabled, true) + mouseClick(connectButton) + compare(connectSignalSpy.count, 1) + + // Disconnect button should be disabled + const disconnectButton = findChild(dappModal, "disconnectButton") + verify(disconnectButton, "Disconnect button should be present") + compare(disconnectButton.text, "Disconnect") + compare(disconnectButton.visible, false) + mouseClick(disconnectButton) + compare(disconnectSignalSpy.count, 0) + + // Account selector should be enabled and user should be able to select an account + const accountSelector = findChild(dappModal, "accountSelector") + verify(accountSelector, "Account selector should be present") + compare(accountSelector.enabled, true) + compare(accountSelector.currentIndex, 0) + mouseClick(accountSelector) + compare(accountSelector.popup.visible, true) + + waitForItemPolished(accountSelector.popup.contentItem) + + const accountsList = findChild(accountSelector, "accountSelectorList") + verify(accountsList, "Accounts list should be present") + const selectAddress = accountsList.itemAtIndex(1).address + mouseClick(accountsList.itemAtIndex(1)) + compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress) + compare(dappModal.selectedAccountAddress, selectAddress) + + // Chain selector is enabled, all common chains preselected + const chainSelector = findChild(dappModal, "networkFilter") + verify(chainSelector, "Chain selector should be present") + compare(chainSelector.enabled, true) + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count) + + // User should be able to deselect a chain + mouseClick(chainSelector) + waitForItemPolished(chainSelector) + const networkSelectorList = findChild(chainSelector, "networkSelectorList") + verify(networkSelectorList, "Network selector list should be present") + mouseClick(networkSelectorList.itemAtIndex(0)) + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count - 1) + compare(chainSelector.selection[0], NetworksModel.flatNetworks.get(1).chainId) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 1) + compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(1).chainId) + } + + function test_connectedState() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true}) + dappModal.pairSuccessful() + compare(dappModal.connectionStatus, dappModal.connectionSuccessfulStatus) + + // Reject button should not be visible + const rejectButton = findChild(dappModal, "rejectButton") + verify(rejectButton, "Reject button should be present") + compare(rejectButton.visible, false) + mouseClick(rejectButton) + compare(declineSignalSpy.count, 0) + + // Close button should be enabled + const closeButton = findChild(dappModal, "primaryActionButton") + verify(closeButton, "Close button should be present") + compare(closeButton.text, "Close") + compare(closeButton.enabled, true) + compare(closeButton.visible, true) + mouseClick(closeButton) + compare(dappModal.opened, false) + dappModal.open() + + // Disconnect button should be enabled + const disconnectButton = findChild(dappModal, "disconnectButton") + verify(disconnectButton, "Disconnect button should be present") + compare(disconnectButton.text, "Disconnect") + compare(disconnectButton.visible, true) + compare(disconnectButton.enabled, true) + mouseClick(disconnectButton) + compare(disconnectSignalSpy.count, 1) + + // Account selector should be disabled + const accountSelector = findChild(dappModal, "accountSelector") + verify(accountSelector, "Account selector should be present") + compare(accountSelector.currentIndex, 0) + mouseClick(accountSelector) + compare(accountSelector.popup.visible, false) + + // Chain selector is disabled + const chainSelector = findChild(dappModal, "networkFilter") + verify(chainSelector, "Chain selector should be present") + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + + // User should not be able to deselect a chain + mouseClick(chainSelector) + waitForItemPolished(chainSelector) + const networkSelectorList = findChild(chainSelector, "networkSelectorList") + verify(networkSelectorList, "Network selector list should be present") + mouseClick(networkSelectorList.itemAtIndex(0)) + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count) + + const connectionTag = findChild(dappModal, "connectionStatusTag") + compare(connectionTag.visible, true) + compare(connectionTag.success, true) + } + + function test_connectionFailedState() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true}) + dappModal.pairFailed() + compare(dappModal.connectionStatus, dappModal.connectionFailedStatus) + + // Reject button should not be visible + const rejectButton = findChild(dappModal, "rejectButton") + verify(rejectButton, "Reject button should be present") + compare(rejectButton.visible, false) + mouseClick(rejectButton) + compare(declineSignalSpy.count, 0) + + // Close button should be enabled + const closeButton = findChild(dappModal, "primaryActionButton") + verify(closeButton, "Close button should be present") + compare(closeButton.text, "Close") + compare(closeButton.enabled, true) + compare(closeButton.visible, true) + mouseClick(closeButton) + compare(dappModal.opened, false) + dappModal.open() + + // Disconnect button should not be visible + const disconnectButton = findChild(dappModal, "disconnectButton") + verify(disconnectButton, "Disconnect button should be present") + compare(disconnectButton.text, "Disconnect") + compare(disconnectButton.visible, false) + mouseClick(disconnectButton) + compare(disconnectSignalSpy.count, 0) + + // Account selector should be disabled + const accountSelector = findChild(dappModal, "accountSelector") + verify(accountSelector, "Account selector should be present") + compare(accountSelector.currentIndex, 0) + mouseClick(accountSelector) + compare(accountSelector.popup.visible, false) + + // Chain selector is disabled + const chainSelector = findChild(dappModal, "networkFilter") + verify(chainSelector, "Chain selector should be present") + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + + // User should not be able to deselect a chain + mouseClick(chainSelector) + waitForItemPolished(chainSelector) + const networkSelectorList = findChild(chainSelector, "networkSelectorList") + verify(networkSelectorList, "Network selector list should be present") + mouseClick(networkSelectorList.itemAtIndex(0)) + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count) + + const connectionTag = findChild(dappModal, "connectionStatusTag") + compare(connectionTag.visible, true) + compare(connectionTag.success, false) + } + + function test_selectingAccount() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true, dAppChains: [1, 11155111]}) + + const accountSelector = findChild(dappModal, "accountSelector") + verify(accountSelector, "Account selector should be present") + compare(accountSelector.currentIndex, 0) + mouseClick(accountSelector) + compare(accountSelector.popup.visible, true) + + waitForItemPolished(accountSelector.popup.contentItem) + + const accountsList = findChild(accountSelector, "accountSelectorList") + verify(accountsList, "Accounts list should be present") + compare(accountsList.count, dappModal.accounts.count) + + const selectAddress = accountsList.itemAtIndex(1).address + mouseClick(accountsList.itemAtIndex(1)) + compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress) + compare(dappModal.selectedAccountAddress, selectAddress) + + const preselectedAddress = accountSelector.currentAccountAddress + + mouseClick(accountSelector) + compare(accountSelector.popup.visible, true) + + waitForItemPolished(accountSelector.popup.contentItem) + const selectAddress1 = accountsList.itemAtIndex(0).address + mouseClick(accountsList.itemAtIndex(0)) + compare(dappModal.selectedAccountAddress, accountSelector.currentAccountAddress) + compare(dappModal.selectedAccountAddress, selectAddress1) + + // Use preselected address + dappModal.selectedAccountAddress = preselectedAddress + compare(accountSelector.currentAccountAddress, preselectedAddress) + } + + function test_chainSelection() { + dappModal = createTemporaryObject(componentUnderTest, root, {visible: true}) + + const chainSelector = findChild(dappModal, "networkFilter") + verify(chainSelector, "Chain selector should be present") + // All selected + compare(chainSelector.selection.length, NetworksModel.flatNetworks.count) + compare(chainSelector.selection[0], NetworksModel.flatNetworks.get(0).chainId) + compare(chainSelector.selection[1], NetworksModel.flatNetworks.get(1).chainId) + + // User should be able to deselect a chain + mouseClick(chainSelector) + waitForItemPolished(chainSelector) + const networkSelectorList = findChild(chainSelector, "networkSelectorList") + verify(networkSelectorList, "Network selector list should be present") + mouseClick(networkSelectorList.itemAtIndex(0)) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 1) + compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(1).chainId) + + waitForItemPolished(networkSelectorList) + mouseClick(networkSelectorList.itemAtIndex(1)) + compare(dappModal.selectedChains.length, NetworksModel.flatNetworks.count - 2) + compare(dappModal.selectedChains[0], NetworksModel.flatNetworks.get(2).chainId) + + const connectButton = findChild(dappModal, "primaryActionButton") + verify(!!connectButton, "Connect button should be present") + compare(connectButton.visible, true) + compare(connectButton.enabled, true) + } + } +} \ No newline at end of file diff --git a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml index 025e364b2c..763588ae55 100644 --- a/storybook/qmlTests/tests/tst_DAppsWorkflow.qml +++ b/storybook/qmlTests/tests/tst_DAppsWorkflow.qml @@ -14,6 +14,7 @@ import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps.types 1.0 import AppLayouts.Profile.stores 1.0 import AppLayouts.Wallet.panels 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores import shared.stores 1.0 @@ -115,8 +116,9 @@ Item { Component { id: walletStoreComponent - WalletStore { - readonly property ListModel flatNetworks: ListModel { + WalletStores.RootStore { + property string selectedAddress: "" + readonly property ListModel filteredFlatModel: ListModel { ListElement { chainId: 1 } ListElement { chainId: 2 @@ -125,7 +127,7 @@ Item { } } - readonly property ListModel accounts: ListModel { + readonly property ListModel nonWatchAccounts: ListModel { ListElement {address: "0x1"} ListElement { address: "0x2" @@ -135,9 +137,9 @@ Item { } ListElement { address: "0x3a" } } - readonly property ListModel ownAccounts: accounts - - + function getNetworkShortNames(chainIds) { + return "eth:oeth:arb" + } } } @@ -167,7 +169,12 @@ Item { verify(!!sdk) let store = createTemporaryObject(dappsStoreComponent, root) verify(!!store) - handler = createTemporaryObject(dappsRequestHandlerComponent, root, {sdk: sdk, store: store, walletStore: walletStore}) + handler = createTemporaryObject(dappsRequestHandlerComponent, root, { + sdk: sdk, + store: store, + accountsModel: walletStore.nonWatchAccounts, + networksModel: walletStore.filteredFlatModel + }) verify(!!handler) } @@ -176,7 +183,7 @@ Item { } function test_TestAuthentication() { - let td = mockSessionRequestEvent(this, handler.sdk, handler.walletStore) + let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel) handler.authenticate(td.request) compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser") @@ -239,7 +246,7 @@ Item { verify(!!sdk) let store = createTemporaryObject(dappsStoreComponent, root) verify(!!store) - service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletStore: walletStore}) + service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore}) verify(!!service) } @@ -251,7 +258,7 @@ Item { function test_TestPairing() { // All calls to SDK are expected as events to be made by the wallet connect SDK let sdk = service.wcSDK - let walletStore = service.walletStore + let walletStore = service.walletRootStore let store = service.store service.pair("wc:12ab@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=12ab") @@ -262,12 +269,12 @@ Item { var args = sdk.buildApprovedNamespacesCalls[0] verify(!!args.supportedNamespaces, "expected supportedNamespaces to be set") let chainsForApproval = args.supportedNamespaces.eip155.chains - let networksArray = ModelUtils.modelToArray(walletStore.flatNetworks).map(entry => entry.chainId) + let networksArray = ModelUtils.modelToArray(walletStore.filteredFlatModel).map(entry => entry.chainId) verify(networksArray.every(chainId => chainsForApproval.some(eip155Chain => eip155Chain === `eip155:${chainId}`)), "expect all the networks to be present") // We test here all accounts for one chain only, we have separate tests to validate that all accounts are present let allAccountsForApproval = args.supportedNamespaces.eip155.accounts - let accountsArray = ModelUtils.modelToArray(walletStore.accounts).map(entry => entry.address) + let accountsArray = ModelUtils.modelToArray(walletStore.nonWatchAccounts).map(entry => entry.address) verify(accountsArray.every(address => allAccountsForApproval.some(eip155Address => eip155Address === `eip155:${networksArray[0]}:${address}`)), "expect at least all accounts for the first chain to be present" ) @@ -276,11 +283,11 @@ Item { sdk.buildApprovedNamespacesResult(allApprovedNamespaces, "") compare(connectDAppSpy.count, 1, "expected a call to service.connectDApp") let connectArgs = connectDAppSpy.signalArguments[0] - compare(connectArgs[connectDAppSpy.argPos.dappChains], networksArray, "expected all provided networks (walletStore.flatNetworks) for the dappChains") + compare(connectArgs[connectDAppSpy.argPos.dappChains], networksArray, "expected all provided networks (walletStore.filteredFlatModel) for the dappChains") verify(!!connectArgs[connectDAppSpy.argPos.sessionProposalJson], "expected sessionProposalJson to be set") verify(!!connectArgs[connectDAppSpy.argPos.availableNamespaces], "expected availableNamespaces to be set") - let selectedAccount = walletStore.accounts.get(1) + let selectedAccount = walletStore.nonWatchAccounts.get(1) service.approvePairSession(connectArgs[connectDAppSpy.argPos.sessionProposalJson], connectArgs[connectDAppSpy.argPos.dappChains], selectedAccount) compare(sdk.buildApprovedNamespacesCalls.length, 2, "expected a call to sdk.buildApprovedNamespaces") args = sdk.buildApprovedNamespacesCalls[1] @@ -314,7 +321,7 @@ Item { function test_SessionRequestMainFlow() { // All calls to SDK are expected as events to be made by the wallet connect SDK let sdk = service.wcSDK - let walletStore = service.walletStore + let walletStore = service.walletRootStore let store = service.store let testAddress = "0x3a" @@ -519,7 +526,7 @@ Item { verify(!!sdk) let store = createTemporaryObject(dappsStoreComponent, root) verify(!!store) - let service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletStore: walletStore}) + let service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore}) verify(!!service) controlUnderTest = createTemporaryObject(componentUnderTest, root, {wcService: service}) verify(!!controlUnderTest) @@ -581,7 +588,7 @@ Item { waitForRendering(controlUnderTest) let service = controlUnderTest.wcService - let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore) + let td = mockSessionRequestEvent(this, service.wcSDK, service.walletRootStore.nonWatchAccounts, service.walletRootStore.filteredFlatModel) waitForRendering(controlUnderTest) let popup = findChild(controlUnderTest, "dappsRequestModal") @@ -604,7 +611,7 @@ Item { waitForRendering(controlUnderTest) let service = controlUnderTest.wcService - let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore) + let td = mockSessionRequestEvent(this, service.wcSDK, service.walletRootStore.nonWatchAccounts, service.walletRootStore.filteredFlatModel) waitForRendering(controlUnderTest) let popup = findChild(controlUnderTest, "dappsRequestModal") @@ -625,9 +632,9 @@ Item { } } - function mockSessionRequestEvent(tc, sdk, walletStore) { - let account = walletStore.accounts.get(1) - let network = walletStore.flatNetworks.get(1) + function mockSessionRequestEvent(tc, sdk, accountsModel, networksMdodel) { + let account = accountsModel.get(1) + let network = networksMdodel.get(1) let method = "personal_sign" let message = "hello world" let params = [Helpers.strToHex(message), account.address] diff --git a/storybook/stubs/AppLayouts/Wallet/stores/RootStore.qml b/storybook/stubs/AppLayouts/Wallet/stores/RootStore.qml new file mode 100644 index 0000000000..c80bf1275e --- /dev/null +++ b/storybook/stubs/AppLayouts/Wallet/stores/RootStore.qml @@ -0,0 +1,5 @@ +import QtQml 2.15 + +QtObject { + id: root +} diff --git a/storybook/stubs/AppLayouts/Wallet/stores/qmldir b/storybook/stubs/AppLayouts/Wallet/stores/qmldir index 9be18ea7ec..2ef284383d 100644 --- a/storybook/stubs/AppLayouts/Wallet/stores/qmldir +++ b/storybook/stubs/AppLayouts/Wallet/stores/qmldir @@ -1,5 +1,6 @@ -CollectiblesStore 1.0 CollectiblesStore.qml -WalletAssetsStore 1.0 WalletAssetsStore.qml -TokensStore 1.0 TokensStore.qml ActivityFiltersStore 1.0 ActivityFiltersStore.qml +CollectiblesStore 1.0 CollectiblesStore.qml +RootStore 1.0 RootStore.qml SwapStore 1.0 SwapStore.qml +TokensStore 1.0 TokensStore.qml +WalletAssetsStore 1.0 WalletAssetsStore.qml diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index d0b6dc3f0b..af5b276c17 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -322,6 +322,7 @@ assets/img/icons/username.svg assets/img/icons/view.svg assets/img/icons/wallet.svg + assets/img/icons/walletConnect.svg assets/img/icons/warning.svg assets/img/icons/youtube.svg assets/img/status-logo-icon.png diff --git a/ui/StatusQ/src/assets/img/icons/dapp.svg b/ui/StatusQ/src/assets/img/icons/dapp.svg index 3649100f22..76455146c3 100644 --- a/ui/StatusQ/src/assets/img/icons/dapp.svg +++ b/ui/StatusQ/src/assets/img/icons/dapp.svg @@ -1,4 +1,4 @@ - + diff --git a/ui/StatusQ/src/assets/img/icons/walletConnect.svg b/ui/StatusQ/src/assets/img/icons/walletConnect.svg new file mode 100644 index 0000000000..d90457adfd --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/walletConnect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/app/AppLayouts/Profile/stores/WalletStore.qml b/ui/app/AppLayouts/Profile/stores/WalletStore.qml index cd7a28a868..8468662b19 100644 --- a/ui/app/AppLayouts/Profile/stores/WalletStore.qml +++ b/ui/app/AppLayouts/Profile/stores/WalletStore.qml @@ -43,13 +43,27 @@ QtObject { property var originModel: accountsModule.keyPairModel property var ownAccounts: SortFilterProxyModel { sourceModel: root.accounts - proxyRoles: FastExpressionRole { - name: "preferredSharingChainShortNames" - expression: { - return root.networksModuleInst.getNetworkShortNames(model.preferredSharingChainIds) + proxyRoles: [ + FastExpressionRole { + name: "preferredSharingChainShortNames" + expression: { + return root.networksModuleInst.getNetworkShortNames(model.preferredSharingChainIds) + } + expectedRoles: ["preferredSharingChainIds"] + }, + FastExpressionRole { + name: "color" + + function getColor(colorId) { + return Utils.getColorForId(colorId) + } + + // Direct call for singleton function is not handled properly by + // SortFilterProxyModel that's why helper function is used instead. + expression: { return getColor(model.colorId) } + expectedRoles: ["colorId"] } - expectedRoles: ["preferredSharingChainIds"] - } + ] filters: ValueFilter { roleName: "walletType" value: Constants.watchWalletType diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index dc958ffcd3..d3b32b2ead 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -2,6 +2,9 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import StatusQ 0.1 +import SortFilterProxyModel 0.2 + import AppLayouts.Wallet.controls 1.0 import shared.popups.walletconnect 1.0 @@ -51,19 +54,34 @@ DappsComboBox { active: false - onLoaded: item.openWithFilter(dappChains, sessionProposal.params.proposer) - property var dappChains: [] property var sessionProposal: null property var availableNamespaces: null property var sessionTopic: null + readonly property var proposalMedatada: !!sessionProposal + ? sessionProposal.params.proposer.metadata + : { name: "", url: "", icons: [] } sourceComponent: ConnectDAppModal { visible: true onClosed: connectDappLoader.active = false accounts: root.wcService.validAccounts - flatNetworks: root.wcService.flatNetworks + flatNetworks: SortFilterProxyModel { + sourceModel: root.wcService.flatNetworks + filters: [ + FastExpressionFilter { + inverted: true + expression: connectDappLoader.dappChains.indexOf(chainId) === -1 + expectedRoles: ["chainId"] + } + ] + } + selectedAccountAddress: root.wcService.selectedAccountAddress + + dAppUrl: proposalMedatada.url + dAppName: proposalMedatada.name + dAppIconUrl: !!proposalMedatada.icons && proposalMedatada.icons.length > 0 ? proposalMedatada.icons[0] : "" onConnect: { root.wcService.approvePairSession(sessionProposal, selectedChains, selectedAccount) @@ -171,9 +189,6 @@ DappsComboBox { } function onApproveSessionResult(session, err) { - connectDappLoader.dappChains = [] - connectDappLoader.sessionProposal = null - connectDappLoader.availableNamespaces = null connectDappLoader.sessionTopic = session.topic let modal = connectDappLoader.item diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml index 80d27467db..ae67374cfa 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsRequestHandler.qml @@ -14,8 +14,9 @@ QObject { id: root required property WalletConnectSDKBase sdk - required property var walletStore required property DAppsStore store + required property var accountsModel + required property var networksModel property alias requestsModel: requests @@ -189,8 +190,8 @@ QObject { } address = event.params.request.params[0].from } - return ModelUtils.getFirstModelEntryIf(walletStore.ownAccounts, (account) => { - return account.address.toLowerCase() === address.toLowerCase() + return ModelUtils.getFirstModelEntryIf(root.accountsModel, (account) => { + return account.address.toLowerCase() === address.toLowerCase(); }) } @@ -200,13 +201,7 @@ QObject { return null } let chainId = Helpers.chainIdFromEip155(event.params.chainId) - for (let i = 0; i < walletStore.flatNetworks.count; i++) { - let network = ModelUtils.get(walletStore.flatNetworks, i) - if (network.chainId === chainId) { - return network - } - } - return null + return ModelUtils.getByKey(root.networksModel, "chainId", chainId) } function extractMethodData(event, method) { diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index 763b4b3a38..555862d6e3 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -5,6 +5,7 @@ import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 import AppLayouts.Wallet 1.0 +import AppLayouts.Wallet.stores 1.0 as WalletStores import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps.types 1.0 import AppLayouts.Profile.stores 1.0 @@ -21,23 +22,20 @@ QObject { required property WalletConnectSDKBase wcSDK required property DAppsStore store - required property WalletStore walletStore + required property WalletStores.RootStore walletRootStore + + readonly property string selectedAccountAddress: walletRootStore.selectedAddress readonly property alias dappsModel: dappsProvider.dappsModel readonly property alias requestHandler: requestHandler readonly property var validAccounts: SortFilterProxyModel { - sourceModel: root.walletStore ? root.walletStore.accounts : null - filters: ValueFilter { - roleName: "walletType" - value: Constants.watchWalletType - inverted: true - } + sourceModel: root.walletRootStore.nonWatchAccounts proxyRoles: [ FastExpressionRole { name: "colorizedChainPrefixes" function getChainShortNames(chainIds) { - const chainShortNames = root.walletStore.getNetworkShortNames(chainIds) + const chainShortNames = root.walletRootStore.getNetworkShortNames(chainIds) return WalletUtils.colorizedChainPrefix(chainShortNames) } expression: getChainShortNames(model.preferredSharingChainIds) @@ -45,7 +43,7 @@ QObject { } ] } - readonly property var flatNetworks: root.walletStore ? root.walletStore.flatNetworks : null + readonly property var flatNetworks: root.walletRootStore.filteredFlatModel function pair(uri) { d.acceptedSessionProposal = null @@ -109,8 +107,9 @@ QObject { } // TODO #14754: implement custom dApp notification - let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" - root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_url), false) + const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" + const app_domain = StringUtils.extractDomainFromLink(app_url) + root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), false) // Persist session if(!store.addWalletConnectSession(JSON.stringify(session))) { @@ -124,20 +123,22 @@ QObject { } function onRejectSessionResult(err) { - let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" + const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" + const app_domain = StringUtils.extractDomainFromLink(app_url) if(err) { - root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_url), true) + root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), true) } else { - root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_url), false) + root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), false) } } function onSessionDelete(topic, err) { - let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" + const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" + const app_domain = StringUtils.extractDomainFromLink(app_url) if(err) { - root.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_url), true) + root.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_domain), true) } else { - root.displayToastMessage(qsTr("Disconnected from %1").arg(app_url), false) + root.displayToastMessage(qsTr("Disconnected from %1").arg(app_domain), false) } } } @@ -175,7 +176,8 @@ QObject { sdk: root.wcSDK store: root.store - walletStore: root.walletStore + accountsModel: root.validAccounts + networksModel: root.flatNetworks onSessionRequest: (request) => { root.sessionRequest(request) diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 5ec369d6f2..8b19f249bf 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -7,6 +7,7 @@ import shared.stores 1.0 as Stores import utils 1.0 +import StatusQ 0.1 import SortFilterProxyModel 0.2 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 as SQUtils @@ -65,7 +66,7 @@ QtObject { property var nonWatchAccounts: SortFilterProxyModel { sourceModel: accounts proxyRoles: [ - ExpressionRole { + FastExpressionRole { name: "color" function getColor(colorId) { @@ -75,6 +76,7 @@ QtObject { // Direct call for singleton function is not handled properly by // SortFilterProxyModel that's why helper function is used instead. expression: { return getColor(model.colorId) } + expectedRoles: ["colorId"] } ] filters: ValueFilter { @@ -159,8 +161,8 @@ QtObject { return d.chainColors[chainShortName] } - property var flatNetworks: networksModule.flatNetworks - property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel { + readonly property var flatNetworks: networksModule.flatNetworks + readonly property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel { sourceModel: root.flatNetworks filters: ValueFilter { roleName: "isTest"; value: root.areTestNetworksEnabled } } diff --git a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml index c9a0241ee1..70b9a9b929 100644 --- a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml +++ b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml @@ -35,7 +35,9 @@ StatusListView { property var selection: [] signal toggleNetwork(int chainId, int index) - + + objectName: "networkSelectorList" + onSelectionChanged: { if (!root.multiSelection && selection.length > 1) { console.warn("Warning: Multi-selection is disabled, but multiple items are selected. Automatically selecting the last inserted item.") diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index d14a41e14d..0f93a7f96f 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2057,7 +2057,7 @@ Item { store: DAppsStore { controller: WalletStore.RootStore.walletConnectController } - walletStore: appMain.rootStore.profileSectionStore.walletStore + walletRootStore: WalletStore.RootStore Component.onCompleted: { Global.walletConnectService = walletConnectService diff --git a/ui/imports/shared/controls/AccountSelector.qml b/ui/imports/shared/controls/AccountSelector.qml index 8d11de2000..10be7606f2 100644 --- a/ui/imports/shared/controls/AccountSelector.qml +++ b/ui/imports/shared/controls/AccountSelector.qml @@ -48,10 +48,7 @@ StatusComboBox { type: StatusComboBox.Type.Secondary size: StatusComboBox.Size.Small - currentIndex: { - if (count === 0) return - return Math.max(control.indexOfValue(d.currentAccountSelection), 0) - } + currentIndex: 0 objectName: "accountSelector" popupContentItemObjectName: "accountSelectorList" @@ -129,10 +126,10 @@ StatusComboBox { QtObject { id: d - property string currentAccountSelection: root.selectedAddress + property string currentAccountSelection: root.selectedAddress || root.currentValue Binding on currentAccountSelection { - value: root.selectedAddress + value: root.selectedAddress || root.currentValue } } } diff --git a/ui/imports/shared/controls/InformationTag.qml b/ui/imports/shared/controls/InformationTag.qml index 5a08bea104..7b467ae1fb 100644 --- a/ui/imports/shared/controls/InformationTag.qml +++ b/ui/imports/shared/controls/InformationTag.qml @@ -16,6 +16,7 @@ Control { property alias middleLabel: middleLabel property alias asset: smartIdenticon.asset property alias rightComponent: rightComponent.sourceComponent + property alias leftComponent: leftComponent.sourceComponent property bool loading: false property int secondarylabelMaxWidth: 100 @@ -48,6 +49,9 @@ Control { contentItem: RowLayout { spacing: root.spacing visible: !root.loading + Loader { + id: leftComponent + } StatusSmartIdenticon { id: smartIdenticon Layout.maximumWidth: visible ? 16 : 0 @@ -60,8 +64,10 @@ Control { } StatusBaseText { id: tagPrimaryLabel + Layout.maximumWidth: root.availableWidth font.pixelSize: Style.current.tertiaryTextFontSize visible: text !== "" + elide: Text.ElideRight } StatusBaseText { id: middleLabel diff --git a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml index 4b27038752..a67bb2d74e 100644 --- a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml +++ b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml @@ -22,73 +22,81 @@ import shared.popups.walletconnect.controls 1.0 import AppLayouts.Wallet.controls 1.0 import utils 1.0 +import shared.popups.walletconnect.private 1.0 StatusDialog { id: root - width: 480 - implicitHeight: d.connectionStatus === root.notConnectedStatus ? 633 : 681 + /* + Accounts model + Expected model structure: + name [string] - account name e.g. "Piggy Bank" + address [string] - wallet account address e.g. "0x1234567890" + colorizedChainPrefixes [string] - chain prefixes with rich text colors e.g. "eth:oeth:arb:" + emoji [string] - emoji for account e.g. "🐷" + colorId [string] - color id for account e.g. "1" + currencyBalance [var] - fiat currency balance + amount [number] - amount of currency e.g. 1234 + symbol [string] - currency symbol e.g. "USD" + optDisplayDecimals [number] - optional number of decimals to display + stripTrailingZeroes [bool] - strip trailing zeroes + walletType [string] - wallet type e.g. Constants.watchWalletType. See `Constants` for possible values + migratedToKeycard [bool] - whether account is migrated to keycard + accountBalance [var] - account balance for a specific network + formattedBalance [string] - formatted balance e.g. "1234.56B" + balance [string] - balance e.g. "123456000000" + iconUrl [string] - icon url e.g. "network/Network=Hermez" + chainColor [string] - chain color e.g. "#FF0000" + */ required property var accounts + /* + Networks model + Expected model structure: + chainName [string] - chain long name. e.g. "Ethereum" or "Optimism" + chainId [int] - chain unique identifier + iconUrl [string] - SVG icon name. e.g. "network/Network=Ethereum" + layer [int] - chain layer. e.g. 1 or 2 + isTest [bool] - true if the chain is a testnet + */ required property var flatNetworks - readonly property alias selectedAccount: d.selectedAccount + property alias dAppUrl: dappCard.dAppUrl + property alias dAppName: dappCard.name + property alias dAppIconUrl: dappCard.iconUrl + property alias connectionStatus: d.connectionStatus + + /* + Selected account address holds the initial account address selection for the account selector. + It is used to preselect the account in the account selector. + */ + property string selectedAccountAddress: contextCard.selectedAccount.address ?? "" + + readonly property alias selectedAccount: contextCard.selectedAccount readonly property alias selectedChains: d.selectedChains readonly property int notConnectedStatus: 0 readonly property int connectionSuccessfulStatus: 1 readonly property int connectionFailedStatus: 2 - function openWithFilter(dappChains, proposer) { - d.connectionStatus = root.notConnectedStatus - d.afterTwoSecondsFromStatus = false - - let m = proposer.metadata - dappCard.name = m.name - dappCard.url = m.url - if(m.icons.length > 0) { - dappCard.iconUrl = m.icons[0] - } else { - dappCard.iconUrl = "" - } - - d.dappChains.clear() - for (let i = 0; i < dappChains.length; i++) { - // Convert to int - d.dappChains.append({ chainId: parseInt(dappChains[i]) }) - } - - root.open() - } - function pairSuccessful(session) { d.connectionStatus = root.connectionSuccessfulStatus - closeAndRetryTimer.start() } function pairFailed(session, err) { d.connectionStatus = root.connectionFailedStatus - closeAndRetryTimer.start() - } - - Timer { - id: closeAndRetryTimer - - interval: 2000 - running: false - repeat: false - - onTriggered: { - d.afterTwoSecondsFromStatus = true - } } signal connect() signal decline() signal disconnect() + width: 480 + implicitHeight: !d.connectionAttempted ? 633 : 681 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - title: qsTr("Connection request") + title: d.connectionSuccessful ? qsTr("dApp connected") : + qsTr("Connection request") padding: 20 @@ -98,37 +106,40 @@ StatusDialog { DAppCard { id: dappCard - - afterTwoSecondsFromStatus: d.afterTwoSecondsFromStatus - - isConnectedSuccessfully: d.connectionStatus === root.connectionSuccessfulStatus - isConnectionFailed: d.connectionStatus === root.connectionFailedStatus - isConnectionStarted: d.connectionStatus !== root.notConnectedStatus - isConnectionFailedOrDisconnected: d.connectionStatus !== root.connectionSuccessfulStatus - - Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: root.availableWidth - Layout.leftMargin * 2 Layout.leftMargin: 12 Layout.rightMargin: Layout.leftMargin - Layout.topMargin: 20 + Layout.topMargin: 14 Layout.bottomMargin: Layout.topMargin } ContextCard { + id: contextCard + Layout.maximumWidth: root.availableWidth Layout.fillWidth: true - accountsProxy: d.accountsProxy - selectedAccount: d.selectedAccount - selectedChains: d.selectedChains - filteredChains: d.filteredChains - notConnected: d.connectionStatus === root.notConnectedStatus + + selectedAccountAddress: root.selectedAccountAddress + connectionAttempted: d.connectionAttempted + accountsModel: d.accountsProxy + chainsModel: root.flatNetworks + chainSelection: d.selectedChains + + onChainSelectionChanged: { + if (d.selectedChains !== chainSelection) { + d.selectedChains = chainSelection + } + } } PermissionsCard { + Layout.maximumWidth: root.availableWidth Layout.fillWidth: true - Layout.leftMargin: 12 + Layout.leftMargin: 16 Layout.rightMargin: Layout.leftMargin - Layout.topMargin: 20 + Layout.topMargin: 12 Layout.bottomMargin: Layout.topMargin + dappName: dappCard.name } } @@ -136,31 +147,39 @@ StatusDialog { id: footer rightButtons: ObjectModel { StatusButton { + objectName: "rejectButton" height: 44 - text: qsTr("Decline") + text: qsTr("Reject") - visible: d.connectionStatus === root.notConnectedStatus + visible: !d.connectionAttempted onClicked: root.decline() } - StatusButton { + StatusFlatButton { + objectName: "disconnectButton" height: 44 text: qsTr("Disconnect") - visible: d.connectionStatus === root.connectionSuccessfulStatus + visible: d.connectionSuccessful type: StatusBaseButton.Type.Danger onClicked: root.disconnect() } StatusButton { + objectName: "primaryActionButton" height: 44 - text: d.connectionStatus === root.notConnectedStatus - ? qsTr("Connect") - : qsTr("Close") + text: d.connectionAttempted + ? qsTr("Close") + : qsTr("Connect") + enabled: { + if (!d.connectionAttempted) + return root.selectedChains.length > 0 + return true + } onClicked: { - if (d.connectionStatus === root.notConnectedStatus) + if (!d.connectionAttempted) root.connect() else root.close() @@ -178,27 +197,19 @@ StatusDialog { sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder } } - property var selectedAccount: ({}) property var selectedChains: allChainIdsAggregator.value - readonly property var filteredChains: LeftJoinModel { - leftModel: d.dappChains - rightModel: root.flatNetworks - - joinRole: "chainId" - } - readonly property FunctionAggregator allChainIdsAggregator: FunctionAggregator { - model: d.filteredChains + model: root.flatNetworks initialValue: [] roleName: "chainId" aggregateFunction: (aggr, value) => [...aggr, value] } - readonly property var dappChains: ListModel {} - - property int connectionStatus: notConnectedStatus - property bool afterTwoSecondsFromStatus: false + property int connectionStatus: root.notConnectedStatus + readonly property bool connectionSuccessful: d.connectionStatus === root.connectionSuccessfulStatus + readonly property bool connectionFailed: d.connectionStatus === root.connectionFailedStatus + readonly property bool connectionAttempted: d.connectionStatus !== root.notConnectedStatus } } diff --git a/ui/imports/shared/popups/walletconnect/ConnectionStatusTag.qml b/ui/imports/shared/popups/walletconnect/ConnectionStatusTag.qml new file mode 100644 index 0000000000..a59f69738c --- /dev/null +++ b/ui/imports/shared/popups/walletconnect/ConnectionStatusTag.qml @@ -0,0 +1,57 @@ +import QtQuick 2.15 + +import StatusQ.Core.Theme 0.1 + +import shared.controls 1.0 +import utils 1.0 + +InformationTag { + id: root + + property bool success: false + + tagPrimaryLabel.text: qsTr("Connected. You can now go back to the dApp.") + tagPrimaryLabel.color: Theme.palette.directColor1 + tagPrimaryLabel.font.pixelSize: Style.current.additionalTextSize + backgroundColor: Theme.palette.successColor3 + bgBorderColor: Theme.palette.alphaColor(Theme.palette.successColor1, 0.4) + asset.color: tagPrimaryLabel.color + verticalPadding: Style.current.halfPadding + horizontalPadding: 12 + leftComponent: successBadge + + states: [ + State { + name: "error" + when: !root.success + PropertyChanges { target: tagPrimaryLabel; text: qsTr("Error connecting to dApp. Close and try again") } + PropertyChanges { target: tagPrimaryLabel; color: Theme.palette.dangerColor1 } + PropertyChanges { target: asset; name: "warning" } + PropertyChanges { + target: root + backgroundColor: Theme.palette.dangerColor3 + bgBorderColor: Theme.palette.alphaColor(Theme.palette.dangerColor1, 0.4) + } + } + ] + + ColorAnimation on backgroundColor { running: root.success && root.visible; to: Theme.palette.successColor2; duration: 2000 } + ColorAnimation on backgroundColor { running: !root.success && root.visible; to: "transparent"; duration: 2000 } + ColorAnimation on bgBorderColor { running: root.success && root.visible; to: Theme.palette.successColor3; duration: 2000 } + ColorAnimation on bgBorderColor { running: !root.success && root.visible; to: Theme.palette.dangerColor2; duration: 2000 } + + Component { + id: successBadge + Item { + width: visible ? 10 : 0 + height: visible ? 6 : 0 + visible: root.success + Rectangle { + width: 6 + height: 6 + radius: width / 2 + color: Theme.palette.successColor1 + } + } + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml b/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml new file mode 100644 index 0000000000..e476a3de66 --- /dev/null +++ b/ui/imports/shared/popups/walletconnect/RoundImageWithBadge.qml @@ -0,0 +1,86 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Components 0.1 + +import utils 1.0 + +Item { + id: root + + property url imageUrl: "" + property string badgeIcon: "walletConnect" + property string fallbackIcon: "dapp" + + readonly property bool iconLoaded: !mainImage.isError && !mainImage.isLoading && mainImage.image.source !== "" + + implicitWidth: mainImage.implicitWidth + implicitHeight: mainImage.implicitHeight + + Item { + id: imageContainer + + width: parent.width + height: width + + StatusRoundedImage { + id: mainImage + + width: parent.width + height: width + visible: !isError && !isLoading && root.imageUrl != "" + image.source: root.imageUrl + } + + Loader { + anchors.fill: mainImage + active: !mainImage.visible + sourceComponent: StatusRoundedComponent { + color: Theme.palette.primaryColor3 + StatusIcon { + anchors.fill: parent + anchors.margins: Style.current.padding + color: Theme.palette.primaryColor1 + icon: "dapp" + } + } + } + + layer.enabled: true + layer.effect: OpacityMask { + id: mask + invert: true + + maskSource: Item { + width: mask.width + 2 + height: mask.height + 2 + + Rectangle { + x: badge.x + 1 + y: badge.y + 1 + + width: badge.width + 2 + height: badge.width + 2 + radius: badge.width / 2 + } + } + } + } + + StatusRoundIcon { + id: badge + width: (root.width / 2) - Style.current.padding + height: width + anchors.bottom: parent.bottom + anchors.right: parent.right + asset.name: root.badgeIcon + asset.color: "transparent" + asset.width: width + asset.height: height + asset.bgWidth: width + asset.bgHeight: height + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/controls/DAppCard.qml b/ui/imports/shared/popups/walletconnect/controls/DAppCard.qml deleted file mode 100644 index 7cdfe9ef3c..0000000000 --- a/ui/imports/shared/popups/walletconnect/controls/DAppCard.qml +++ /dev/null @@ -1,137 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import StatusQ 0.1 -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Controls 0.1 -import StatusQ.Components 0.1 - -ColumnLayout { - id: root - - property alias name: appNameText.text - property alias url: appUrlText.text - property string iconUrl: "" - - property bool afterTwoSecondsFromStatus - property bool isConnectedSuccessfully: false - property bool isConnectionFailed: true - property bool isConnectionStarted: false - property bool isConnectionFailedOrDisconnected: true - - Rectangle { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 72 - Layout.preferredHeight: Layout.preferredWidth - - radius: width / 2 - color: Theme.palette.primaryColor3 - - StatusRoundedImage { - id: iconDisplay - - anchors.fill: parent - - visible: !fallbackImage.visible - - image.source: iconUrl - } - - StatusIcon { - id: fallbackImage - - anchors.centerIn: parent - - width: 40 - height: 40 - - icon: "dapp" - color: Theme.palette.primaryColor1 - - visible: iconDisplay.image.isLoading || iconDisplay.image.isError || !iconUrl - } - } - - StatusBaseText { - id: appNameText - - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 4 - - font.bold: true - font.pixelSize: 17 - } - - // TODO replace with the proper URL control - StatusLinkText { - id: appUrlText - - Layout.alignment: Qt.AlignHCenter - font.pixelSize: 15 - } - - Rectangle { - Layout.preferredWidth: pairingStatusLayout.implicitWidth + 32 - Layout.preferredHeight: pairingStatusLayout.implicitHeight + 14 - - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: 16 - - visible: root.isConnectionStarted - - color: root.isConnectedSuccessfully - ? root.afterTwoSecondsFromStatus - ? Theme.palette.successColor2 - : Theme.palette.successColor3 - : root.afterTwoSecondsFromStatus - ? "transparent" - : Theme.palette.dangerColor3 - border.color: root.isConnectedSuccessfully - ? Theme.palette.successColor2 - : Theme.palette.dangerColor2 - border.width: 1 - radius: height / 2 - - RowLayout { - id: pairingStatusLayout - - anchors.centerIn: parent - - spacing: 8 - - Rectangle { - width: 6 - height: 6 - radius: width / 2 - - visible: root.isConnectedSuccessfully - color: Theme.palette.successColor1 - } - - StatusIcon { - Layout.preferredWidth: 16 - Layout.preferredHeight: 16 - - visible: root.isConnectionFailedOrDisconnected - - color: Theme.palette.dangerColor1 - icon: "warning" - } - - StatusBaseText { - text: { - if (root.isConnectedSuccessfully) - return qsTr("Connected. You can now go back to the dApp.") - else if (root.isConnectionFailed) - return qsTr("Error connecting to dApp. Close and try again") - return "" - } - - font.pixelSize: 12 - color: root.isConnectedSuccessfully ? Theme.palette.directColor1 : Theme.palette.dangerColor1 - } - } - } -} diff --git a/ui/imports/shared/popups/walletconnect/controls/ContextCard.qml b/ui/imports/shared/popups/walletconnect/private/ContextCard.qml similarity index 61% rename from ui/imports/shared/popups/walletconnect/controls/ContextCard.qml rename to ui/imports/shared/popups/walletconnect/private/ContextCard.qml index d9c23778bf..5d5845659b 100644 --- a/ui/imports/shared/popups/walletconnect/controls/ContextCard.qml +++ b/ui/imports/shared/popups/walletconnect/private/ContextCard.qml @@ -1,88 +1,88 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.15 - -import StatusQ 0.1 -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 - -import AppLayouts.Wallet.controls 1.0 - -import shared.controls 1.0 - -Rectangle { - id: root - - property var accountsProxy - property var selectedAccount - property var selectedChains - property var filteredChains - property bool notConnected: true - - implicitWidth: contextLayout.implicitWidth - implicitHeight: contextLayout.implicitHeight - - radius: 8 - // TODO: the color matched the design color (grey4); It is also matching the intention or we should add some another color to the theme? (e.g. sectionBorder)? - border.color: Theme.palette.baseColor2 - border.width: 1 - color: "transparent" - - ColumnLayout { - id: contextLayout - - anchors.fill: parent - - RowLayout { - Layout.margins: 16 - - StatusBaseText { - text: qsTr("Connect with") - - Layout.fillWidth: true - } - - AccountSelector { - id: accountsDropdown - - Layout.preferredWidth: 204 - - control.enabled: root.notConnected && count > 1 - model: accountsProxy - onCurrentAccountChanged: root.selectedAccount = currentAccount - } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: root.border.color - } - - RowLayout { - Layout.margins: 16 - - StatusBaseText { - text: qsTr("On") - - Layout.fillWidth: true - } - - NetworkFilter { - id: networkFilter - Layout.preferredWidth: accountsDropdown.Layout.preferredWidth - - flatNetworks: root.filteredChains - showTitle: true - multiSelection: true - selectionAllowed: notConnected && root.selectedChains.length > 1 - selection: root.selectedChains - - onSelectionChanged: { - if (root.selectedChains !== networkFilter.selection) { - root.selectedChains = networkFilter.selection - } - } - } - } - } -} +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +import AppLayouts.Wallet.controls 1.0 +import shared.controls 1.0 + +Rectangle { + id: root + + property string selectedAccountAddress: "" + property bool connectionAttempted: false + property var accountsModel + property var chainsModel + property alias chainSelection: networkFilter.selection + + readonly property alias selectedAccount: accountsDropdown.currentAccount + + + implicitWidth: contextLayout.implicitWidth + implicitHeight: contextLayout.implicitHeight + + radius: 8 + // TODO: the color matched the design color (grey4); It is also matching the intention or we should add some another color to the theme? (e.g. sectionBorder)? + border.color: Theme.palette.baseColor2 + border.width: 1 + color: "transparent" + + ColumnLayout { + id: contextLayout + + anchors.fill: parent + + RowLayout { + Layout.margins: 16 + + StatusBaseText { + text: qsTr("Connect with") + + Layout.fillWidth: true + } + + AccountSelector { + id: accountsDropdown + + Layout.preferredWidth: 204 + Layout.preferredHeight: 38 + control.horizontalPadding: 12 + control.verticalPadding: 4 + control.enabled: !root.connectionAttempted && count > 1 + model: root.accountsModel + indicator.visible: control.enabled + selectedAddress: root.selectedAccountAddress + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.border.color + } + + RowLayout { + Layout.margins: 15 + + StatusBaseText { + text: qsTr("On") + + Layout.fillWidth: true + } + + NetworkFilter { + id: networkFilter + objectName: "networkFilter" + Layout.preferredWidth: accountsDropdown.Layout.preferredWidth + + flatNetworks: root.chainsModel + showTitle: true + multiSelection: true + showAllSelectedText: false + selectionAllowed: !root.connectionAttempted && root.chainsModel.ModelCount.count > 1 + } + } + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/private/DAppCard.qml b/ui/imports/shared/popups/walletconnect/private/DAppCard.qml new file mode 100644 index 0000000000..eb4dcb6c9c --- /dev/null +++ b/ui/imports/shared/popups/walletconnect/private/DAppCard.qml @@ -0,0 +1,74 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 + +import shared.popups.walletconnect 1.0 +import utils 1.0 + +ColumnLayout { + id: root + property alias name: appNameText.text + property url dAppUrl: "" + property url iconUrl: "" + + spacing: Style.current.padding + + RoundImageWithBadge { + objectName: "dappIcon" + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 72 + Layout.preferredHeight: Layout.preferredWidth + + imageUrl: iconUrl + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + StatusBaseText { + id: appNameText + objectName: "appNameText" + Layout.fillWidth: true + Layout.maximumWidth: root.width + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + font.bold: true + font.pixelSize: 17 + } + + StatusFlatButton { + id: appUrlText + objectName: "appUrlControl" + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: root.width + icon.name: "external-link" + icon.color: hovered ? Theme.palette.baseColor1 : Theme.palette.directColor1 + textPosition: StatusBaseButton.TextPosition.Left + size: StatusBaseButton.Size.Tiny + textColor: Theme.palette.directColor1 + hoverColor: "transparent" + spacing: 0 + font.pixelSize: 15 + font.weight: Font.Normal + horizontalPadding: 0 + verticalPadding: 0 + text: StringUtils.extractDomainFromLink(dAppUrl) + onClicked: { + Global.openLinkWithConfirmation(dAppUrl, text) + } + } + ConnectionStatusTag { + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: root.width + objectName: "connectionStatusTag" + success: d.connectionSuccessful + visible: d.connectionAttempted + } + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/controls/PermissionsCard.qml b/ui/imports/shared/popups/walletconnect/private/PermissionsCard.qml similarity index 64% rename from ui/imports/shared/popups/walletconnect/controls/PermissionsCard.qml rename to ui/imports/shared/popups/walletconnect/private/PermissionsCard.qml index ba7aa0c096..7769519780 100644 --- a/ui/imports/shared/popups/walletconnect/controls/PermissionsCard.qml +++ b/ui/imports/shared/popups/walletconnect/private/PermissionsCard.qml @@ -1,28 +1,34 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.15 - -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 - -ColumnLayout { - spacing: 8 - - StatusBaseText { - text: qsTr("Uniswap Interface will be able to:") - - font.pixelSize: 13 - color: Theme.palette.baseColor1 - } - - StatusBaseText { - text: qsTr("Check your account balance and activity") - - font.pixelSize: 13 - } - - StatusBaseText { - text: qsTr("Request transactions and message signing") - - font.pixelSize: 13 - } -} +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 + +ColumnLayout { + id: root + spacing: 8 + + property string dappName: "" + + StatusBaseText { + objectName: "permissionsTitle" + text: qsTr("%1 will be able to:").arg(root.dappName) + Layout.preferredHeight: 18 + font.pixelSize: 13 + color: Theme.palette.baseColor1 + } + + StatusBaseText { + text: qsTr("Check your account balance and activity") + Layout.preferredHeight: 18 + + font.pixelSize: 13 + } + + StatusBaseText { + text: qsTr("Request transactions and message signing") + Layout.preferredHeight: 18 + + font.pixelSize: 13 + } +} \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/private/qmldir b/ui/imports/shared/popups/walletconnect/private/qmldir new file mode 100644 index 0000000000..d60b972aab --- /dev/null +++ b/ui/imports/shared/popups/walletconnect/private/qmldir @@ -0,0 +1,3 @@ +ContextCard 1.0 ContextCard.qml +DAppCard 1.0 DAppCard.qml +PermissionsCard 1.0 PermissionsCard.qml \ No newline at end of file diff --git a/ui/imports/shared/popups/walletconnect/qmldir b/ui/imports/shared/popups/walletconnect/qmldir index ddc397c8c5..db9ca02679 100644 --- a/ui/imports/shared/popups/walletconnect/qmldir +++ b/ui/imports/shared/popups/walletconnect/qmldir @@ -1,5 +1,7 @@ PairWCModal 1.0 PairWCModal.qml DAppsListPopup 1.0 DAppsListPopup.qml ConnectDAppModal 1.0 ConnectDAppModal.qml +ConnectionStatusTag 1.0 ConnectionStatusTag.qml DAppRequestModal 1.0 DAppRequestModal.qml DAppsUriCopyInstructionsPopup 1.0 DAppsUriCopyInstructionsPopup.qml +RoundImageWithBadge 1.0 RoundImageWithBadge.qml