feat(dapp) handle pairing errors or timeout if no response

Found out while testing that in some corner cases there will be no
response of error in case of pairing. This is handled now by showing
a generic error message. The implementation is using a timer to handle
this case.
Extend the logic to report errors in the pairing process.

Closes #14676
This commit is contained in:
Stefan 2024-07-08 14:18:14 +03:00 committed by Stefan Dunca
parent 1e256c8bf1
commit 136194c112
6 changed files with 68 additions and 47 deletions

View File

@ -88,6 +88,7 @@ Item {
} }
} }
StatusBaseText { text: "Custom Accounts" }
StatusTextArea { StatusTextArea {
text: settings.customAccounts text: settings.customAccounts
onTextChanged: { onTextChanged: {
@ -99,7 +100,8 @@ Item {
}) })
} }
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: !!text ? 400 : -1 Layout.maximumHeight: 300
clip: true
} }
Rectangle { Rectangle {

View File

@ -185,9 +185,9 @@ DappsComboBox {
Connections { Connections {
target: root.wcService target: root.wcService
function onPairingUriValidated(validationState) { function onPairingValidated(validationState) {
if (pairWCLoader.item) { if (pairWCLoader.item) {
pairWCLoader.item.pairingUriValidated(validationState) pairWCLoader.item.pairingValidated(validationState)
} }
} }

View File

@ -17,6 +17,15 @@ import utils 1.0
import "types" import "types"
// The WC SDK has an async (function call then signal response)
// A complete pairing flow to connect a dApp:
// - user provides pairing url -> root.validatePairingUri -> signal pairingValidated
// - user requests pair -> root.pair(uri) -> pairResponse(ok)
// -> if pairResponse ok -> onSessionProposal -> sdk.buildApprovedNamespaces
// -> onBuildApprovedNamespace -> signal connectDApp
// - user requests root.approvePairSession/root.rejectPairSession
// -> if approvePairSession -> sdk.buildApprovedNamespaces
// -> onBuildApprovedNamespace -> sdk.approveSession -> onApproveSessionResult
QObject { QObject {
id: root id: root
@ -47,38 +56,39 @@ QObject {
function validatePairingUri(uri) { function validatePairingUri(uri) {
if(Helpers.containsOnlyEmoji(uri)) { if(Helpers.containsOnlyEmoji(uri)) {
root.pairingUriValidated(Pairing.uriErrors.tooCool) root.pairingValidated(Pairing.errors.tooCool)
return return
} else if(!Helpers.validURI(uri)) { } else if(!Helpers.validURI(uri)) {
root.pairingUriValidated(Pairing.uriErrors.invalidUri) root.pairingValidated(Pairing.errors.invalidUri)
return return
} }
let info = Helpers.extractInfoFromPairUri(uri) let info = Helpers.extractInfoFromPairUri(uri)
wcSDK.getActiveSessions((sessions) => { wcSDK.getActiveSessions((sessions) => {
// Check if the URI is already paired // Check if the URI is already paired
var validationState = Pairing.uriErrors.ok var validationState = Pairing.errors.ok
for (let key in sessions) { for (let key in sessions) {
if (sessions[key].pairingTopic == info.topic) { if (sessions[key].pairingTopic == info.topic) {
validationState = Pairing.uriErrors.alreadyUsed validationState = Pairing.errors.alreadyUsed
break break
} }
} }
// Check if expired // Check if expired
if (validationState == Pairing.uriErrors.ok) { if (validationState == Pairing.errors.ok) {
const now = (new Date().getTime())/1000 const now = (new Date().getTime())/1000
if (info.expiry < now) { if (info.expiry < now) {
validationState = Pairing.uriErrors.expired validationState = Pairing.errors.expired
} }
} }
root.pairingUriValidated(validationState) root.pairingValidated(validationState)
}); });
} }
function pair(uri) { function pair(uri) {
d.acceptedSessionProposal = null d.acceptedSessionProposal = null
timeoutTimer.start()
wcSDK.pair(uri) wcSDK.pair(uri)
} }
@ -116,11 +126,19 @@ QObject {
signal approveSessionResult(var session, var error) signal approveSessionResult(var session, var error)
signal sessionRequest(SessionRequestResolved request) signal sessionRequest(SessionRequestResolved request)
signal displayToastMessage(string message, bool error) signal displayToastMessage(string message, bool error)
signal pairingUriValidated(int validationState) // Emitted as a response to WalletConnectService.validatePairingUri or other WalletConnectService.pair
// and WalletConnectService.approvePair errors
signal pairingValidated(int validationState)
readonly property Connections sdkConnections: Connections { readonly property Connections sdkConnections: Connections {
target: wcSDK target: wcSDK
function onPairResponse(ok) {
if (!ok) {
d.reportPairErrorState(Pairing.errors.unknownError)
} // else waiting for onSessionProposal
}
function onSessionProposal(sessionProposal) { function onSessionProposal(sessionProposal) {
d.currentSessionProposal = sessionProposal d.currentSessionProposal = sessionProposal
@ -133,9 +151,9 @@ QObject {
if(error) { if(error) {
// Check that it contains Non conforming namespaces" // Check that it contains Non conforming namespaces"
if (error.includes("Non conforming namespaces")) { if (error.includes("Non conforming namespaces")) {
root.pairingUriValidated(Pairing.uriErrors.unsupportedNetwork) d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
} else { } else {
root.pairingUriValidated(Pairing.uriErrors.unknownError) d.reportPairErrorState(Pairing.errors.unknownError)
} }
return return
} }
@ -151,8 +169,7 @@ QObject {
function onApproveSessionResult(session, err) { function onApproveSessionResult(session, err) {
if (err) { if (err) {
// TODO #14676: handle the error d.reportPairErrorState(Pairing.errors.unknownError)
console.error("Failed to approve session", err)
return return
} }
@ -176,6 +193,7 @@ QObject {
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-" const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
const app_domain = StringUtils.extractDomainFromLink(app_url) const app_domain = StringUtils.extractDomainFromLink(app_url)
if(err) { if(err) {
d.reportPairErrorState(Pairing.errors.unknownError)
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), true) root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), true)
} else { } else {
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), false) root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), false)
@ -202,21 +220,9 @@ QObject {
property var currentSessionProposal: null property var currentSessionProposal: null
property var acceptedSessionProposal: null property var acceptedSessionProposal: null
// TODO #14676: use it to check if already paired function reportPairErrorState(state) {
function getPairingTopicFromPairingUrl(url) timeoutTimer.stop()
{ root.pairingValidated(state)
if (!url.startsWith("wc:"))
{
return null;
}
const atIndex = url.indexOf("@");
if (atIndex < 0)
{
return null;
}
return url.slice(3, atIndex);
} }
} }
@ -233,6 +239,7 @@ QObject {
networksModel: root.flatNetworks networksModel: root.flatNetworks
onSessionRequest: (request) => { onSessionRequest: (request) => {
timeoutTimer.stop()
root.sessionRequest(request) root.sessionRequest(request)
} }
onDisplayToastMessage: (message, error) => { onDisplayToastMessage: (message, error) => {
@ -246,4 +253,17 @@ QObject {
sdk: root.wcSDK sdk: root.wcSDK
store: root.store store: root.store
} }
// 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
Timer {
id: timeoutTimer
interval: 10000 // (10 seconds)
running: false
repeat: false
onTriggered: {
d.reportPairErrorState(Pairing.errors.unknownError)
}
}
} }

View File

@ -3,7 +3,7 @@ pragma Singleton
import QtQml 2.15 import QtQml 2.15
QtObject { QtObject {
readonly property QtObject uriErrors: QtObject { readonly property QtObject errors: QtObject {
readonly property int notChecked: 0 readonly property int notChecked: 0
readonly property int ok: 1 readonly property int ok: 1
readonly property int tooCool: 2 readonly property int tooCool: 2

View File

@ -27,9 +27,9 @@ StatusDialog {
property bool isPairing: false property bool isPairing: false
function pairingUriValidated(validationState) { function pairingValidated(validationState) {
uriInput.errorState = validationState uriInput.errorState = validationState
if (validationState === Pairing.uriErrors.ok) { if (validationState === Pairing.errors.ok) {
d.doPair() d.doPair()
} }
} }
@ -51,7 +51,7 @@ StatusDialog {
WCUriInput { WCUriInput {
id: uriInput id: uriInput
pending: uriInput.errorState === Pairing.uriErrors.notChecked pending: uriInput.errorState === Pairing.errors.notChecked
onTextChanged: { onTextChanged: {
root.isPairing = false root.isPairing = false
@ -91,7 +91,7 @@ StatusDialog {
enabled: uriInput.valid enabled: uriInput.valid
&& !root.isPairing && !root.isPairing
&& uriInput.text.length > 0 && uriInput.text.length > 0
&& uriInput.errorState === Pairing.uriErrors.ok && uriInput.errorState === Pairing.errors.ok
onClicked: { onClicked: {
d.doPair() d.doPair()

View File

@ -17,7 +17,7 @@ ColumnLayout {
readonly property bool valid: input.valid && input.text.length > 0 readonly property bool valid: input.valid && input.text.length > 0
readonly property alias text: input.text readonly property alias text: input.text
property alias pending: input.pending property alias pending: input.pending
property int errorState: Pairing.uriErrors.notChecked property int errorState: Pairing.errors.notChecked
StatusBaseInput { StatusBaseInput {
id: input id: input
@ -37,22 +37,21 @@ ColumnLayout {
return true return true
} }
if(root.errorState === Pairing.uriErrors.tooCool) { if(root.errorState === Pairing.errors.tooCool) {
errorText.text = qsTr("WalletConnect URI too cool") errorText.text = qsTr("WalletConnect URI too cool")
} else if(root.errorState === Pairing.uriErrors.invalidUri) { } else if(root.errorState === Pairing.errors.invalidUri) {
errorText.text = qsTr("WalletConnect URI invalid") errorText.text = qsTr("WalletConnect URI invalid")
} else if(root.errorState === Pairing.uriErrors.alreadyUsed) { } else if(root.errorState === Pairing.errors.alreadyUsed) {
errorText.text = qsTr("WalletConnect URI already used") errorText.text = qsTr("WalletConnect URI already used")
} else if(root.errorState === Pairing.uriErrors.expired) { } else if(root.errorState === Pairing.errors.expired) {
errorText.text = qsTr("WalletConnect URI has expired") errorText.text = qsTr("WalletConnect URI has expired")
} } else if(root.errorState === Pairing.errors.unsupportedNetwork) {
if (errorText.text.length > 0) {
return false
} else if(root.errorState === Pairing.uriErrors.unsupportedNetwork) {
errorText.text = qsTr("dApp is requesting to connect on an unsupported network") errorText.text = qsTr("dApp is requesting to connect on an unsupported network")
return false } else if(root.errorState === Pairing.errors.unknownError) {
} else if(root.errorState === Pairing.uriErrors.unknownError) { errorText.text = qsTr("Unexpected error occurred. Try again.")
errorText.text = qsTr("Unexpected error occurred, please try again") }
if (errorText.text.length > 0) {
return false return false
} }