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