feat(dapps) add DAppsService component and ConnectDAppModal

Implement the initial Pairing user workflow and disconnect option for
the first session.

Also

- rename pairing modal accordingly (`PairWCModal.qml`) to make room for the proper
`ConnectDAppModal.qml`
- basic tests for service helpers
- update storybook to reflect the new user workflows

Closes #14607
This commit is contained in:
Stefan 2024-05-06 22:22:43 +02:00 committed by Stefan Dunca
parent 4771f0d77f
commit ee72ec7aee
25 changed files with 2812 additions and 103 deletions

View File

@ -56,8 +56,8 @@ SplitView {
store: ProfileSectionStore {
property WalletStore walletStore: WalletStore {
accountSensitiveSettings: mockData.accountSettings
dappList: dappsModel
property var accountSensitiveSettings: mockData.accountSettings
property var dappList: dappsModel
function disconnect(dappName) {
for (let i = 0; i < dappsModel.count; i++) {

View File

@ -0,0 +1,126 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import Qt.labs.settings 1.0
import QtTest 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import Models 1.0
import Storybook 1.0
import shared.popups.walletconnect 1.0
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.panels 1.0
import utils 1.0
import shared.stores 1.0
Item {
id: root
function openModal() {
modal.openWithFilter([1, 42161], JSON.parse(`{
"metadata": {
"description": "React App for WalletConnect",
"icons": [
"https://avatars.githubusercontent.com/u/37784886"
],
"name": "React App",
"url": "https://react-app.walletconnect.com",
"verifyUrl": "https://verify.walletconnect.com"
},
"publicKey": "300a6a1df4cb0cd73eb652f11845f35a318541eb18ab369860be85c0c2ada54a"
}`))
if (pairedCheckbox.checked) {
pairedResultTimer.restart()
}
}
// qml Splitter
SplitView {
anchors.fill: parent
ColumnLayout {
SplitView.fillWidth: true
Component.onCompleted: root.openModal()
StatusButton {
id: openButton
Layout.alignment: Qt.AlignHCenter
Layout.margins: 20
text: "Open ConnectDAppModal"
onClicked: root.openModal()
}
ConnectDAppModal {
id: modal
anchors.centerIn: parent
spacing: 8
accounts: WalletAccountsModel{
}
flatNetworks: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: false; }
}
}
ColumnLayout {}
}
ColumnLayout {
id: optionsSpace
CheckBox {
id: pairedCheckbox
text: "Report Paired"
checked: true
}
CheckBox {
id: pairedStatusCheckbox
text: "Paired Successful"
checked: true
}
Item { Layout.fillHeight: true }
}
}
Timer {
id: pairedResultTimer
interval: 1000
running: false
repeat: false
onTriggered: {
if (pairedCheckbox.checked) {
if (pairedStatusCheckbox.checked) {
modal.pairSuccessful(null)
} else {
modal.pairFailed(null, "Pairing failed")
}
}
}
}
}
// category: Wallet

View File

@ -11,6 +11,7 @@ import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import Models 1.0
import Storybook 1.0
@ -21,6 +22,7 @@ import AppLayouts.Wallet.services.dapps 1.0
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Profile.stores 1.0
import utils 1.0
import shared.stores 1.0
@ -49,6 +51,8 @@ Item {
anchors.centerIn: parent
spacing: 8
wcService: walletConnectService
}
}
ColumnLayout {}
@ -62,11 +66,20 @@ Item {
Text {
id: projectIdText
readonly property string projectId: SystemUtils.getEnvVar("WALLET_CONNECT_PROJECT_ID")
text: projectId.substring(0, 3) + "..." + projectId.substring(projectId.length - 3)
text: SQUtils.Utils.elideText(projectId, 3)
font.bold: true
}
}
CheckBox {
text: "Testnet Mode"
checked: settings.testNetworks
onCheckedChanged: {
settings.testNetworks = checked
}
}
// spacer
ColumnLayout {}
@ -87,7 +100,7 @@ Item {
CheckBox {
id: openPairCheckBox
text: "Open Pair"
checked: settings.openPair
onCheckedChanged: {
@ -96,10 +109,11 @@ Item {
d.startPairing()
}
}
Connections {
target: dappsWorkflow
// Open Pairing workflow if selected in the side bar
// If Open Pair workflow if selected in the side bar
function onDAppsListReady() {
if (!d.startPairingWorkflowActive)
return
@ -113,7 +127,7 @@ Item {
}
}
function onConnectDappReady() {
function onPairWCReady() {
if (!d.startPairingWorkflowActive)
return
@ -121,25 +135,52 @@ Item {
let items = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "StatusBaseInput")
if (items.length === 1) {
items[0].text = pairUriInput.text
clickDoneIfSDKReady()
}
}
d.startPairingWorkflowActive = false
}
function clickDoneIfSDKReady() {
if (!d.startPairingWorkflowActive) {
return
}
let modals = InspectionUtils.findVisualsByTypeName(dappsWorkflow, "PairWCModal")
if (modals.length === 1) {
let buttons = InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton")
if (buttons.length === 1 && walletConnectService.wcSDK.sdkReady) {
d.startPairingWorkflowActive = false
buttons[0].clicked()
return
}
}
Backpressure.debounce(dappsWorkflow, 250, clickDoneIfSDKReady)()
}
}
}
}
}
DAppsStore {
wCSDK: WalletConnectSDK {
WalletConnectService {
id: walletConnectService
wcSDK: WalletConnectSDK {
active: true
projectId: projectIdText.projectId
}
onSessionRequestEvent: (details) => {
// TODO #14556
console.debug(`@dd onSessionRequestEvent: ${JSON.stringify(details)}`)
dappsStore: DAppsStore {
}
walletStore: WalletStore {
property var flatNetworks: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; }
}
property var accounts: WalletAccountsModel{}
}
}
@ -150,7 +191,7 @@ Item {
property bool startPairingWorkflowActive: false
function startPairing() {
startPairingWorkflowActive = true
d.startPairingWorkflowActive = true
if(root.visible) {
dappsWorkflow.clicked()
}
@ -168,6 +209,7 @@ Item {
property bool openPair: false
property string pairUri: ""
property bool testNetworks: false
}
}

View File

@ -98,6 +98,10 @@ SplitView {
}
return prefChains
}
function addressWasShown(account) {
return true
}
}
}
}

View File

@ -7,44 +7,128 @@ import QtQuick.Controls 2.15
import Storybook 1.0
import AppLayouts.Wallet.controls 1.0
//import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.services.dapps 1.0
import QtQml.Models 2.15
Item {
id: root
width: 600
height: 400
Component {
id: componentUnderTest
DAppsWorkflow {
}
}
// TODO: mock WalletConnectSDK
// Component {
// id: componentUnderTest
// DAppsWorkflow {
// }
// }
// TestCase {
// name: "DAppsWorkflow"
// when: windowShown
// property DAppsWorkflow controlUnderTest: null
// function init() {
// controlUnderTest = createTemporaryObject(componentUnderTest, root)
// }
// function test_ClickToOpenAndClosePopup() {
// verify(!!controlUnderTest)
// waitForRendering(controlUnderTest)
// mouseClick(controlUnderTest, Qt.LeftButton)
// waitForRendering(controlUnderTest)
// let popup = findChild(controlUnderTest, "dappsPopup")
// verify(!!popup)
// verify(popup.opened)
// mouseClick(Overlay.overlay, Qt.LeftButton)
// waitForRendering(controlUnderTest)
// verify(!popup.opened)
// }
// }
TestCase {
name: "DAppsWorkflow"
when: windowShown
name: "ServiceHelpers"
property DAppsWorkflow controlUnderTest: null
function init() {
controlUnderTest = createTemporaryObject(componentUnderTest, root)
function test_extractChainsAndAccountsFromApprovedNamespaces() {
let res = Helpers.extractChainsAndAccountsFromApprovedNamespaces(JSON.parse(`{
"eip155": {
"accounts": [
"eip155:1:0x1",
"eip155:1:0x2",
"eip155:2:0x1",
"eip155:2:0x2"
],
"chains": [
"eip155:1",
"eip155:2"
],
"events": [
"accountsChanged",
"chainChanged"
],
"methods": [
"eth_sendTransaction",
"personal_sign"
]
}
}`))
verify(res.chains.length === 2)
verify(res.accounts.length === 2)
verify(res.chains[0] === 1)
verify(res.chains[1] === 2)
verify(res.accounts[0] === "0x1")
verify(res.accounts[1] === "0x2")
}
function test_ClickToOpenAndClosePopup() {
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
readonly property ListModel chainsModel: ListModel {
ListElement { chainId: 1 }
ListElement { chainId: 2 }
}
mouseClick(controlUnderTest, Qt.LeftButton)
waitForRendering(controlUnderTest)
readonly property ListModel accountsModel: ListModel {
ListElement { address: "0x1" }
ListElement { address: "0x2" }
}
let popup = findChild(controlUnderTest, "dappsPopup")
verify(!!popup)
verify(popup.opened)
function test_buildSupportedNamespacesFromModels() {
let resStr = Helpers.buildSupportedNamespacesFromModels(chainsModel, accountsModel)
let jsonObj = JSON.parse(resStr)
verify(jsonObj.hasOwnProperty("eip155"))
let eip155 = jsonObj.eip155
mouseClick(Overlay.overlay, Qt.LeftButton)
waitForRendering(controlUnderTest)
verify(eip155.hasOwnProperty("chains"))
let chains = eip155.chains
verify(chains.length === 2)
verify(chains[0] === "eip155:1")
verify(chains[1] === "eip155:2")
verify(!popup.opened)
verify(eip155.hasOwnProperty("accounts"))
let accounts = eip155.accounts
verify(accounts.length === 4)
for (let chainI = 0; chainI < chainsModel.count; chainI++) {
for (let accountI = 0; accountI < chainsModel.count; accountI++) {
var found = false
for (let entry of accounts) {
if(entry === `eip155:${chainsModel.get(chainI).chainId}:${accountsModel.get(accountI).address}`) {
found = true
break
}
}
verify(found, `found ${accountsModel.get(accountI).address} for chain ${chainsModel.get(chainI).chainId}`)
}
}
verify(eip155.hasOwnProperty("methods"))
verify(eip155.methods.length > 0)
verify(eip155.hasOwnProperty("events"))
verify(eip155.events.length > 0)
}
}
}

View File

@ -76,12 +76,12 @@ QtObject {
isEnabled: true,
},
{
chainId: 5,
chainName: "Goerli",
blockExplorerUrl: "https://goerli.etherscan.io/",
iconUrl: "network/Network=Testnet",
chainColor: "#939BA1",
shortName: "goEth",
chainId: 11155111,
chainName: "Sepolia Mainnet",
blockExplorerUrl: "https://sepolia.etherscan.io/",
iconUrl: "network/Network=Ethereum",
chainColor: "#627EEA",
shortName: "eth",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,

View File

@ -1,6 +1,4 @@
import QtQuick 2.14
QtObject {
property var accountSensitiveSettings: ({})
property var dappList: []
}

View File

@ -1,9 +1,3 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
QtObject {
id: root
required property WalletConnectSDK wCSDK
}
QtObject {}

View File

@ -5,12 +5,17 @@ import QtQuick.Layouts 1.15
import AppLayouts.Wallet.controls 1.0
import shared.popups.walletconnect 1.0
import AppLayouts.Wallet.services.dapps 1.0
import shared.stores 1.0
ConnectedDappsButton {
id: root
required property WalletConnectService wcService
signal dAppsListReady()
signal connectDappReady()
signal pairWCReady()
onClicked: {
dappsListLoader.active = true
@ -19,23 +24,23 @@ ConnectedDappsButton {
highlighted: dappsListLoader.active
Loader {
id: connectDappLoader
id: pairWCLoader
active: false
onLoaded: {
item.open()
root.connectDappReady()
root.pairWCReady()
}
sourceComponent: ConnectDappModal {
sourceComponent: PairWCModal {
visible: true
onClosed: connectDappLoader.active = false
onClosed: pairWCLoader.active = false
onPair: (uri) => {
this.close()
console.debug(`TODO(#14556): ConnectionRequestDappModal with ${uri}`)
root.wcService.pair(uri)
this.isPairing = true
}
}
}
@ -53,8 +58,8 @@ ConnectedDappsButton {
sourceComponent: DAppsListPopup {
visible: true
onConnectDapp: {
connectDappLoader.active = true
onPairWCDapp: {
pairWCLoader.active = true
this.close()
}
onOpened: {
@ -64,4 +69,72 @@ ConnectedDappsButton {
onClosed: dappsListLoader.active = false
}
}
Loader {
id: connectDappLoader
active: false
onLoaded: item.openWithFilter(dappChains, sessionProposal.params.proposer)
property var dappChains: []
property var sessionProposal: null
property var availableNamespaces: null
property var sessionTopic: null
sourceComponent: ConnectDAppModal {
visible: true
onClosed: connectDappLoader.active = false
accounts: wcService.validAccounts
flatNetworks: wcService.flatNetworks
onConnect: {
root.wcService.approvePairSession(sessionProposal, dappChains, selectedAccount)
}
onDecline: {
connectDappLoader.active = false
root.wcService.rejectPairSession(sessionProposal.id)
}
onDisconnect: {
connectDappLoader.active = false
root.wcService.disconnectDapp(sessionTopic)
}
}
}
Connections {
target: root.wcService
function onConnectDApp(dappChains, sessionProposal, availableNamespaces) {
connectDappLoader.dappChains = dappChains
connectDappLoader.sessionProposal = sessionProposal
connectDappLoader.availableNamespaces = availableNamespaces
connectDappLoader.sessionTopic = null
if (pairWCLoader.item) {
pairWCLoader.item.close()
}
connectDappLoader.active = true
}
function onApproveSessionResult(session, err) {
connectDappLoader.dappChains = []
connectDappLoader.sessionProposal = null
connectDappLoader.availableNamespaces = null
connectDappLoader.sessionTopic = session.topic
let modal = connectDappLoader.item
if (!!modal) {
if (err) {
modal.pairFailed(session, err)
} else {
modal.pairSuccessful(session)
}
}
}
}
}

View File

@ -80,6 +80,9 @@ Item {
spacing: 8
visible: !root.walletStore.showSavedAddresses && Global.featureFlags.dappsEnabled
enabled: !!Global.walletConnectService
wcService: Global.walletConnectService
}
StatusButton {

View File

@ -27,9 +27,11 @@ Item {
signal statusChanged(string message)
signal sdkInit(bool success, var result)
signal pairResponse(bool success)
signal sessionProposal(var sessionProposal)
signal sessionProposalExpired()
signal approveSessionResult(var session, string error)
signal buildApprovedNamespacesResult(var session, string error)
signal approveSessionResult(var approvedNamespaces, string error)
signal rejectSessionResult(string error)
signal sessionRequestEvent(var sessionRequest)
signal sessionRequestUserAnswerResult(bool accept, string error)
@ -40,6 +42,8 @@ Item {
signal sessionDelete(var topic, string error)
/// Generates \c pairResponse signal and expects to receive
/// a \c sessionProposal signal with the sessionProposal object
function pair(pairLink) {
wcCalls.pair(pairLink)
}
@ -64,6 +68,10 @@ Item {
wcCalls.ping(topic)
}
function buildApprovedNamespaces(params, supportedNamespaces) {
wcCalls.buildApprovedNamespaces(params, supportedNamespaces)
}
function approveSession(sessionProposal, supportedNamespaces) {
wcCalls.approveSession(sessionProposal, supportedNamespaces)
}
@ -227,6 +235,21 @@ Item {
)
}
function buildApprovedNamespaces(params, supportedNamespaces) {
console.debug(`WC WalletConnectSDK.wcCall.buildApprovedNamespaces; params: ${JSON.stringify(params)}, supportedNamespaces: ${JSON.stringify(supportedNamespaces)}`)
d.engine.runJavaScript(`
wc.buildApprovedNamespaces(${JSON.stringify(params)}, ${JSON.stringify(supportedNamespaces)})
.then((approvedNamespaces) => {
wc.statusObject.onBuildApprovedNamespacesResponse(approvedNamespaces, "")
})
.catch((e) => {
wc.statusObject.onBuildApprovedNamespacesResponse("", e.message)
})
`
)
}
function approveSession(sessionProposal, supportedNamespaces) {
console.debug(`WC WalletConnectSDK.wcCall.approveSession; sessionProposal: ${JSON.stringify(sessionProposal)}, supportedNamespaces: ${JSON.stringify(supportedNamespaces)}`)
@ -413,6 +436,7 @@ Item {
function onPairResponse(error) {
console.debug(`WC WalletConnectSDK.onPairResponse; error: ${error}`)
root.pairResponse(error == "")
}
function onPingResponse(error) {
@ -430,6 +454,11 @@ Item {
d.resetPairingsModel()
}
function onBuildApprovedNamespacesResponse(approvedNamespaces, error) {
console.debug(`WC WalletConnectSDK.onBuildApprovedNamespacesResponse; approvedNamespaces: ${approvedNamespaces ? JSON.stringify(approvedNamespaces, null, 2) : "-"}, error: ${error}`)
root.buildApprovedNamespacesResult(approvedNamespaces, error)
}
function onApproveSessionResponse(session, error) {
console.debug(`WC WalletConnectSDK.onApproveSessionResponse; sessionTopic: ${JSON.stringify(session, null, 2)}, error: ${error}`)
d.resetPairingsModel()

View File

@ -0,0 +1,102 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Profile.stores 1.0
import shared.stores 1.0
import shared.popups.walletconnect 1.0
import SortFilterProxyModel 0.2
import utils 1.0
QtObject {
id: root
required property WalletConnectSDK wcSDK
required property DAppsStore dappsStore
required property WalletStore walletStore
readonly property var validAccounts: SortFilterProxyModel {
sourceModel: walletStore.accounts
filters: ValueFilter {
roleName: "walletType"
value: Constants.watchWalletType
inverted: true
}
}
readonly property var flatNetworks: walletStore.flatNetworks
function pair(uri) {
_d.acceptedSessionProposal = null
wcSDK.pair(uri)
}
function approvePairSession(sessionProposal, approvedChainIds, approvedAccount) {
_d.acceptedSessionProposal = sessionProposal
let approvedNamespaces = JSON.parse(Helpers.buildSupportedNamespaces(approvedChainIds, [approvedAccount.address]))
wcSDK.buildApprovedNamespaces(sessionProposal.params, approvedNamespaces)
}
function rejectPairSession(id) {
wcSDK.rejectSession(id)
}
function disconnectDapp(sessionTopic) {
wcSDK.disconnectSession(sessionTopic)
}
signal connectDApp(var dappChains, var sessionProposal, var approvedNamespaces)
signal approveSessionResult(var session, var error)
readonly property Connections sdkConnections: Connections {
target: wcSDK
function onSessionProposal(sessionProposal) {
_d.currentSessionProposal = sessionProposal
let supportedNamespacesStr = Helpers.buildSupportedNamespacesFromModels(root.flatNetworks, root.validAccounts)
wcSDK.buildApprovedNamespaces(sessionProposal.params, JSON.parse(supportedNamespacesStr))
}
function onBuildApprovedNamespacesResult(approvedNamespaces, error) {
if(error) {
// TODO: error reporting
return
}
if (_d.acceptedSessionProposal) {
wcSDK.approveSession(_d.acceptedSessionProposal, approvedNamespaces)
} else {
let res = Helpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
root.connectDApp(res.chains, _d.currentSessionProposal, approvedNamespaces)
}
}
function onApproveSessionResult(session, err) {
root.approveSessionResult(session, err)
}
function onRejectSessionResult(err) {
let app_url = _d.currentSessionProposal ? _d.currentSessionProposal.params.proposer.url : "-"
if(err) {
console.debug(`TODO #14556: show a notification "Failed to reject connection request for ${app_url}"`)
} else {
console.debug(`TODO #14556: show a notification "Connection request for ${app_url} was rejected"`)
}
}
function onSessionDelete(topic, error) {
let app_url = _d.currentSessionProposal ? _d.currentSessionProposal.params.proposer.url : "-"
if(error) {
console.debug(`TODO #14556: show a notification "Failed to disconnect from ${app_url}"`)
} else {
console.debug(`TODO #14556: show a notification "Disconnected from ${app_url}"`)
}
}
}
readonly property QtObject _d: QtObject {
property var currentSessionProposal: null
property var acceptedSessionProposal: null
}
}

View File

@ -0,0 +1,39 @@
.import StatusQ.Core.Utils 0.1 as SQUtils
function extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) {
const eip155Data = approvedNamespaces.eip155;
const chains = eip155Data.chains.map(chain => parseInt(chain.split(':').pop().trim(), 10));
const accountSet = new Set(
eip155Data.accounts.map(account => account.split(':').pop().trim())
);
const uniqueAccounts = Array.from(accountSet);
return { chains, accounts: uniqueAccounts };
}
function buildSupportedNamespacesFromModels(chainsModel, accountsModel) {
var chainIds = []
var addresses = []
for (let i = 0; i < chainsModel.count; i++) {
let entry = SQUtils.ModelUtils.get(chainsModel, i)
chainIds.push(parseInt(entry.chainId))
}
for (let i = 0; i < accountsModel.count; i++) {
let entry = SQUtils.ModelUtils.get(accountsModel, i)
addresses.push(entry.address)
}
return buildSupportedNamespaces(chainIds, addresses)
}
function buildSupportedNamespaces(chainIds, addresses) {
var eipChainIds = []
var eipAddresses = []
for (let i = 0; i < chainIds.length; i++) {
let chainId = chainIds[i]
eipChainIds.push(`"eip155:${chainId}"`)
for (let i = 0; i < addresses.length; i++) {
eipAddresses.push(`"eip155:${chainId}:${addresses[i]}"`)
}
}
return `{
"eip155":{"chains": [${eipChainIds.join(',')}],"methods": ["eth_sendTransaction", "personal_sign"],"events": ["accountsChanged", "chainChanged"],"accounts": [${eipAddresses.join(',')}]}}`
}

View File

@ -1 +1,4 @@
WalletConnectSDK 1.0 WalletConnectSDK.qml
WalletConnectSDK 1.0 WalletConnectSDK.qml
WalletConnectService 1.0 WalletConnectService.qml
Helpers 1.0 helpers.js

View File

@ -17,7 +17,7 @@ Install dependencies steps by executing commands in this directory:
- or to update to the latest run `ncu -u; npm install` in here
- run `npm install -g npm-check-updates` for `ncu` command
- these commands will also create or update a `package-lock.json` file and populate the `node_modules` directory
- update the [`bundle.js`](./dist/main.js) file by running `npm run build`
- update the [`bundle.js`](./generated/bundle.js) file by running `npm run build`
- the result will be embedded with the app and loaded by [`WalletConnectSDK.qml`](../WalletConnectSDK.qml) component
- add the newly generated files to index `git add --update .` to include in the commit

File diff suppressed because one or more lines are too long

View File

@ -129,18 +129,18 @@ window.wc = {
await window.wc.web3wallet.engine.signClient.ping({ topic });
},
approveSession: async function (sessionProposal, supportedNamespaces) {
buildApprovedNamespaces: async function (params, supportedNamespaces) {
return buildApprovedNamespaces({
proposal: params,
supportedNamespaces: supportedNamespaces,
});
},
approveSession: async function (sessionProposal, approvedNamespaces) {
const { id, params } = sessionProposal;
const { relays } = params
const approvedNamespaces = buildApprovedNamespaces(
{
proposal: params,
supportedNamespaces: supportedNamespaces,
}
);
return await window.wc.web3wallet.approveSession(
{
id,

View File

@ -78,18 +78,6 @@ Item {
walletAssetStore: appMain.walletAssetsStore
tokensStore: appMain.tokensStore
}
readonly property DAppsStore dappsStore: DAppsStore {
wCSDK: WalletConnectSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
onSessionRequestEvent: (details) => {
// TODO #14556
console.debug(`@dd onSessionRequestEvent: ${JSON.stringify(details)}`)
}
}
}
// set from main.qml
property var sysPalette
@ -2040,4 +2028,27 @@ Item {
onClosed: userAgreementLoader.active = false
}
}
Loader {
id: walletConnectServiceLoader
active: Global.featureFlags.dappsEnabled
sourceComponent: WalletConnectService {
id: walletConnectService
wcSDK: WalletConnectSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
}
dappsStore: DAppsStore {
}
walletStore: appMain.rootStore.profileSectionStore.walletStore
Component.onCompleted: {
Global.walletConnectService = walletConnectService
}
}
}
}

View File

@ -0,0 +1,380 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.14
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
// TODO extract the components to StatusQ
import shared.popups.send.controls 1.0
import AppLayouts.Wallet.controls 1.0
import utils 1.0
StatusDialog {
id: root
width: 480
implicitHeight: d.connectionStatus === root.notConnectedStatus ? 633 : 681
required property var accounts
required property var flatNetworks
readonly property alias selectedAccount: d.selectedAccount
readonly property int notConnectedStatus: 0
readonly property int connectionSuccessfulStatus: 1
readonly property int connectionFailedStatus: 2
function openWithFilter(dappChains, proposer) {
d.connectionStatus = root.notConnectedStatus
d.afterTwoSecondsFromStatus = false
let m = proposer.metadata
dappCard.name = m.name
dappCard.url = m.url
if(m.icons.length > 0) {
dappCard.icon = m.icons[0]
}
d.dappChains.clear()
for (let i = 0; i < dappChains.length; i++) {
// Convert to int
d.dappChains.append({ chainId: parseInt(dappChains[i]) })
}
root.open()
}
function pairSuccessful(session) {
d.connectionStatus = root.connectionSuccessfulStatus
closeAndRetryTimer.start()
}
function pairFailed(session, err) {
d.connectionStatus = root.connectionFailedStatus
closeAndRetryTimer.start()
}
Timer {
id: closeAndRetryTimer
interval: 2000
running: false
repeat: false
onTriggered: {
d.afterTwoSecondsFromStatus = true
}
}
signal connect()
signal decline()
signal disconnect()
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
title: qsTr("Connection request")
padding: 20
contentItem: ColumnLayout {
spacing: 20
clip: true
DAppCard {
id: dappCard
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: 12
Layout.rightMargin: Layout.leftMargin
Layout.topMargin: 20
Layout.bottomMargin: Layout.topMargin
}
ContextCard {
Layout.fillWidth: true
}
PermissionsCard {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.rightMargin: Layout.leftMargin
Layout.topMargin: 20
Layout.bottomMargin: Layout.topMargin
}
}
footer: StatusDialogFooter {
id: footer
rightButtons: ObjectModel {
StatusButton {
height: 44
text: qsTr("Decline")
visible: d.connectionStatus === root.notConnectedStatus
onClicked: root.decline()
}
StatusButton {
height: 44
text: qsTr("Disconnect")
visible: d.connectionStatus === root.connectionSuccessfulStatus
type: StatusBaseButton.Type.Danger
onClicked: root.disconnect()
}
StatusButton {
height: 44
text: d.connectionStatus === root.notConnectedStatus
? qsTr("Connect")
: qsTr("Close")
onClicked: {
if (d.connectionStatus === root.notConnectedStatus)
root.connect()
else
root.close()
}
}
}
}
component ContextCard: Rectangle {
id: contextCard
implicitWidth: contextLayout.implicitWidth
implicitHeight: contextLayout.implicitHeight
radius: 8
// TODO: the color matched the design color (grey4); It is also matching the intention or we should add some another color to the theme? (e.g. sectionBorder)?
border.color: Theme.palette.baseColor2
border.width: 1
color: "transparent"
ColumnLayout {
id: contextLayout
anchors.fill: parent
RowLayout {
Layout.margins: 16
StatusBaseText {
text: qsTr("Connect with")
Layout.fillWidth: true
}
// TODO: have a reusable component for this
AccountsModalHeader {
id: accountsDropdown
Layout.preferredWidth: 204
control.enabled: d.connectionStatus === root.notConnectedStatus && count > 1
model: d.accountsProxy
onCountChanged: {
if (count > 0) {
selectedAccount = d.accountsProxy.get(0)
}
}
selectedAccount: d.accountsProxy.get(0)
onSelectedAccountChanged: d.selectedAccount = selectedAccount
onSelectedIndexChanged: {
d.selectedAccount = model.get(selectedIndex)
selectedAccount = d.selectedAccount
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: contextCard.border.color
}
RowLayout {
Layout.margins: 16
StatusBaseText {
text: qsTr("On")
Layout.fillWidth: true
}
// TODO: replace with a specialized network selection control
NetworkFilter {
Layout.preferredWidth: accountsDropdown.Layout.preferredWidth
flatNetworks: d.filteredChains
showAllSelectedText: false
showCheckboxes: false
enabled: d.connectionStatus === root.notConnectedStatus
}
}
}
}
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
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 16
width: 72
height: 72
asset.width: width
asset.height: height
asset.color: "transparent"
asset.bgColor: "transparent"
}
StatusBaseText {
id: appNameText
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 4
font.bold: true
font.pixelSize: 17
}
// TODO replace with the proper URL control
StatusLinkText {
id: appUrlText
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 15
}
Rectangle {
Layout.preferredWidth: pairingStatusLayout.implicitWidth + 32
Layout.preferredHeight: pairingStatusLayout.implicitHeight + 14
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 16
visible: d.connectionStatus !== root.notConnectedStatus
color: d.connectionStatus === root.connectionSuccessfulStatus
? d.afterTwoSecondsFromStatus
? Theme.palette.successColor2
: Theme.palette.successColor3
: d.afterTwoSecondsFromStatus
? "transparent"
: Theme.palette.dangerColor3
border.color: d.connectionStatus === root.connectionSuccessfulStatus
? Theme.palette.successColor2
: Theme.palette.dangerColor2
border.width: 1
radius: height / 2
RowLayout {
id: pairingStatusLayout
anchors.centerIn: parent
spacing: 8
Rectangle {
width: 6
height: 6
radius: width / 2
visible: d.connectionStatus === root.connectionSuccessfulStatus
color: Theme.palette.successColor1
}
StatusIcon {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
visible: d.connectionStatus !== root.connectionSuccessfulStatus
color: Theme.palette.dangerColor1
icon: "warning"
}
StatusBaseText {
text: {
if (d.connectionStatus === root.connectionSuccessfulStatus)
return qsTr("Connected. You can now go back to the dApp.")
else if (d.connectionStatus === root.connectionFailedStatus)
return qsTr("Error connecting to dApp. Close and try again")
return ""
}
font.pixelSize: 12
color: d.connectionStatus === root.connectionSuccessfulStatus ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
}
}
}
component PermissionsCard: ColumnLayout {
spacing: 8
StatusBaseText {
text: qsTr("Uniswap Interface will be able to:")
font.pixelSize: 13
color: Theme.palette.baseColor1
}
StatusBaseText {
text: qsTr("Check your account balance and activity")
font.pixelSize: 13
}
StatusBaseText {
text: qsTr("Request transactions and message signing")
font.pixelSize: 13
}
}
QtObject {
id: d
property SortFilterProxyModel accountsProxy: SortFilterProxyModel {
sourceModel: root.accounts
sorters: RoleSorter { roleName: "position"; sortOrder: Qt.AscendingOrder }
}
property var selectedAccount: accountsProxy.count > 0 ? accountsProxy.get(0) : null
readonly property var filteredChains: LeftJoinModel {
leftModel: d.dappChains
rightModel: root.flatNetworks
joinRole: "chainId"
}
readonly property var dappChains: ListModel {}
property int connectionStatus: notConnectedStatus
property bool afterTwoSecondsFromStatus: false
}
}

View File

@ -15,7 +15,7 @@ Popup {
property int menuWidth: 312
signal connectDapp()
signal pairWCDapp()
contentWidth: root.menuWidth
contentHeight: list.height
@ -60,7 +60,7 @@ Popup {
text: qsTr("Connect a dApp via WalletConnect")
onClicked: {
root.connectDapp()
root.pairWCDapp()
}
}
}

View File

@ -9,16 +9,18 @@ import StatusQ.Controls 0.1
import utils 1.0
import "ConnectDappModal"
import "PairWCModal"
StatusDialog {
id: root
signal pair(string uri)
width: 480
implicitHeight: 633
property bool isPairing: false
signal pair(string uri)
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
title: qsTr("Connect a dApp via WalletConnect")
@ -32,10 +34,12 @@ StatusDialog {
WCUriInput {
id: uriInput
onTextChanged: root.isPairing = false
}
// Spacer
ColumnLayout {}
Item { Layout.fillHeight: true }
StatusLinkText {
text: qsTr("How to copy the dApp URI")
@ -58,7 +62,7 @@ StatusDialog {
height: 44
text: qsTr("Done")
enabled: uriInput.valid && uriInput.text.length > 0
enabled: uriInput.valid && !root.isPairing && uriInput.text.length > 0
onClicked: root.pair(uriInput.text)
}

View File

@ -1,2 +1,3 @@
ConnectDappModal 1.0 ConnectDappModal.qml
DAppsListPopup 1.0 DAppsListPopup.qml
PairWCModal 1.0 PairWCModal.qml
DAppsListPopup 1.0 DAppsListPopup.qml
ConnectDAppModal 1.0 ConnectDAppModal.qml

View File

@ -1,11 +1,3 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
QtObject {
id: root
required property WalletConnectSDK wCSDK
// Here we will have business logic calls and expose connections history models
}
QtObject {}

View File

@ -14,6 +14,9 @@ QtObject {
property var userProfile
property bool appIsReady: false
// use the generic var as type to break the cyclic dependency
property var walletConnectService: null
// avoid lookup of context property in QML
readonly property var featureFlags: featureFlagsRootContextProperty