fix(dApps): Improved handling of connected dApps. (#15985)

1. Hiding DApps button on not supported wallet account selection
2. Filtering DApps in connected dApps list based on account selection

closes: #15589
closes: #15647
This commit is contained in:
Roman Chornii 2024-08-05 16:41:20 +03:00 committed by GitHub
parent 6159f53839
commit 6aa6746de2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 177 additions and 42 deletions

View File

@ -427,6 +427,8 @@ Item {
// Name mismatch between storybook and production // Name mismatch between storybook and production
readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel
} }
readonly property string selectedAddress: ""
} }
onDisplayToastMessage: (message, isErr) => { onDisplayToastMessage: (message, isErr) => {

View File

@ -17,7 +17,7 @@ SplitView {
id: connectedDappComboBox id: connectedDappComboBox
anchors.top: parent.top anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
model: emptyModelCheckbox.checked ? emptyModel : dappsModel model: emptyModelCheckbox.checked ? emptyModel : smallModelCheckbox.checked ? smallModel: dappsModel
popup.visible: true popup.visible: true
} }
@ -25,6 +25,15 @@ SplitView {
id: emptyModel id: emptyModel
} }
ListModel {
id: smallModel
ListElement {
name: "SMALL Model"
url: "https://dapp.test/1"
iconUrl: "https://se-sdk-dapp.vercel.app/assets/eip155:1.png"
}
}
ListModel { ListModel {
id: dappsModel id: dappsModel
ListElement { ListElement {
@ -96,6 +105,12 @@ SplitView {
text: "Empty model" text: "Empty model"
checked: false checked: false
} }
CheckBox {
id: smallModelCheckbox
text: "Small model"
checked: false
}
} }
} }
} }

View File

@ -182,16 +182,26 @@ Item {
} }
readonly property ListModel nonWatchAccounts: ListModel { readonly property ListModel nonWatchAccounts: ListModel {
ListElement {address: "0x1"} ListElement {
address: "0x1"
keycardAccount: false
}
ListElement { ListElement {
address: "0x2" address: "0x2"
name: "helloworld" name: "helloworld"
emoji: "😋" emoji: "😋"
color: "#2A4AF5" color: "#2A4AF5"
keycardAccount: false
}
ListElement {
address: "0x3a"
keycardAccount: false
} }
ListElement { address: "0x3a" }
// Account from GroupedAccountsAssetsModel // Account from GroupedAccountsAssetsModel
ListElement { address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" } ListElement {
address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240"
keycardAccount: false
}
} }
function getNetworkShortNames(chainIds) { function getNetworkShortNames(chainIds) {
return "eth:oeth:arb" return "eth:oeth:arb"
@ -605,7 +615,7 @@ Item {
const address = ModelUtils.get(provider.supportedAccountsModel, 0, "address") const address = ModelUtils.get(provider.supportedAccountsModel, 0, "address")
let session = JSON.parse(Testing.formatApproveSessionResponse([1, 2], [address], {dappMetadataJsonString: Testing.noIconsDappMetadataJsonString})) let session = JSON.parse(Testing.formatApproveSessionResponse([1, 2], [address], {dappMetadataJsonString: Testing.noIconsDappMetadataJsonString}))
callback({"b536a": session, "b537b": session}) callback({"b536a": session, "b537b": session})
compare(provider.dappsModel.count, 1, "expected dappsModel have the SDK's reported dapps") compare(provider.dappsModel.count, 1, "expected dappsModel have the SDK's reported dapp, 2 sessions of the same dApp per 2 wallet account, meaning 1 dApp model entry")
compare(provider.dappsModel.get(0).iconUrl, "", "expected iconUrl to be missing") compare(provider.dappsModel.get(0).iconUrl, "", "expected iconUrl to be missing")
let updateCalls = provider.store.updateWalletConnectSessionsCalls let updateCalls = provider.store.updateWalletConnectSessionsCalls
compare(updateCalls.length, 1, "expected a call to store.updateWalletConnectSessions") compare(updateCalls.length, 1, "expected a call to store.updateWalletConnectSessions")
@ -693,26 +703,27 @@ Item {
compare(eip155.events.length, 2) compare(eip155.events.length, 2)
} }
function test_filterActiveSessionsForKnownAccounts() { function test_getAccountsInSession() {
const account1 = accountsModel.get(0) const account1 = accountsModel.get(0)
const account2 = accountsModel.get(1) const account2 = accountsModel.get(1)
const chainIds = [chainsModel.get(0).chainId, chainsModel.get(1).chainId] const chainIds = [chainsModel.get(0).chainId, chainsModel.get(1).chainId]
const knownSession = JSON.parse(Testing.formatApproveSessionResponse(chainIds, [account2.address]))
// Allow the unlikely unknown accounts to cover for the deleted accounts case const oneAccountSession = JSON.parse(Testing.formatApproveSessionResponse(chainIds, [account2.address]))
const unknownSessionWithKnownAccount = JSON.parse(Testing.formatApproveSessionResponse(chainIds, ['0x03acc', account1.address])) const twoAccountsSession = JSON.parse(Testing.formatApproveSessionResponse(chainIds, ['0x03acc', account1.address]))
const unknownSession1 = JSON.parse(Testing.formatApproveSessionResponse(chainIds, ['0x83acc'])) const duplicateAccountsSession = JSON.parse(Testing.formatApproveSessionResponse(chainIds, ['0x83acb', '0x83acb']))
const unknownSession2 = JSON.parse(Testing.formatApproveSessionResponse(chainIds, ['0x12acc']))
let testSessions = { const res = DAppsHelpers.getAccountsInSession(oneAccountSession)
"b536a": knownSession, compare(res.length, 1, "expected the only account to be returned")
"b537b": unknownSession1, compare(res[0], account2.address, "expected the only account to be the one in the session")
"b538c": unknownSession2,
"b539d": unknownSessionWithKnownAccount const res2 = DAppsHelpers.getAccountsInSession(twoAccountsSession)
} compare(res2.length, 2, "expected the two accounts to be returned")
const res = DAppsHelpers.filterActiveSessionsForKnownAccounts(testSessions, accountsModel) compare(res2[0], '0x03acc', "expected the first account to be the one in the session")
compare(Object.keys(res).length, 2, "expected two sessions to be returned") compare(res2[1], account1.address, "expected the second account to be the one in the session")
// Also test that order is stable
compare(res["b536a"], knownSession, "expected the known session to be returned") const res3 = DAppsHelpers.getAccountsInSession(duplicateAccountsSession)
compare(res["b539d"], unknownSessionWithKnownAccount, "expected the known session to be returned") compare(res3.length, 1, "expected the only account to be returned")
compare(res3[0], '0x83acb', "expected the duplicated account")
} }
} }

View File

@ -133,9 +133,12 @@ Item {
spacing: 8 spacing: 8
visible: !root.walletStore.showSavedAddresses && Global.featureFlags.dappsEnabled visible: !root.walletStore.showSavedAddresses
&& Global.featureFlags.dappsEnabled
&& Global.walletConnectService.isServiceAvailableForAddressSelection
enabled: !!Global.walletConnectService enabled: !!Global.walletConnectService
wcService: Global.walletConnectService wcService: Global.walletConnectService
loginType: root.store.loginType loginType: root.store.loginType
selectedAccountAddress: root.walletStore.selectedAddress selectedAccountAddress: root.walletStore.selectedAddress

View File

@ -1,7 +1,10 @@
import QtQuick 2.15 import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps 1.0
import shared.stores 1.0 import shared.stores 1.0
@ -15,7 +18,24 @@ QObject {
required property DAppsStore store required property DAppsStore store
required property var supportedAccountsModel required property var supportedAccountsModel
readonly property alias dappsModel: d.dappsModel property string selectedAddress: ""
readonly property SortFilterProxyModel dappsModel: SortFilterProxyModel {
objectName: "DAppsModelFiltered"
sourceModel: d.dappsModel
filters: FastExpressionFilter {
enabled: !!root.selectedAddress
function isAddressIncluded(accountAddressesSubModel, selectedAddress) {
const addresses = ModelUtils.modelToFlatArray(accountAddressesSubModel, "address")
return addresses.includes(root.selectedAddress)
}
expression: isAddressIncluded(model.accountAddresses, root.selectedAddress)
expectedRoles: "accountAddresses"
}
}
function updateDapps() { function updateDapps() {
d.updateDappsModel() d.updateDappsModel()
@ -26,6 +46,7 @@ QObject {
property ListModel dappsModel: ListModel { property ListModel dappsModel: ListModel {
id: dapps id: dapps
objectName: "DAppsModel"
} }
property var dappsListReceivedFn: null property var dappsListReceivedFn: null
@ -33,10 +54,24 @@ QObject {
function updateDappsModel() function updateDappsModel()
{ {
dappsListReceivedFn = (dappsJson) => { dappsListReceivedFn = (dappsJson) => {
root.store.dappsListReceived.disconnect(dappsListReceivedFn);
dapps.clear(); dapps.clear();
let dappsList = JSON.parse(dappsJson); let dappsList = JSON.parse(dappsJson);
for (let i = 0; i < dappsList.length; i++) { for (let i = 0; i < dappsList.length; i++) {
const cachedEntry = dappsList[i];
let accountAddresses = cachedEntry.accountAddresses
if (!accountAddresses) {
accountAddresses = [{address: ''}];
}
const dappEntryWithRequiredRoles = {
description: cachedEntry.description,
url: cachedEntry.url,
name: cachedEntry.name,
iconUrl: cachedEntry.url,
accountAddresses: cachedEntry.accountAddresses
}
dapps.append(dappsList[i]); dapps.append(dappsList[i]);
} }
} }
@ -49,27 +84,45 @@ QObject {
} }
getActiveSessionsFn = () => { getActiveSessionsFn = () => {
sdk.getActiveSessions((allSessions) => { sdk.getActiveSessions((allSessionsAllProfiles) => {
root.store.dappsListReceived.disconnect(dappsListReceivedFn); root.store.dappsListReceived.disconnect(dappsListReceivedFn);
let tmpMap = {} const dAppsMap = {}
var topics = [] const topics = []
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessions, root.supportedAccountsModel) const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, supportedAccountsModel)
for (const key in sessions) { for (const sessionID in sessions) {
const dapp = sessions[key].peer.metadata const session = sessions[sessionID]
const dapp = session.peer.metadata
if (!!dapp.icons && dapp.icons.length > 0) { if (!!dapp.icons && dapp.icons.length > 0) {
dapp.iconUrl = dapp.icons[0] dapp.iconUrl = dapp.icons[0]
} else { } else {
dapp.iconUrl = "" dapp.iconUrl = ""
} }
tmpMap[dapp.url] = dapp; const accounts = DAppsHelpers.getAccountsInSession(session)
topics.push(key) const existingDApp = dAppsMap[dapp.url]
if (existingDApp) {
// In Qt5.15.2 this is the way to make a "union" of two arrays
// more modern syntax (ES-6) is not available yet
const combinedAddresses = new Set(existingDApp.accountAddresses.concat(dapp.accountAddresses));
existingDApp.accountAddresses = Array.from(combinedAddresses);
} else {
dapp.accountAddresses = accounts
dAppsMap[dapp.url] = dapp
} }
topics.push(sessionID)
}
// TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset // TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset
dapps.clear(); dapps.clear();
// Iterate tmpMap and fill dapps
for (const key in tmpMap) { // Iterate dAppsMap and fill dapps
dapps.append(tmpMap[key]); for (const topic in dAppsMap) {
const dAppEntry = dAppsMap[topic];
// Due to ListModel converting flat array to empty nested ListModel
// having array of key value pair fixes the problem
dAppEntry.accountAddresses = dAppEntry.accountAddresses.filter(account => (!!account)).map(account => ({address: account}));
dapps.append(dAppEntry);
} }
root.store.updateWalletConnectSessions(JSON.stringify(topics)) root.store.updateWalletConnectSessions(JSON.stringify(topics))

View File

@ -35,6 +35,8 @@ QObject {
readonly property alias dappsModel: dappsProvider.dappsModel readonly property alias dappsModel: dappsProvider.dappsModel
readonly property alias requestHandler: requestHandler readonly property alias requestHandler: requestHandler
readonly property bool isServiceAvailableForAddressSelection: dappsProvider.supportedAccountsModel.ModelCount.count
readonly property var validAccounts: SortFilterProxyModel { readonly property var validAccounts: SortFilterProxyModel {
sourceModel: d.supportedAccountsModel sourceModel: d.supportedAccountsModel
proxyRoles: [ proxyRoles: [
@ -109,16 +111,21 @@ QObject {
} }
function disconnectDapp(url) { function disconnectDapp(url) {
wcSDK.getActiveSessions((allSessions) => { wcSDK.getActiveSessions((allSessionsAllProfiles) => {
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessions, d.supportedAccountsModel) const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, validAccounts)
for (const sessionID in sessions) { for (const sessionID in sessions) {
const session = sessions[sessionID] const session = sessions[sessionID]
const accountsInSession = DAppsHelpers.getAccountsInSession(session)
const dapp = session.peer.metadata const dapp = session.peer.metadata
const topic = session.topic const topic = session.topic
if (dapp.url === url) { if (dapp.url === url) {
if (!dappsProvider.selectedAddress ||
(accountsInSession.includes(dappsProvider.selectedAddress)))
{
wcSDK.disconnectSession(topic) wcSDK.disconnectSession(topic)
} }
} }
}
}); });
} }
@ -226,7 +233,13 @@ QObject {
QObject { QObject {
id: d id: d
readonly property var supportedAccountsModel: root.walletRootStore.nonWatchAccounts readonly property var supportedAccountsModel: SortFilterProxyModel {
sourceModel: root.walletRootStore.nonWatchAccounts
filters: ValueFilter {
roleName: "keycardAccount"
value: false
}
}
property var currentSessionProposal: null property var currentSessionProposal: null
property var acceptedSessionProposal: null property var acceptedSessionProposal: null
@ -265,7 +278,19 @@ QObject {
sdk: root.wcSDK sdk: root.wcSDK
store: root.store store: root.store
supportedAccountsModel: d.supportedAccountsModel supportedAccountsModel: SortFilterProxyModel {
objectName: "SelectedAddressModelForDAppsListProvider"
sourceModel: d.supportedAccountsModel
filters: FastExpressionFilter {
enabled: !root.walletRootStore.showAllAccounts
expression: root.walletRootStore.selectedAddress.toLowerCase() === model.address.toLowerCase()
expectedRoles: ["address"]
}
}
selectedAddress: root.walletRootStore.selectedAddress
} }
// Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal // Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal

View File

@ -121,3 +121,12 @@ function filterActiveSessionsForKnownAccounts(sessions, accountsModel) {
}) })
return knownSessions return knownSessions
} }
function getAccountsInSession(session) {
const eip155Addresses = session.namespaces.eip155.accounts
const accountSet = new Set(
eip155Addresses.map(eip155Address => eip155Address.split(':').pop().trim())
);
const uniqueAddresses = Array.from(accountSet);
return uniqueAddresses
}

View File

@ -1,4 +1,5 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQml 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQml.Models 2.15 import QtQml.Models 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
@ -46,9 +47,25 @@ Popup {
} }
} }
// workaround for https://bugreports.qt.io/browse/QTBUG-87804
Binding on margins {
id: workaroundBinding
when: false
restoreMode: Binding.RestoreBindingOrValue
}
onImplicitContentHeightChanged: {
workaroundBinding.value = root.margins + 1
workaroundBinding.when = true
workaroundBinding.when = false
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
id: mainLayout id: mainLayout
spacing: 0 spacing: 0
ShapeRectangle { ShapeRectangle {
id: listPlaceholder id: listPlaceholder