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

View File

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

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

View File

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

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 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)
# 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 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 ""

View File

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

View File

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

View File

@ -311,6 +311,7 @@
<file>assets/img/icons/token-sale.svg</file>
<file>assets/img/icons/token.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/travel-and-places.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
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

View File

@ -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);
}
}
}

View File

@ -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()
}
}

View File

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

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 {
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)

View File

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

View File

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

View File

@ -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()
}
}
}
}
}

View File

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

2
vendor/status-go vendored

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