From 35b81eadf66a865df17968a427b44968eabcc9a7 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 21 May 2024 13:42:50 +0300 Subject: [PATCH] feat(dapps): show dapps list in wallet connect popup Things done here: Integrate basic functionality for wallet connect in status-go Update the list of dapps from the SDK Retrieve the persistence dapps list from the backend as a fallback if there is no connection and SDK can't be initialized Provide a basic simple view of dapps in the wallet connect popup Closes: #14557 --- src/app/boot/app_controller.nim | 2 +- .../modules/main/wallet_section/module.nim | 10 +- src/app/modules/main/wallet_section/view.nim | 20 +-- .../wallet_connect/controller.nim | 49 +++--- .../wallet_connect/io_interface.nim | 16 -- .../shared_modules/wallet_connect/module.nim | 51 ------ .../shared_modules/wallet_connect/view.nim | 23 --- .../service/wallet_connect/service.nim | 16 +- src/backend/wallet_connect.nim | 17 +- storybook/pages/ConnectDAppModalPage.qml | 2 +- storybook/pages/DAppsWorkflowPage.qml | 73 ++++++--- ui/StatusQ/src/assets.qrc | 1 + .../src/assets/img/icons/disconnect.svg | 5 + .../Wallet/panels/DAppsWorkflow.qml | 8 +- .../services/dapps/DAppsListProvider.qml | 107 ++++++------ .../services/dapps/WalletConnectService.qml | 53 ++++-- ui/app/AppLayouts/Wallet/stores/RootStore.qml | 2 +- .../POCWalletConnectModal.qml | 21 +-- ui/app/mainui/AppMain.qml | 2 +- .../popups/walletconnect/ConnectDAppModal.qml | 48 ++++-- .../popups/walletconnect/DAppsListPopup.qml | 152 ++++++++++++++++-- ui/imports/shared/stores/DAppsStore.qml | 23 ++- vendor/status-go | 2 +- 23 files changed, 438 insertions(+), 265 deletions(-) delete mode 100644 src/app/modules/shared_modules/wallet_connect/io_interface.nim delete mode 100644 src/app/modules/shared_modules/wallet_connect/module.nim delete mode 100644 src/app/modules/shared_modules/wallet_connect/view.nim create mode 100644 ui/StatusQ/src/assets/img/icons/disconnect.svg diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index f9473a80dc..a619eaf371 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -192,7 +192,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService, result.tokenService, result.networkService, result.currencyService ) - result.walletConnectService = wallet_connect_service.newService(statusFoundation.events, statusFoundation.threadpool) + result.walletConnectService = wallet_connect_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService) result.messageService = message_service.newService( statusFoundation.events, statusFoundation.threadpool, diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index ee3ffa336b..6a50dfa05a 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -18,12 +18,12 @@ import ./activity/controller as activityc import ./activity/details_controller as activity_detailsc import app/modules/shared_modules/collectible_details/controller as collectible_detailsc +import app/modules/shared_modules/wallet_connect/controller as wc_controller import app/global/global_singleton import app/core/eventemitter import app/modules/shared_modules/add_account/module as add_account_module import app/modules/shared_modules/keypair_import/module as keypair_import_module -import app/modules/shared_modules/wallet_connect/module as wc_module import app_service/service/keycard/service as keycard_service import app_service/service/token/service as token_service import app_service/service/collectible/service as collectible_service @@ -61,7 +61,6 @@ type # shared modules addAccountModule: add_account_module.AccessInterface keypairImportModule: keypair_import_module.AccessInterface - walletConnectModule: wc_module.AccessInterface # modules accountsModule: accounts_module.AccessInterface allTokensModule: all_tokens_module.AccessInterface @@ -80,6 +79,7 @@ type savedAddressService: saved_address_service.Service devicesService: devices_service.Service walletConnectService: wc_service.Service + walletConnectController: wc_controller.Controller activityController: activityc.Controller collectibleDetailsController: collectible_detailsc.Controller @@ -167,10 +167,10 @@ proc newModule*( result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events) result.filter = initFilter(result.controller) - result.walletConnectService = wc_service.newService(result.events, result.threadpool) - result.walletConnectModule = wc_module.newModule(result, result.events, result.walletAccountService, result.walletConnectService) + result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService) + result.walletConnectController = wc_controller.newController(result.walletConnectService) - result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectModule) + result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController) method delete*(self: Module) = self.accountsModule.delete diff --git a/src/app/modules/main/wallet_section/view.nim b/src/app/modules/main/wallet_section/view.nim index b33bf01633..d0eb0359d2 100644 --- a/src/app/modules/main/wallet_section/view.nim +++ b/src/app/modules/main/wallet_section/view.nim @@ -5,7 +5,7 @@ import ./activity/details_controller as activity_detailsc import app/modules/shared_modules/collectible_details/controller as collectible_detailsc import ./io_interface import app/modules/shared_models/currency_amount -import app/modules/shared_modules/wallet_connect/io_interface as wc_module +import app/modules/shared_modules/wallet_connect/controller as wc_controller type ActivityControllerArray* = array[2, activityc.Controller] @@ -25,7 +25,7 @@ QtObject: collectibleDetailsController: collectible_detailsc.Controller isNonArchivalNode: bool keypairOperabilityForObservedAccount: string - wcModule: wc_module.AccessInterface + wcController: QVariant walletReady: bool addressFilters: string currentCurrency: string @@ -34,6 +34,8 @@ QtObject: self.QObject.setup proc delete*(self: View) = + self.wcController.delete + self.QObject.delete proc newView*(delegate: io_interface.AccessInterface, @@ -41,14 +43,14 @@ QtObject: tmpActivityControllers: ActivityControllerArray, activityDetailsController: activity_detailsc.Controller, collectibleDetailsController: collectible_detailsc.Controller, - wcModule: wc_module.AccessInterface): View = + wcController: wc_controller.Controller): View = new(result, delete) result.delegate = delegate result.activityController = activityController result.tmpActivityControllers = tmpActivityControllers result.activityDetailsController = activityDetailsController result.collectibleDetailsController = collectibleDetailsController - result.wcModule = wcModule + result.wcController = newQVariant(wcController) result.setup() @@ -236,13 +238,13 @@ QtObject: proc emitDestroyKeypairImportPopup*(self: View) = self.destroyKeypairImportPopup() - proc getWalletConnectModule(self: View): QVariant {.slot.} = - if self.wcModule == nil: + proc getWalletConnectController(self: View): QVariant {.slot.} = + if self.wcController == nil: return newQVariant() - return self.wcModule.getModuleAsVariant() + return self.wcController - QtProperty[QVariant] walletConnectModule: - read = getWalletConnectModule + QtProperty[QVariant] walletConnectController: + read = getWalletConnectController proc walletReadyChanged*(self: View) {.signal.} diff --git a/src/app/modules/shared_modules/wallet_connect/controller.nim b/src/app/modules/shared_modules/wallet_connect/controller.nim index 2124e6b51f..61112805fb 100644 --- a/src/app/modules/shared_modules/wallet_connect/controller.nim +++ b/src/app/modules/shared_modules/wallet_connect/controller.nim @@ -1,33 +1,36 @@ +import NimQml import chronicles -import io_interface -import app/core/eventemitter -import app_service/service/wallet_account/service as wallet_account_service import app_service/service/wallet_connect/service as wallet_connect_service logScope: topics = "wallet-connect-controller" -type - Controller* = ref object of RootObj - delegate: io_interface.AccessInterface - events: EventEmitter - walletAccountService: wallet_account_service.Service - walletConnectService: wallet_connect_service.Service +QtObject: + type + Controller* = ref object of QObject + service: wallet_connect_service.Service -proc newController*(delegate: io_interface.AccessInterface, - events: EventEmitter, - walletAccountService: wallet_account_service.Service, - walletConnectService: wallet_connect_service.Service): Controller = - result = Controller() - result.delegate = delegate - result.events = events - result.walletAccountService = walletAccountService - result.walletConnectService = walletConnectService + proc delete*(self: Controller) = + self.QObject.delete -proc init*(self: Controller) = - discard + proc newController*(service: wallet_connect_service.Service): Controller = + new(result, delete) -proc addWalletConnectSession*(self: Controller, session_json: string): bool = - echo "@ddd Controller.addWalletConnectSession", session_json.len - return self.walletConnectService.addSession(session_json) \ No newline at end of file + result.service = service + + result.QObject.setup + + proc addWalletConnectSession*(self: Controller, session_json: string): bool {.slot.} = + return self.service.addSession(session_json) + + proc dappsListReceived*(self: Controller, dappsJson: string) {.signal.} + + # Emits signal dappsListReceived with the list of dApps + proc getDapps*(self: Controller): bool {.slot.} = + let res = self.service.getDapps() + if res == "": + return false + else: + self.dappsListReceived(res) + return true diff --git a/src/app/modules/shared_modules/wallet_connect/io_interface.nim b/src/app/modules/shared_modules/wallet_connect/io_interface.nim deleted file mode 100644 index 45ebc5126b..0000000000 --- a/src/app/modules/shared_modules/wallet_connect/io_interface.nim +++ /dev/null @@ -1,16 +0,0 @@ -import NimQml - -type - AccessInterface* {.pure inheritable.} = ref object of RootObj - -method delete*(self: AccessInterface) {.base.} = - raise newException(ValueError, "No implementation available") - -method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} = - raise newException(ValueError, "No implementation available") - -method addWalletConnectSession*(self: AccessInterface, session_json: string): bool {.base.} = - raise newException(ValueError, "No implementation available") - -type - DelegateInterface* = concept c diff --git a/src/app/modules/shared_modules/wallet_connect/module.nim b/src/app/modules/shared_modules/wallet_connect/module.nim deleted file mode 100644 index d17f095884..0000000000 --- a/src/app/modules/shared_modules/wallet_connect/module.nim +++ /dev/null @@ -1,51 +0,0 @@ -import NimQml, chronicles - -import io_interface -import view, controller -import app/core/eventemitter - -import app_service/service/wallet_account/service as wallet_account_service -import app_service/service/wallet_connect/service as wallet_connect_service -import app_service/service/keychain/service as keychain_service - -export io_interface - -logScope: - topics = "wallet-connect-module" - -type - Module*[T: io_interface.DelegateInterface] = ref object of io_interface.AccessInterface - delegate: T - view: View - viewVariant: QVariant - controller: Controller - -proc newModule*[T](delegate: T, - events: EventEmitter, - walletAccountService: wallet_account_service.Service, - walletConnectService: wallet_connect_service.Service): - Module[T] = - result = Module[T]() - result.delegate = delegate - result.view = view.newView(result) - result.viewVariant = newQVariant(result.view) - result.controller = controller.newController(result, events, walletAccountService, walletConnectService) - -proc addWalletConnectSession*[T](self: Module[T], session_json: string): bool = - echo "@dd Module.addWalletConnectSession: ", session_json - return self.controller.addWalletConnectSession(session_json) - -{.push warning[Deprecated]: off.} - -method delete*[T](self: Module[T]) = - self.view.delete - self.viewVariant.delete - -proc init[T](self: Module[T], fullConnect = true) = - self.controller.init() - -method getModuleAsVariant*[T](self: Module[T]): QVariant = - return self.viewVariant - - -{.pop.} diff --git a/src/app/modules/shared_modules/wallet_connect/view.nim b/src/app/modules/shared_modules/wallet_connect/view.nim deleted file mode 100644 index ce1b8062e4..0000000000 --- a/src/app/modules/shared_modules/wallet_connect/view.nim +++ /dev/null @@ -1,23 +0,0 @@ -import NimQml -import io_interface - -QtObject: - type - View* = ref object of QObject - delegate: io_interface.AccessInterface - - proc delete*(self: View) = - self.QObject.delete - - proc newView*(delegate: io_interface.AccessInterface): View = - new(result, delete) - result.QObject.setup - result.delegate = delegate - - proc addWalletConnectSession*(self: View, session_json: string): bool {.slot.} = - echo "@dd Vew.addWalletConnectSession: ", session_json, "; self.delegate.isNil: ", self.delegate.isNil - try: - return self.delegate.addWalletConnectSession(session_json) - except Exception as e: - echo "@dd Error in View.addWalletConnectSession: ", e.msg - return false \ No newline at end of file diff --git a/src/app_service/service/wallet_connect/service.nim b/src/app_service/service/wallet_connect/service.nim index 1dfda85155..ae15e27e5a 100644 --- a/src/app_service/service/wallet_connect/service.nim +++ b/src/app_service/service/wallet_connect/service.nim @@ -1,7 +1,9 @@ -import NimQml, chronicles +import NimQml, chronicles, times import backend/wallet_connect as status_go +import app_service/service/settings/service as settings_service + import app/global/global_singleton import app/core/eventemitter @@ -17,6 +19,7 @@ QtObject: type Service* = ref object of QObject events: EventEmitter threadpool: ThreadPool + settingsService: settings_service.Service proc delete*(self: Service) = self.QObject.delete @@ -24,14 +27,23 @@ QtObject: proc newService*( events: EventEmitter, threadpool: ThreadPool, + settingsService: settings_service.Service, ): Service = new(result, delete) result.QObject.setup result.events = events result.threadpool = threadpool + result.settingsService = settings_service proc init*(self: Service) = discard proc addSession*(self: Service, session_json: string): bool = - return status_go.addSession(session_json) \ No newline at end of file + # TODO #14588: call it async + return status_go.addSession(session_json) + + proc getDapps*(self: Service): string = + let validAtEpoch = now().toTime().toUnix() + let testChains = self.settingsService.areTestNetworksEnabled() + # TODO #14588: call it async + return status_go.getDapps(validAtEpoch, testChains) \ No newline at end of file diff --git a/src/backend/wallet_connect.nim b/src/backend/wallet_connect.nim index 1d4a3ab4c5..8a60094cd1 100644 --- a/src/backend/wallet_connect.nim +++ b/src/backend/wallet_connect.nim @@ -1,5 +1,5 @@ import options, logging -import json +import json, json_serialization import core, response_type from gen import rpc @@ -18,3 +18,18 @@ proc addSession*(sessionJson: string): bool = except Exception as e: warn "AddWalletConnectSession failed: ", "msg", e.msg return false + +proc getDapps*(validAtEpoch: int64, testChains: bool): string = + try: + let params = %*[validAtEpoch, testChains] + let rpcResRaw = callPrivateRPCNoDecode("wallet_getWalletConnectDapps", params) + let rpcRes = Json.decode(rpcResRaw, RpcResponse[JsonNode]) + if(not rpcRes.error.isNil): + return "" + + # Expect nil golang array to be valid empty array + let jsonArray = $rpcRes.result + return if jsonArray != "null": jsonArray else: "[]" + except Exception as e: + warn "GetWalletConnectDapps failed: ", "msg", e.msg + return "" diff --git a/storybook/pages/ConnectDAppModalPage.qml b/storybook/pages/ConnectDAppModalPage.qml index b34381d460..a8d8be700b 100644 --- a/storybook/pages/ConnectDAppModalPage.qml +++ b/storybook/pages/ConnectDAppModalPage.qml @@ -32,7 +32,7 @@ Item { "metadata": { "description": "React App for WalletConnect", "icons": [ - "https://avatars.githubusercontent.com/u/37784886" + "https://avatars.githubusercontent.com/u/37784886" ], "name": "React App", "url": "https://react-app.walletconnect.com", diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index 08c71a7df0..7533a06c04 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -98,15 +98,18 @@ Item { } } - - CheckBox { - - text: "Open Pair" - checked: settings.openPair - onCheckedChanged: { - settings.openPair = checked - if (checked) { - d.startPairing() + ComboBox { + model: [{testCase: d.noTestCase, name: "No Test Case"}, + {testCase: d.openDappsTestCase, name: "Open dApps"}, + {testCase: d.openPairTestCase, name: "Open Pair"} + ] + textRole: "name" + valueRole: "testCase" + currentIndex: settings.testCase + onCurrentValueChanged: { + settings.testCase = currentValue + if (currentValue !== d.noTestCase) { + d.startTestCase() } } @@ -114,8 +117,8 @@ Item { target: dappsWorkflow // If Open Pair workflow if selected in the side bar - function onDAppsListReady() { - if (!d.startPairingWorkflowActive) + function onDappsListReady() { + if (d.activeTestCase < d.openPairTestCase) return let items = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "DAppsListPopup") @@ -128,7 +131,7 @@ Item { } function onPairWCReady() { - if (!d.startPairingWorkflowActive) + if (d.activeTestCase < d.openPairTestCase) return if (pairUriInput.text.length > 0) { @@ -142,7 +145,7 @@ Item { } function clickDoneIfSDKReady() { - if (!d.startPairingWorkflowActive) { + if (d.activeTestCase < d.openPairTestCase) { return } @@ -150,7 +153,7 @@ Item { if (modals.length === 1) { let buttons = InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton") if (buttons.length === 1 && walletConnectService.wcSDK.sdkReady) { - d.startPairingWorkflowActive = false + d.activeTestCase = d.noTestCase buttons[0].clicked() return } @@ -173,8 +176,25 @@ Item { } store: DAppsStore { + signal dappsListReceived(string dappsJson) + function addWalletConnectSession(sessionJson) { console.info("Persist Session", sessionJson) + + let session = JSON.parse(sessionJson) + + let firstIconUrl = session.peer.metadata.icons.length > 0 ? session.peer.metadata.icons[0] : "" + let persistedDapp = { + "name": session.peer.metadata.name, + "url": session.peer.metadata.url, + "iconUrl": firstIconUrl + } + d.persistedDapps.push(persistedDapp) + } + + function getDapps() { + this.dappsListReceived(JSON.stringify(d.persistedDapps)) + return true } } @@ -191,26 +211,39 @@ Item { QtObject { id: d - property bool startPairingWorkflowActive: false + property int activeTestCase: noTestCase - function startPairing() { - d.startPairingWorkflowActive = true + function startTestCase() { + d.activeTestCase = settings.testCase if(root.visible) { dappsWorkflow.clicked() } } + + readonly property int noTestCase: 0 + readonly property int openDappsTestCase: 1 + readonly property int openPairTestCase: 2 + + property var persistedDapps: [ + {"name":"Test dApp 1", "url":"https://dapp.test/1","iconUrl":"https://se-sdk-dapp.vercel.app/assets/eip155:1.png"}, + {"name":"Test dApp 2", "url":"https://dapp.test/2","iconUrl":"https://react-app.walletconnect.com/assets/eip155-1.png"}, + {"name":"Test dApp 3", "url":"https://dapp.test/3","iconUrl":"https://react-app.walletconnect.com/assets/eip155-1.png"}, + {"name":"Test dApp 4 - very long name !!!!!!!!!!!!!!!!", "url":"https://dapp.test/4","iconUrl":"https://react-app.walletconnect.com/assets/eip155-1.png"}, + {"name":"Test dApp 5 - very long url", "url":"https://dapp.test/very_long/url/unusual","iconUrl":"https://react-app.walletconnect.com/assets/eip155-1.png"}, + {"name":"Test dApp 6", "url":"https://dapp.test/6","iconUrl":"https://react-app.walletconnect.com/assets/eip155-1.png"} + ] } onVisibleChanged: { - if (visible && d.startPairingWorkflowActive) { - d.startPairing() + if (visible && d.activeTestCase !== d.noTestCase) { + d.startTestCase() } } Settings { id: settings - property bool openPair: false + property int testCase: d.noTestCase property string pairUri: "" property bool testNetworks: false } diff --git a/ui/StatusQ/src/assets.qrc b/ui/StatusQ/src/assets.qrc index 73cf882d29..85067c1642 100644 --- a/ui/StatusQ/src/assets.qrc +++ b/ui/StatusQ/src/assets.qrc @@ -311,6 +311,7 @@ assets/img/icons/token-sale.svg assets/img/icons/token.svg assets/img/icons/destroy.svg + assets/img/icons/disconnect.svg assets/img/icons/touch-id.svg assets/img/icons/travel-and-places.svg assets/img/icons/tributeToTalk.svg diff --git a/ui/StatusQ/src/assets/img/icons/disconnect.svg b/ui/StatusQ/src/assets/img/icons/disconnect.svg new file mode 100644 index 0000000000..b2e01f1ce8 --- /dev/null +++ b/ui/StatusQ/src/assets/img/icons/disconnect.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml index 1692f20e29..d599444e62 100644 --- a/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml +++ b/ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml @@ -14,7 +14,7 @@ ConnectedDappsButton { required property WalletConnectService wcService - signal dAppsListReady() + signal dappsListReady() signal pairWCReady() onClicked: { @@ -52,18 +52,20 @@ ConnectedDappsButton { onLoaded: { item.open() - root.dAppsListReady() + root.dappsListReady() } sourceComponent: DAppsListPopup { visible: true + model: wcService.dappsModel + onPairWCDapp: { pairWCLoader.active = true this.close() } onOpened: { - this.x = root.width - this.menuWidth - 2 * this.padding + this.x = root.width - this.contentWidth - 2 * this.padding this.y = root.height + 4 } onClosed: dappsListLoader.active = false diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml index 6ed3b12dc1..5815e62ad9 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml @@ -1,79 +1,86 @@ import QtQuick 2.15 +import StatusQ.Core.Utils 0.1 + +import shared.stores 1.0 + import utils 1.0 -QtObject { +QObject { id: root required property WalletConnectSDK sdk + required property DAppsStore store - readonly property alias pairingsModel: d.pairingsModel - readonly property alias sessionsModel: d.sessionsModel + readonly property alias dappsModel: d.dappsModel - function updatePairings() { - d.resetPairingsModel() - } - function updateSessions() { - d.resetSessionsModel() + function updateDapps() { + d.updateDappsModel() } - readonly property QtObject _d: QtObject { + QObject { id: d - property ListModel pairingsModel: ListModel { - id: pairings - } - property ListModel sessionsModel: ListModel { - id: sessions + property ListModel dappsModel: ListModel { + id: dapps } - function resetPairingsModel(entryCallback) + property var dappsListReceivedFn: null + property var getActiveSessionsFn: null + function updateDappsModel() { - pairings.clear(); + dappsListReceivedFn = (dappsJson) => { + dapps.clear(); - // We have to postpone `getPairings` call, cause otherwise: - // - the last made pairing will always have `active` prop set to false + let dappsList = JSON.parse(dappsJson); + for (let i = 0; i < dappsList.length; i++) { + dapps.append(dappsList[i]); + } + } + root.store.dappsListReceived.connect(dappsListReceivedFn); + + // triggers a potential fast response from store.dappsListReceived + if (!store.getDapps()) { + console.warn("Failed retrieving dapps from persistence") + root.store.dappsListReceived.disconnect(dappsListReceivedFn); + } + + // TODO DEV: check if still holds true + // Reasons to postpone `getDapps` call: + // - the first recent made session will always have `active` prop set to false // - expiration date won't be the correct one, but one used in session proposal - // - the list of pairings will display succesfully made pairing as inactive - Backpressure.debounce(this, 250, () => { - sdk.getPairings((pairList) => { - for (let i = 0; i < pairList.length; i++) { - pairings.append(pairList[i]); + // - the list of dapps will display successfully made pairing as inactive + getActiveSessionsFn = () => { + sdk.getActiveSessions((sessions) => { + root.store.dappsListReceived.disconnect(dappsListReceivedFn); - if (entryCallback) { - entryCallback(pairList[i]) + // TODO #14755: on SDK dApps refresh update the model that has data source from persistence instead of using reset + dapps.clear(); + + let tmpMap = {} + for (let key in sessions) { + let dapp = sessions[key].peer.metadata + if (dapp.icons.length > 0) { + dapp.iconUrl = dapp.icons[0] } + tmpMap[dapp.url] = dapp; + } + // Iterate tmpMap and fill dapps + for (let key in tmpMap) { + dapps.append(tmpMap[key]); } }); - })(); - } + } - function resetSessionsModel() { - sessions.clear(); - - Backpressure.debounce(this, 250, () => { - sdk.getActiveSessions((sessionList) => { - for (var topic of Object.keys(sessionList)) { - sessions.append(sessionList[topic]); + if (root.sdk.sdkReady) { + getActiveSessionsFn() + } else { + let conn = root.sdk.sdkReadyChanged.connect(() => { + if (root.sdk.sdkReady) { + getActiveSessionsFn() } }); - })(); - } - - function getPairingTopicFromPairingUrl(url) - { - if (!url.startsWith("wc:")) - { - return null; } - - const atIndex = url.indexOf("@"); - if (atIndex < 0) - { - return null; - } - - return url.slice(3, atIndex); } } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index d380580eb9..cbd7e43182 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Profile.stores 1.0 @@ -10,13 +11,15 @@ import shared.popups.walletconnect 1.0 import SortFilterProxyModel 0.2 import utils 1.0 -QtObject { +QObject { id: root required property WalletConnectSDK wcSDK required property DAppsStore store required property WalletStore walletStore + readonly property alias dappsModel: d.dappsProvider.dappsModel + readonly property var validAccounts: SortFilterProxyModel { sourceModel: walletStore.accounts filters: ValueFilter { @@ -28,12 +31,12 @@ QtObject { readonly property var flatNetworks: walletStore.flatNetworks function pair(uri) { - _d.acceptedSessionProposal = null + d.acceptedSessionProposal = null wcSDK.pair(uri) } function approvePairSession(sessionProposal, approvedChainIds, approvedAccount) { - _d.acceptedSessionProposal = sessionProposal + d.acceptedSessionProposal = sessionProposal let approvedNamespaces = JSON.parse(Helpers.buildSupportedNamespaces(approvedChainIds, [approvedAccount.address])) wcSDK.buildApprovedNamespaces(sessionProposal.params, approvedNamespaces) } @@ -53,7 +56,7 @@ QtObject { target: wcSDK function onSessionProposal(sessionProposal) { - _d.currentSessionProposal = sessionProposal + d.currentSessionProposal = sessionProposal let supportedNamespacesStr = Helpers.buildSupportedNamespacesFromModels(root.flatNetworks, root.validAccounts) wcSDK.buildApprovedNamespaces(sessionProposal.params, JSON.parse(supportedNamespacesStr)) @@ -65,12 +68,12 @@ QtObject { return } - if (_d.acceptedSessionProposal) { - wcSDK.approveSession(_d.acceptedSessionProposal, approvedNamespaces) + if (d.acceptedSessionProposal) { + wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces) } else { let res = Helpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) - root.connectDApp(res.chains, _d.currentSessionProposal, approvedNamespaces) + root.connectDApp(res.chains, d.currentSessionProposal, approvedNamespaces) } } @@ -79,19 +82,23 @@ QtObject { root.approveSessionResult(session, err) if (err) { + // TODO #14676: handle the error + console.error("Failed to approve session", err) return } // TODO #14754: implement custom dApp notification - let app_url = _d.currentSessionProposal ? _d.currentSessionProposal.params.proposer.metadata.url : "-" + let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" Global.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_url), "", "checkmark-circle", false, Constants.ephemeralNotificationType.success, "") // Persist session store.addWalletConnectSession(JSON.stringify(session)) + + d.dappsProvider.updateDapps() } function onRejectSessionResult(err) { - let app_url = _d.currentSessionProposal ? _d.currentSessionProposal.params.proposer.metadata.url : "-" + let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" if(err) { Global.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "") } else { @@ -100,7 +107,7 @@ QtObject { } function onSessionDelete(topic, err) { - let app_url = _d.currentSessionProposal ? _d.currentSessionProposal.params.proposer.metadata.url : "-" + let app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" if(err) { Global.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "") } else { @@ -109,12 +116,36 @@ QtObject { } } - readonly property QtObject _d: QtObject { + QtObject { + id: d + property var currentSessionProposal: null property var acceptedSessionProposal: null readonly property DAppsListProvider dappsProvider: DAppsListProvider { sdk: root.wcSDK + store: root.store + } + + // TODO #14676: use it to check if already paired + function getPairingTopicFromPairingUrl(url) + { + if (!url.startsWith("wc:")) + { + return null; + } + + const atIndex = url.indexOf("@"); + if (atIndex < 0) + { + return null; + } + + return url.slice(3, atIndex); } } + + Component.onCompleted: { + d.dappsProvider.updateDapps() + } } \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 7450d316b8..cca93870f2 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -56,7 +56,7 @@ QtObject { property var activityDetailsController: walletSectionInst.activityDetailsController property string signingPhrase: walletSectionInst.signingPhrase property string mnemonicBackedUp: walletSectionInst.isMnemonicBackedUp - property var walletConnectModule: walletSectionInst.walletConnectModule + property var walletConnectController: walletSectionInst.walletConnectController property CollectiblesStore collectiblesStore: CollectiblesStore {} diff --git a/ui/app/AppLayouts/Wallet/views/pocwalletconnect/POCWalletConnectModal.qml b/ui/app/AppLayouts/Wallet/views/pocwalletconnect/POCWalletConnectModal.qml index 22fbc06d58..a4e991fff0 100644 --- a/ui/app/AppLayouts/Wallet/views/pocwalletconnect/POCWalletConnectModal.qml +++ b/ui/app/AppLayouts/Wallet/views/pocwalletconnect/POCWalletConnectModal.qml @@ -204,25 +204,6 @@ Popup { } } - ColumnLayout { - Layout.fillWidth: true - - POCSessions { - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - - model: root.sdk.sessionsModel - - onDisconnect: function (topic) { - root.sdk.disconnectSession(topic) - } - - onPing: function (topic) { - root.sdk.ping(topic) - } - } - } - ColumnLayout { Layout.fillWidth: true @@ -230,7 +211,7 @@ Popup { Layout.fillWidth: true Layout.preferredHeight: contentHeight - model: root.sdk.pairingsModel + model: root.sdk.dappsModel onDisconnect: function (topic) { root.sdk.disconnectPairing(topic) diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index 87895ec733..4f84fd41e7 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2044,7 +2044,7 @@ Item { projectId: WalletStore.RootStore.appSettings.walletConnectProjectID } store: DAppsStore { - module: WalletStore.RootStore.walletConnectModule + controller: WalletStore.RootStore.walletConnectController } walletStore: appMain.rootStore.profileSectionStore.walletStore diff --git a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml index 998e838018..188aef9ee2 100644 --- a/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml +++ b/ui/imports/shared/popups/walletconnect/ConnectDAppModal.qml @@ -4,6 +4,8 @@ import QtQuick.Layouts 1.15 import QtQml.Models 2.14 import SortFilterProxyModel 0.2 +import QtGraphicalEffects 1.15 + import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Popups.Dialog 0.1 @@ -41,7 +43,9 @@ StatusDialog { dappCard.name = m.name dappCard.url = m.url if(m.icons.length > 0) { - dappCard.icon = m.icons[0] + dappCard.iconUrl = m.icons[0] + } else { + dappCard.iconUrl = "" } d.dappChains.clear() @@ -230,23 +234,39 @@ StatusDialog { component DAppCard: ColumnLayout { property alias name: appNameText.text property alias url: appUrlText.text - property alias icon: iconDisplay.asset.source - - // TODO: this doesn't work as expected, the icon is not displayed properly - // TODO: set a fallback icon for when the provided icon is not available - StatusRoundIcon { - id: iconDisplay + property string iconUrl: "" + Rectangle { Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: 16 + Layout.preferredWidth: 72 + Layout.preferredHeight: Layout.preferredWidth - width: 72 - height: 72 + radius: width / 2 + color: Theme.palette.primaryColor3 - asset.width: width - asset.height: height - asset.color: "transparent" - asset.bgColor: "transparent" + 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 { diff --git a/ui/imports/shared/popups/walletconnect/DAppsListPopup.qml b/ui/imports/shared/popups/walletconnect/DAppsListPopup.qml index 606343c13e..83ea5cde80 100644 --- a/ui/imports/shared/popups/walletconnect/DAppsListPopup.qml +++ b/ui/imports/shared/popups/walletconnect/DAppsListPopup.qml @@ -3,8 +3,10 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtGraphicalEffects 1.15 +import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 import shared.controls 1.0 @@ -13,25 +15,23 @@ Popup { objectName: "dappsPopup" - property int menuWidth: 312 + required property var model signal pairWCDapp() - contentWidth: root.menuWidth - contentHeight: list.height modal: false padding: 8 closePolicy: Popup.CloseOnEscape | Popup.CloseOnOutsideClick | Popup.CloseOnPressOutside background: Rectangle { - id: bckgContent + id: backgroundContent color: Theme.palette.statusMenu.backgroundColor radius: 8 layer.enabled: true layer.effect: DropShadow { anchors.fill: parent - source: bckgContent + source: backgroundContent horizontalOffset: 0 verticalOffset: 4 radius: 12 @@ -42,16 +42,56 @@ Popup { } ColumnLayout { - id: list - anchors.left: parent.left - anchors.right: parent.right - width: parent.width + id: mainLayout + + implicitWidth: 280 + spacing: 8 ShapeRectangle { + id: listPlaceholder + + text: qsTr("Connected dApps will appear here") + Layout.fillWidth: true Layout.preferredHeight: implicitHeight - text: qsTr("Connected dApps will appear here") + + visible: listView.count === 0 + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + Layout.leftMargin: 8 + + visible: !listPlaceholder.visible + + StatusBaseText { + text: qsTr("Connected dApps") + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + font.pixelSize: 12 + color: Theme.palette.baseColor1 + } + } + + StatusListView { + id: listView + + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + Layout.maximumHeight: 280 + + model: root.model + visible: !listPlaceholder.visible + + delegate: DAppDelegate { + implicitWidth: listView.width + } + + ScrollBar.vertical: null } StatusButton { @@ -64,4 +104,96 @@ Popup { } } } + + component DAppDelegate: Item { + implicitHeight: 50 + + required property string name + required property string url + required property string iconUrl + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + + Item { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + + StatusImage { + id: iconImage + + anchors.fill: parent + + source: iconUrl + visible: !fallbackImage.visible + } + + StatusIcon { + id: fallbackImage + + anchors.fill: parent + + icon: "dapp" + color: Theme.palette.baseColor1 + + visible: iconImage.isLoading || iconImage.isError || !iconUrl + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: iconImage.width + height: iconImage.height + radius: width / 2 + visible: false + } + } + } + + ColumnLayout { + Layout.leftMargin: 12 + Layout.rightMargin: 12 + + StatusBaseText { + text: name + + Layout.fillWidth: true + + font.pixelSize: 13 + font.bold: true + + elide: Text.ElideRight + + clip: true + } + StatusBaseText { + text: url + + Layout.fillWidth: true + + font.pixelSize: 12 + color: Theme.palette.baseColor1 + + elide: Text.ElideRight + + clip: true + } + } + + // TODO #14588 - Show tooltip on hover "Disconnect dApp" + StatusRoundButton { + implicitWidth: 32 + implicitHeight: 32 + radius: width / 2 + + icon.name: "disconnect" + + onClicked: { + console.debug(`TODO #14755 - Disconnect ${name}`) + //root.disconnectDapp() + } + } + } + } } diff --git a/ui/imports/shared/stores/DAppsStore.qml b/ui/imports/shared/stores/DAppsStore.qml index ded2ca0c60..c6270a9861 100644 --- a/ui/imports/shared/stores/DAppsStore.qml +++ b/ui/imports/shared/stores/DAppsStore.qml @@ -1,9 +1,28 @@ import QtQuick 2.15 QtObject { - required property var module + id: root + + required property var controller + + /// \c dappsJson serialized from status-go.wallet.GetDapps + signal dappsListReceived(string dappsJson) function addWalletConnectSession(sessionJson) { - module.addWalletConnectSession(sessionJson) + controller.addWalletConnectSession(sessionJson) + } + + /// \c getDapps triggers an async response to \c dappsListReceived + function getDapps() { + return controller.getDapps() + } + + // Handle async response from controller + property Connections _connections: Connections { + target: controller + + function onDappsListReceived(dappsJson) { + root.dappsListReceived(dappsJson) + } } } \ No newline at end of file diff --git a/vendor/status-go b/vendor/status-go index ad9032d036..e06c490ec8 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit ad9032d036057cf00ae2d510b475e74fb3185b43 +Subproject commit e06c490ec870a70ae72ede2b37f1235a3d903ed8