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
This commit is contained in:
Stefan 2024-05-21 13:42:50 +03:00 committed by Stefan Dunca
parent 07bc6c49df
commit 35b81eadf6
23 changed files with 438 additions and 265 deletions

View File

@ -192,7 +192,7 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService, statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService,
result.tokenService, result.networkService, result.currencyService 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( result.messageService = message_service.newService(
statusFoundation.events, statusFoundation.events,
statusFoundation.threadpool, statusFoundation.threadpool,

View File

@ -18,12 +18,12 @@ import ./activity/controller as activityc
import ./activity/details_controller as activity_detailsc import ./activity/details_controller as activity_detailsc
import app/modules/shared_modules/collectible_details/controller as collectible_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/global/global_singleton
import app/core/eventemitter import app/core/eventemitter
import app/modules/shared_modules/add_account/module as add_account_module 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/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/keycard/service as keycard_service
import app_service/service/token/service as token_service import app_service/service/token/service as token_service
import app_service/service/collectible/service as collectible_service import app_service/service/collectible/service as collectible_service
@ -61,7 +61,6 @@ type
# shared modules # shared modules
addAccountModule: add_account_module.AccessInterface addAccountModule: add_account_module.AccessInterface
keypairImportModule: keypair_import_module.AccessInterface keypairImportModule: keypair_import_module.AccessInterface
walletConnectModule: wc_module.AccessInterface
# modules # modules
accountsModule: accounts_module.AccessInterface accountsModule: accounts_module.AccessInterface
allTokensModule: all_tokens_module.AccessInterface allTokensModule: all_tokens_module.AccessInterface
@ -80,6 +79,7 @@ type
savedAddressService: saved_address_service.Service savedAddressService: saved_address_service.Service
devicesService: devices_service.Service devicesService: devices_service.Service
walletConnectService: wc_service.Service walletConnectService: wc_service.Service
walletConnectController: wc_controller.Controller
activityController: activityc.Controller activityController: activityc.Controller
collectibleDetailsController: collectible_detailsc.Controller collectibleDetailsController: collectible_detailsc.Controller
@ -167,10 +167,10 @@ proc newModule*(
result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events) result.collectibleDetailsController = collectible_detailsc.newController(int32(backend_collectibles.CollectiblesRequestID.WalletAccount), networkService, events)
result.filter = initFilter(result.controller) result.filter = initFilter(result.controller)
result.walletConnectService = wc_service.newService(result.events, result.threadpool) result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService)
result.walletConnectModule = wc_module.newModule(result, result.events, result.walletAccountService, result.walletConnectService) 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) = method delete*(self: Module) =
self.accountsModule.delete self.accountsModule.delete

View File

@ -5,7 +5,7 @@ import ./activity/details_controller as activity_detailsc
import app/modules/shared_modules/collectible_details/controller as collectible_detailsc import app/modules/shared_modules/collectible_details/controller as collectible_detailsc
import ./io_interface import ./io_interface
import app/modules/shared_models/currency_amount 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 type
ActivityControllerArray* = array[2, activityc.Controller] ActivityControllerArray* = array[2, activityc.Controller]
@ -25,7 +25,7 @@ QtObject:
collectibleDetailsController: collectible_detailsc.Controller collectibleDetailsController: collectible_detailsc.Controller
isNonArchivalNode: bool isNonArchivalNode: bool
keypairOperabilityForObservedAccount: string keypairOperabilityForObservedAccount: string
wcModule: wc_module.AccessInterface wcController: QVariant
walletReady: bool walletReady: bool
addressFilters: string addressFilters: string
currentCurrency: string currentCurrency: string
@ -34,6 +34,8 @@ QtObject:
self.QObject.setup self.QObject.setup
proc delete*(self: View) = proc delete*(self: View) =
self.wcController.delete
self.QObject.delete self.QObject.delete
proc newView*(delegate: io_interface.AccessInterface, proc newView*(delegate: io_interface.AccessInterface,
@ -41,14 +43,14 @@ QtObject:
tmpActivityControllers: ActivityControllerArray, tmpActivityControllers: ActivityControllerArray,
activityDetailsController: activity_detailsc.Controller, activityDetailsController: activity_detailsc.Controller,
collectibleDetailsController: collectible_detailsc.Controller, collectibleDetailsController: collectible_detailsc.Controller,
wcModule: wc_module.AccessInterface): View = wcController: wc_controller.Controller): View =
new(result, delete) new(result, delete)
result.delegate = delegate result.delegate = delegate
result.activityController = activityController result.activityController = activityController
result.tmpActivityControllers = tmpActivityControllers result.tmpActivityControllers = tmpActivityControllers
result.activityDetailsController = activityDetailsController result.activityDetailsController = activityDetailsController
result.collectibleDetailsController = collectibleDetailsController result.collectibleDetailsController = collectibleDetailsController
result.wcModule = wcModule result.wcController = newQVariant(wcController)
result.setup() result.setup()
@ -236,13 +238,13 @@ QtObject:
proc emitDestroyKeypairImportPopup*(self: View) = proc emitDestroyKeypairImportPopup*(self: View) =
self.destroyKeypairImportPopup() self.destroyKeypairImportPopup()
proc getWalletConnectModule(self: View): QVariant {.slot.} = proc getWalletConnectController(self: View): QVariant {.slot.} =
if self.wcModule == nil: if self.wcController == nil:
return newQVariant() return newQVariant()
return self.wcModule.getModuleAsVariant() return self.wcController
QtProperty[QVariant] walletConnectModule: QtProperty[QVariant] walletConnectController:
read = getWalletConnectModule read = getWalletConnectController
proc walletReadyChanged*(self: View) {.signal.} proc walletReadyChanged*(self: View) {.signal.}

View File

@ -1,33 +1,36 @@
import NimQml
import chronicles 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 import app_service/service/wallet_connect/service as wallet_connect_service
logScope: logScope:
topics = "wallet-connect-controller" topics = "wallet-connect-controller"
type QtObject:
Controller* = ref object of RootObj type
delegate: io_interface.AccessInterface Controller* = ref object of QObject
events: EventEmitter service: wallet_connect_service.Service
walletAccountService: wallet_account_service.Service
walletConnectService: wallet_connect_service.Service
proc newController*(delegate: io_interface.AccessInterface, proc delete*(self: Controller) =
events: EventEmitter, self.QObject.delete
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 init*(self: Controller) = proc newController*(service: wallet_connect_service.Service): Controller =
discard new(result, delete)
proc addWalletConnectSession*(self: Controller, session_json: string): bool = result.service = service
echo "@ddd Controller.addWalletConnectSession", session_json.len
return self.walletConnectService.addSession(session_json) 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

View File

@ -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

View File

@ -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.}

View File

@ -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

View File

@ -1,7 +1,9 @@
import NimQml, chronicles import NimQml, chronicles, times
import backend/wallet_connect as status_go import backend/wallet_connect as status_go
import app_service/service/settings/service as settings_service
import app/global/global_singleton import app/global/global_singleton
import app/core/eventemitter import app/core/eventemitter
@ -17,6 +19,7 @@ QtObject:
type Service* = ref object of QObject type Service* = ref object of QObject
events: EventEmitter events: EventEmitter
threadpool: ThreadPool threadpool: ThreadPool
settingsService: settings_service.Service
proc delete*(self: Service) = proc delete*(self: Service) =
self.QObject.delete self.QObject.delete
@ -24,14 +27,23 @@ QtObject:
proc newService*( proc newService*(
events: EventEmitter, events: EventEmitter,
threadpool: ThreadPool, threadpool: ThreadPool,
settingsService: settings_service.Service,
): Service = ): Service =
new(result, delete) new(result, delete)
result.QObject.setup result.QObject.setup
result.events = events result.events = events
result.threadpool = threadpool result.threadpool = threadpool
result.settingsService = settings_service
proc init*(self: Service) = proc init*(self: Service) =
discard discard
proc addSession*(self: Service, session_json: string): bool = proc addSession*(self: Service, session_json: string): bool =
return status_go.addSession(session_json) # 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)

View File

@ -1,5 +1,5 @@
import options, logging import options, logging
import json import json, json_serialization
import core, response_type import core, response_type
from gen import rpc from gen import rpc
@ -18,3 +18,18 @@ proc addSession*(sessionJson: string): bool =
except Exception as e: except Exception as e:
warn "AddWalletConnectSession failed: ", "msg", e.msg warn "AddWalletConnectSession failed: ", "msg", e.msg
return false 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 ""

View File

@ -32,7 +32,7 @@ Item {
"metadata": { "metadata": {
"description": "React App for WalletConnect", "description": "React App for WalletConnect",
"icons": [ "icons": [
"https://avatars.githubusercontent.com/u/37784886" "https://avatars.githubusercontent.com/u/37784886"
], ],
"name": "React App", "name": "React App",
"url": "https://react-app.walletconnect.com", "url": "https://react-app.walletconnect.com",

View File

@ -98,15 +98,18 @@ Item {
} }
} }
ComboBox {
CheckBox { model: [{testCase: d.noTestCase, name: "No Test Case"},
{testCase: d.openDappsTestCase, name: "Open dApps"},
text: "Open Pair" {testCase: d.openPairTestCase, name: "Open Pair"}
checked: settings.openPair ]
onCheckedChanged: { textRole: "name"
settings.openPair = checked valueRole: "testCase"
if (checked) { currentIndex: settings.testCase
d.startPairing() onCurrentValueChanged: {
settings.testCase = currentValue
if (currentValue !== d.noTestCase) {
d.startTestCase()
} }
} }
@ -114,8 +117,8 @@ Item {
target: dappsWorkflow target: dappsWorkflow
// If Open Pair workflow if selected in the side bar // If Open Pair workflow if selected in the side bar
function onDAppsListReady() { function onDappsListReady() {
if (!d.startPairingWorkflowActive) if (d.activeTestCase < d.openPairTestCase)
return return
let items = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "DAppsListPopup") let items = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "DAppsListPopup")
@ -128,7 +131,7 @@ Item {
} }
function onPairWCReady() { function onPairWCReady() {
if (!d.startPairingWorkflowActive) if (d.activeTestCase < d.openPairTestCase)
return return
if (pairUriInput.text.length > 0) { if (pairUriInput.text.length > 0) {
@ -142,7 +145,7 @@ Item {
} }
function clickDoneIfSDKReady() { function clickDoneIfSDKReady() {
if (!d.startPairingWorkflowActive) { if (d.activeTestCase < d.openPairTestCase) {
return return
} }
@ -150,7 +153,7 @@ Item {
if (modals.length === 1) { if (modals.length === 1) {
let buttons = InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton") let buttons = InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton")
if (buttons.length === 1 && walletConnectService.wcSDK.sdkReady) { if (buttons.length === 1 && walletConnectService.wcSDK.sdkReady) {
d.startPairingWorkflowActive = false d.activeTestCase = d.noTestCase
buttons[0].clicked() buttons[0].clicked()
return return
} }
@ -173,8 +176,25 @@ Item {
} }
store: DAppsStore { store: DAppsStore {
signal dappsListReceived(string dappsJson)
function addWalletConnectSession(sessionJson) { function addWalletConnectSession(sessionJson) {
console.info("Persist Session", 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 { QtObject {
id: d id: d
property bool startPairingWorkflowActive: false property int activeTestCase: noTestCase
function startPairing() { function startTestCase() {
d.startPairingWorkflowActive = true d.activeTestCase = settings.testCase
if(root.visible) { if(root.visible) {
dappsWorkflow.clicked() 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: { onVisibleChanged: {
if (visible && d.startPairingWorkflowActive) { if (visible && d.activeTestCase !== d.noTestCase) {
d.startPairing() d.startTestCase()
} }
} }
Settings { Settings {
id: settings id: settings
property bool openPair: false property int testCase: d.noTestCase
property string pairUri: "" property string pairUri: ""
property bool testNetworks: false property bool testNetworks: false
} }

View File

@ -311,6 +311,7 @@
<file>assets/img/icons/token-sale.svg</file> <file>assets/img/icons/token-sale.svg</file>
<file>assets/img/icons/token.svg</file> <file>assets/img/icons/token.svg</file>
<file>assets/img/icons/destroy.svg</file> <file>assets/img/icons/destroy.svg</file>
<file>assets/img/icons/disconnect.svg</file>
<file>assets/img/icons/touch-id.svg</file> <file>assets/img/icons/touch-id.svg</file>
<file>assets/img/icons/travel-and-places.svg</file> <file>assets/img/icons/travel-and-places.svg</file>
<file>assets/img/icons/tributeToTalk.svg</file> <file>assets/img/icons/tributeToTalk.svg</file>

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Feature / Disconnect">
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M2.23291 3.29594C2.4928 2.9618 2.97436 2.9016 3.3085 3.16149L8.3723 7.10001C8.57572 7.25822 8.86512 7.2402 9.04734 7.05798L10.2771 5.8282C11.79 4.31537 14.0946 4.07686 15.857 5.11268C16.0734 5.23987 16.3522 5.22047 16.5297 5.04298L17.6127 3.96001C17.912 3.66068 18.3973 3.66068 18.6966 3.96001C18.996 4.25934 18.996 4.74465 18.6966 5.04398L17.6011 6.13952C17.4256 6.31499 17.4045 6.5898 17.5273 6.80541C18.5273 8.56084 18.2786 10.8344 16.7809 12.332L16.4545 12.6585C16.2371 12.8758 16.2595 13.2345 16.5021 13.4232L21.7041 17.4691C22.0382 17.729 22.0984 18.2106 21.8385 18.5447C21.5786 18.8789 21.0971 18.9391 20.7629 18.6792L2.36735 4.37154C2.03321 4.11165 1.97301 3.63009 2.23291 3.29594ZM10.4047 8.68079C10.1621 8.49209 10.1398 8.13346 10.3571 7.91612L11.3611 6.91217C12.5584 5.71485 14.4996 5.71485 15.697 6.91217C16.8943 8.10949 16.8943 10.0507 15.697 11.248L15.1447 11.8003C14.9624 11.9826 14.673 12.0006 14.4696 11.8424L10.4047 8.68079ZM12.9702 15.0408C13.2695 15.3401 13.2695 15.8254 12.9702 16.1247L11.3442 17.7507C9.84424 19.2507 7.56583 19.4979 5.80927 18.4923C5.59853 18.3716 5.3296 18.3924 5.1579 18.5641L3.30847 20.4135C3.00914 20.7129 2.52383 20.7129 2.2245 20.4135C1.92517 20.1142 1.92517 19.6289 2.2245 19.3296L4.06161 17.4925C4.23528 17.3188 4.25434 17.0461 4.12949 16.8346C3.0884 15.0711 3.32538 12.7619 4.84044 11.2469L5.74374 10.3436C6.04307 10.0442 6.52838 10.0442 6.82771 10.3436C7.12704 10.6429 7.12704 11.1282 6.82771 11.4275L5.9244 12.3309C4.72709 13.5282 4.72709 15.4694 5.9244 16.6667C7.12172 17.864 9.06296 17.864 10.2603 16.6667L11.8862 15.0408C12.1856 14.7414 12.6709 14.7414 12.9702 15.0408Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -14,7 +14,7 @@ ConnectedDappsButton {
required property WalletConnectService wcService required property WalletConnectService wcService
signal dAppsListReady() signal dappsListReady()
signal pairWCReady() signal pairWCReady()
onClicked: { onClicked: {
@ -52,18 +52,20 @@ ConnectedDappsButton {
onLoaded: { onLoaded: {
item.open() item.open()
root.dAppsListReady() root.dappsListReady()
} }
sourceComponent: DAppsListPopup { sourceComponent: DAppsListPopup {
visible: true visible: true
model: wcService.dappsModel
onPairWCDapp: { onPairWCDapp: {
pairWCLoader.active = true pairWCLoader.active = true
this.close() this.close()
} }
onOpened: { onOpened: {
this.x = root.width - this.menuWidth - 2 * this.padding this.x = root.width - this.contentWidth - 2 * this.padding
this.y = root.height + 4 this.y = root.height + 4
} }
onClosed: dappsListLoader.active = false onClosed: dappsListLoader.active = false

View File

@ -1,79 +1,86 @@
import QtQuick 2.15 import QtQuick 2.15
import StatusQ.Core.Utils 0.1
import shared.stores 1.0
import utils 1.0 import utils 1.0
QtObject { QObject {
id: root id: root
required property WalletConnectSDK sdk required property WalletConnectSDK sdk
required property DAppsStore store
readonly property alias pairingsModel: d.pairingsModel readonly property alias dappsModel: d.dappsModel
readonly property alias sessionsModel: d.sessionsModel
function updatePairings() { function updateDapps() {
d.resetPairingsModel() d.updateDappsModel()
}
function updateSessions() {
d.resetSessionsModel()
} }
readonly property QtObject _d: QtObject { QObject {
id: d id: d
property ListModel pairingsModel: ListModel { property ListModel dappsModel: ListModel {
id: pairings id: dapps
}
property ListModel sessionsModel: ListModel {
id: sessions
} }
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: let dappsList = JSON.parse(dappsJson);
// - the last made pairing will always have `active` prop set to false 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 // - 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 // - the list of dapps will display successfully made pairing as inactive
Backpressure.debounce(this, 250, () => { getActiveSessionsFn = () => {
sdk.getPairings((pairList) => { sdk.getActiveSessions((sessions) => {
for (let i = 0; i < pairList.length; i++) { root.store.dappsListReceived.disconnect(dappsListReceivedFn);
pairings.append(pairList[i]);
if (entryCallback) { // TODO #14755: on SDK dApps refresh update the model that has data source from persistence instead of using reset
entryCallback(pairList[i]) 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() { if (root.sdk.sdkReady) {
sessions.clear(); getActiveSessionsFn()
} else {
Backpressure.debounce(this, 250, () => { let conn = root.sdk.sdkReadyChanged.connect(() => {
sdk.getActiveSessions((sessionList) => { if (root.sdk.sdkReady) {
for (var topic of Object.keys(sessionList)) { getActiveSessionsFn()
sessions.append(sessionList[topic]);
} }
}); });
})();
}
function getPairingTopicFromPairingUrl(url)
{
if (!url.startsWith("wc:"))
{
return null;
} }
const atIndex = url.indexOf("@");
if (atIndex < 0)
{
return null;
}
return url.slice(3, atIndex);
} }
} }
} }

View File

@ -1,6 +1,7 @@
import QtQuick 2.15 import QtQuick 2.15
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Profile.stores 1.0 import AppLayouts.Profile.stores 1.0
@ -10,13 +11,15 @@ import shared.popups.walletconnect 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import utils 1.0 import utils 1.0
QtObject { QObject {
id: root id: root
required property WalletConnectSDK wcSDK required property WalletConnectSDK wcSDK
required property DAppsStore store required property DAppsStore store
required property WalletStore walletStore required property WalletStore walletStore
readonly property alias dappsModel: d.dappsProvider.dappsModel
readonly property var validAccounts: SortFilterProxyModel { readonly property var validAccounts: SortFilterProxyModel {
sourceModel: walletStore.accounts sourceModel: walletStore.accounts
filters: ValueFilter { filters: ValueFilter {
@ -28,12 +31,12 @@ QtObject {
readonly property var flatNetworks: walletStore.flatNetworks readonly property var flatNetworks: walletStore.flatNetworks
function pair(uri) { function pair(uri) {
_d.acceptedSessionProposal = null d.acceptedSessionProposal = null
wcSDK.pair(uri) wcSDK.pair(uri)
} }
function approvePairSession(sessionProposal, approvedChainIds, approvedAccount) { function approvePairSession(sessionProposal, approvedChainIds, approvedAccount) {
_d.acceptedSessionProposal = sessionProposal d.acceptedSessionProposal = sessionProposal
let approvedNamespaces = JSON.parse(Helpers.buildSupportedNamespaces(approvedChainIds, [approvedAccount.address])) let approvedNamespaces = JSON.parse(Helpers.buildSupportedNamespaces(approvedChainIds, [approvedAccount.address]))
wcSDK.buildApprovedNamespaces(sessionProposal.params, approvedNamespaces) wcSDK.buildApprovedNamespaces(sessionProposal.params, approvedNamespaces)
} }
@ -53,7 +56,7 @@ QtObject {
target: wcSDK target: wcSDK
function onSessionProposal(sessionProposal) { function onSessionProposal(sessionProposal) {
_d.currentSessionProposal = sessionProposal d.currentSessionProposal = sessionProposal
let supportedNamespacesStr = Helpers.buildSupportedNamespacesFromModels(root.flatNetworks, root.validAccounts) let supportedNamespacesStr = Helpers.buildSupportedNamespacesFromModels(root.flatNetworks, root.validAccounts)
wcSDK.buildApprovedNamespaces(sessionProposal.params, JSON.parse(supportedNamespacesStr)) wcSDK.buildApprovedNamespaces(sessionProposal.params, JSON.parse(supportedNamespacesStr))
@ -65,12 +68,12 @@ QtObject {
return return
} }
if (_d.acceptedSessionProposal) { if (d.acceptedSessionProposal) {
wcSDK.approveSession(_d.acceptedSessionProposal, approvedNamespaces) wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
} else { } else {
let res = Helpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) 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) root.approveSessionResult(session, err)
if (err) { if (err) {
// TODO #14676: handle the error
console.error("Failed to approve session", err)
return return
} }
// TODO #14754: implement custom dApp notification // 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, "") Global.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_url), "", "checkmark-circle", false, Constants.ephemeralNotificationType.success, "")
// Persist session // Persist session
store.addWalletConnectSession(JSON.stringify(session)) store.addWalletConnectSession(JSON.stringify(session))
d.dappsProvider.updateDapps()
} }
function onRejectSessionResult(err) { 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) { if(err) {
Global.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "") Global.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "")
} else { } else {
@ -100,7 +107,7 @@ QtObject {
} }
function onSessionDelete(topic, err) { 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) { if(err) {
Global.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "") Global.displayToastMessage(qsTr("Failed to disconnect from %1").arg(app_url), "", "warning", false, Constants.ephemeralNotificationType.danger, "")
} else { } else {
@ -109,12 +116,36 @@ QtObject {
} }
} }
readonly property QtObject _d: QtObject { QtObject {
id: d
property var currentSessionProposal: null property var currentSessionProposal: null
property var acceptedSessionProposal: null property var acceptedSessionProposal: null
readonly property DAppsListProvider dappsProvider: DAppsListProvider { readonly property DAppsListProvider dappsProvider: DAppsListProvider {
sdk: root.wcSDK 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()
}
} }

View File

@ -56,7 +56,7 @@ QtObject {
property var activityDetailsController: walletSectionInst.activityDetailsController property var activityDetailsController: walletSectionInst.activityDetailsController
property string signingPhrase: walletSectionInst.signingPhrase property string signingPhrase: walletSectionInst.signingPhrase
property string mnemonicBackedUp: walletSectionInst.isMnemonicBackedUp property string mnemonicBackedUp: walletSectionInst.isMnemonicBackedUp
property var walletConnectModule: walletSectionInst.walletConnectModule property var walletConnectController: walletSectionInst.walletConnectController
property CollectiblesStore collectiblesStore: CollectiblesStore {} property CollectiblesStore collectiblesStore: CollectiblesStore {}

View File

@ -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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@ -230,7 +211,7 @@ Popup {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: contentHeight Layout.preferredHeight: contentHeight
model: root.sdk.pairingsModel model: root.sdk.dappsModel
onDisconnect: function (topic) { onDisconnect: function (topic) {
root.sdk.disconnectPairing(topic) root.sdk.disconnectPairing(topic)

View File

@ -2044,7 +2044,7 @@ Item {
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
} }
store: DAppsStore { store: DAppsStore {
module: WalletStore.RootStore.walletConnectModule controller: WalletStore.RootStore.walletConnectController
} }
walletStore: appMain.rootStore.profileSectionStore.walletStore walletStore: appMain.rootStore.profileSectionStore.walletStore

View File

@ -4,6 +4,8 @@ import QtQuick.Layouts 1.15
import QtQml.Models 2.14 import QtQml.Models 2.14
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import QtGraphicalEffects 1.15
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Popups.Dialog 0.1 import StatusQ.Popups.Dialog 0.1
@ -41,7 +43,9 @@ StatusDialog {
dappCard.name = m.name dappCard.name = m.name
dappCard.url = m.url dappCard.url = m.url
if(m.icons.length > 0) { if(m.icons.length > 0) {
dappCard.icon = m.icons[0] dappCard.iconUrl = m.icons[0]
} else {
dappCard.iconUrl = ""
} }
d.dappChains.clear() d.dappChains.clear()
@ -230,23 +234,39 @@ StatusDialog {
component DAppCard: ColumnLayout { component DAppCard: ColumnLayout {
property alias name: appNameText.text property alias name: appNameText.text
property alias url: appUrlText.text property alias url: appUrlText.text
property alias icon: iconDisplay.asset.source property string iconUrl: ""
// 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
Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16 Layout.preferredWidth: 72
Layout.preferredHeight: Layout.preferredWidth
width: 72 radius: width / 2
height: 72 color: Theme.palette.primaryColor3
asset.width: width StatusRoundedImage {
asset.height: height id: iconDisplay
asset.color: "transparent"
asset.bgColor: "transparent" 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 { StatusBaseText {

View File

@ -3,8 +3,10 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15 import QtGraphicalEffects 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import shared.controls 1.0 import shared.controls 1.0
@ -13,25 +15,23 @@ Popup {
objectName: "dappsPopup" objectName: "dappsPopup"
property int menuWidth: 312 required property var model
signal pairWCDapp() signal pairWCDapp()
contentWidth: root.menuWidth
contentHeight: list.height
modal: false modal: false
padding: 8 padding: 8
closePolicy: Popup.CloseOnEscape | Popup.CloseOnOutsideClick | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnOutsideClick | Popup.CloseOnPressOutside
background: Rectangle { background: Rectangle {
id: bckgContent id: backgroundContent
color: Theme.palette.statusMenu.backgroundColor color: Theme.palette.statusMenu.backgroundColor
radius: 8 radius: 8
layer.enabled: true layer.enabled: true
layer.effect: DropShadow { layer.effect: DropShadow {
anchors.fill: parent anchors.fill: parent
source: bckgContent source: backgroundContent
horizontalOffset: 0 horizontalOffset: 0
verticalOffset: 4 verticalOffset: 4
radius: 12 radius: 12
@ -42,16 +42,56 @@ Popup {
} }
ColumnLayout { ColumnLayout {
id: list id: mainLayout
anchors.left: parent.left
anchors.right: parent.right implicitWidth: 280
width: parent.width
spacing: 8 spacing: 8
ShapeRectangle { ShapeRectangle {
id: listPlaceholder
text: qsTr("Connected dApps will appear here")
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: implicitHeight 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 { 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()
}
}
}
}
} }

View File

@ -1,9 +1,28 @@
import QtQuick 2.15 import QtQuick 2.15
QtObject { 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) { 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)
}
} }
} }

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit ad9032d036057cf00ae2d510b475e74fb3185b43 Subproject commit e06c490ec870a70ae72ede2b37f1235a3d903ed8