From 98c18901e0d711f392c50d35278f36ba6915484d Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 29 Jul 2024 16:50:08 +0300 Subject: [PATCH] fix(dapps) Wallet Connect internet connection reestablishing issue Add a new NetworkChecker QObject to StatusQ to be used in checking internet connection status. This is used by the WebEngineLoader to only allow loading of web pages when there is an active internet to cover for a corner case on MacOS where the internet connection is not reestablished if the WebEngineView was loaded without an active internet connection. Closes: #15598, #15806 --- src/backend/wallet_connect.nim | 3 +- storybook/pages/DAppsWorkflowPage.qml | 11 +++- ui/StatusQ/CMakeLists.txt | 2 + ui/StatusQ/include/StatusQ/networkchecker.h | 41 +++++++++++++ .../StatusQ/Components/WebEngineLoader.qml | 60 +++++++++++++----- ui/StatusQ/src/networkchecker.cpp | 61 +++++++++++++++++++ ui/StatusQ/src/plugin.cpp | 2 + .../TestComponents/tst_WebEngineLoader.qml | 6 +- .../services/dapps/WalletConnectSDK.qml | 28 ++++----- .../Wallet/services/dapps/helpers.js | 12 +++- ui/app/mainui/AppMain.qml | 2 +- 11 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 ui/StatusQ/include/StatusQ/networkchecker.h create mode 100644 ui/StatusQ/src/networkchecker.cpp diff --git a/src/backend/wallet_connect.nim b/src/backend/wallet_connect.nim index 28414f9a26..d3863da3ba 100644 --- a/src/backend/wallet_connect.nim +++ b/src/backend/wallet_connect.nim @@ -57,8 +57,7 @@ proc getActiveSessions*(validAtTimestamp: int): JsonNode = return nil let jsonResultStr = rpcRes.result.getStr() - if jsonResultStr == "null": - # nil means error + if jsonResultStr == "null" or jsonResultStr == "": return newJArray() if rpcRes.result.kind != JArray: diff --git a/storybook/pages/DAppsWorkflowPage.qml b/storybook/pages/DAppsWorkflowPage.qml index c281a253b3..6e54e05b74 100644 --- a/storybook/pages/DAppsWorkflowPage.qml +++ b/storybook/pages/DAppsWorkflowPage.qml @@ -83,6 +83,15 @@ Item { font.bold: true } } + RowLayout { + StatusBaseText { text: "SDK status:" } + Rectangle { + Layout.preferredWidth: 20 + Layout.preferredHeight: Layout.preferredWidth + radius: Layout.preferredWidth / 2 + color: walletConnectService.wcSDK.sdkReady ? "green" : "red" + } + } CheckBox { text: "Testnet Mode" @@ -289,7 +298,7 @@ Item { id: walletConnectService wcSDK: WalletConnectSDK { - active: settings.enableSDK + enableSdk: settings.enableSDK projectId: projectIdText.projectId } diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index 3cd2f83c6c..5734c5d2fc 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -108,6 +108,7 @@ add_library(StatusQ SHARED include/StatusQ/modelsyncedcontainer.h include/StatusQ/modelutilsinternal.h include/StatusQ/movablemodel.h + include/StatusQ/networkchecker.h include/StatusQ/objectproxymodel.h include/StatusQ/permissionutilsinternal.h include/StatusQ/rolesrenamingmodel.h @@ -137,6 +138,7 @@ add_library(StatusQ SHARED src/modelentry.cpp src/modelutilsinternal.cpp src/movablemodel.cpp + src/networkchecker.cpp src/objectproxymodel.cpp src/permissionutilsinternal.cpp src/plugin.cpp diff --git a/ui/StatusQ/include/StatusQ/networkchecker.h b/ui/StatusQ/include/StatusQ/networkchecker.h new file mode 100644 index 0000000000..39e4f91c3f --- /dev/null +++ b/ui/StatusQ/include/StatusQ/networkchecker.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include + +using namespace std::chrono_literals; + +/// Checks if the internet connection is available, when active. +/// It checks the connection every 30 seconds as long as the \c active property is \c true. +class NetworkChecker : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged) + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + +public: + explicit NetworkChecker(QObject* parent = nullptr); + bool isOnline() const; + + bool isActive() const; + void setActive(bool active); + +signals: + void isOnlineChanged(bool online); + void activeChanged(bool active); + +private: + QNetworkAccessManager manager; + QTimer timer; + bool online = false; + bool active = true; + constexpr static std::chrono::milliseconds checkInterval = 30s; + + void checkNetwork(); + void onFinished(QNetworkReply* reply); + void updateRegularCheck(bool active); +}; \ No newline at end of file diff --git a/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml b/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml index afd84415a5..a7fe2b6a91 100644 --- a/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml +++ b/ui/StatusQ/src/StatusQ/Components/WebEngineLoader.qml @@ -3,6 +3,8 @@ import QtQuick 2.15 import QtWebEngine 1.10 import QtWebChannel 1.15 +import StatusQ 0.1 + // Helper to load and setup an instance of \c WebEngineView // // The \c webChannelObjects property is used to register specific objects @@ -11,34 +13,33 @@ import QtWebChannel 1.15 // qrc:/StatusQ/Components/private/qwebchannel/helpers.js will provide // access to window.statusq APIs used to exchange data between the internal // web engine and the QML application +// +// It doesn't load the web engine until NetworkChecker detects and active internet +// connection to avoid the corner case of initializing the web engine without +// network connectivity. If the web engine is initialized without network connectivity +// it won't restore the connectivity when it's available on Mac OS Item { id: root required property url url required property var webChannelObjects - property alias active: loader.active + // Used to control the loading of the web engine + property bool active: false + // Useful to monitor the loading state of the web engine (depends on active and internet connectivity) + readonly property bool isActive: loader.active property alias instance: loader.item + property bool waitForInternet: true signal engineLoaded(WebEngineView instance) signal engineUnloaded() signal pageLoaded() signal pageLoadingError(string errorString) - Loader { - id: loader + Component { + id: webEngineViewComponent - active: false - - onStatusChanged: function() { - if (status === Loader.Ready) { - root.engineLoaded(loader.item) - } else if (status === Loader.Null) { - root.engineUnloaded() - } - } - - sourceComponent: WebEngineView { + WebEngineView { id: webEngineView anchors.fill: parent @@ -65,4 +66,35 @@ Item { } } } + + Loader { + id: loader + + active: root.active && (!root.waitForInternet || (d.passedFirstTimeInitialization || networkChecker.isOnline)) + + onStatusChanged: function() { + if (status === Loader.Ready) { + root.engineLoaded(loader.item) + d.passedFirstTimeInitialization = true + } else if (status === Loader.Null) { + root.engineUnloaded() + } + } + + sourceComponent: webEngineViewComponent + } + + NetworkChecker { + id: networkChecker + + // Deactivate searching for network connectivity after the web engine is loaded + active: !d.passedFirstTimeInitialization + } + + QtObject { + id: d + + // Used to hold the loading of the web engine until internet connectivity is available + property bool passedFirstTimeInitialization: false + } } \ No newline at end of file diff --git a/ui/StatusQ/src/networkchecker.cpp b/ui/StatusQ/src/networkchecker.cpp new file mode 100644 index 0000000000..9a8fea3136 --- /dev/null +++ b/ui/StatusQ/src/networkchecker.cpp @@ -0,0 +1,61 @@ +#include "StatusQ/networkchecker.h" + +NetworkChecker::NetworkChecker(QObject* parent) + : QObject(parent) +{ + connect(&manager, &QNetworkAccessManager::finished, this, &NetworkChecker::onFinished); + connect(&timer, &QTimer::timeout, this, &NetworkChecker::checkNetwork); + + updateRegularCheck(active); +} + +bool NetworkChecker::isOnline() const +{ + return online; +} + +void NetworkChecker::checkNetwork() +{ + QNetworkRequest request(QUrl("https://fedoraproject.org/static/hotspot.txt")); + manager.get(request); +} + +void NetworkChecker::onFinished(QNetworkReply* reply) +{ + bool wasOnline = online; + online = (reply->error() == QNetworkReply::NoError); + reply->deleteLater(); + + if(wasOnline != online) + { + emit isOnlineChanged(online); + } +} + +bool NetworkChecker::isActive() const +{ + return active; +} + +void NetworkChecker::setActive(bool active) +{ + if(active == this->active) return; + + this->active = active; + emit activeChanged(active); + + updateRegularCheck(active); +} + +void NetworkChecker::updateRegularCheck(bool active) +{ + if(active) + { + checkNetwork(); + timer.start(checkInterval); + } + else + { + timer.stop(); + } +} \ No newline at end of file diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp index 93526ac313..dcc0bf9a82 100644 --- a/ui/StatusQ/src/plugin.cpp +++ b/ui/StatusQ/src/plugin.cpp @@ -17,6 +17,7 @@ #include "StatusQ/modelentry.h" #include "StatusQ/modelutilsinternal.h" #include "StatusQ/movablemodel.h" +#include "StatusQ/networkchecker.h" #include "StatusQ/objectproxymodel.h" #include "StatusQ/permissionutilsinternal.h" #include "StatusQ/rolesrenamingmodel.h" @@ -58,6 +59,7 @@ public: qmlRegisterType("StatusQ", 0, 1, "SourceModel"); qmlRegisterType("StatusQ", 0, 1, "ConcatModel"); qmlRegisterType("StatusQ", 0, 1, "MovableModel"); + qmlRegisterType("StatusQ", 0, 1, "NetworkChecker"); qmlRegisterType("StatusQ", 0, 1, "FastExpressionFilter"); qmlRegisterType("StatusQ", 0, 1, "FastExpressionRole"); diff --git a/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml b/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml index 339db1bd80..b14c1126b7 100644 --- a/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml +++ b/ui/StatusQ/tests/TestComponents/tst_WebEngineLoader.qml @@ -38,6 +38,8 @@ TestCase { sourceComponent: WebEngineLoader { url: "./WebEngineLoader/test.html" webChannelObjects: [testObject] + + waitForInternet: false } } SignalSpy { id: loadedSpy; target: loader; signalName: "loaded" } @@ -67,13 +69,13 @@ TestCase { compare(webEngine.instance, null, "By default the engine is not loaded") webEngine.active = true - webEngineLoadedSpy.wait(1000); + webEngineLoadedSpy.wait(1000) verify(webEngine.instance !== null , "The WebEngineView should be available") if (Qt.platform.os === "linux") { skip("fails to load page on linux") } - pageLoadedSpy.wait(1000); + pageLoadedSpy.wait(1000) webEngine.active = false engineUnloadedSpy.wait(1000); diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml index 9f24366b32..149bcd5539 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectSDK.qml @@ -14,10 +14,10 @@ WalletConnectSDKBase { id: root readonly property alias sdkReady: d.sdkReady - readonly property alias webEngineLoader: loader - property alias active: loader.active - property alias url: loader.url + // Enable the WalletConnect SDK + property alias enableSdk: loader.active + readonly property alias url: loader.url implicitWidth: 1 implicitHeight: 1 @@ -81,15 +81,15 @@ WalletConnectSDKBase { function init() { console.debug(`WC WalletConnectSDK.wcCall.init; root.projectId: ${root.projectId}`) - d.engine.runJavaScript(`wc.init("${root.projectId}").catch((error) => {wc.statusObject.sdkInitialized("SDK init error: "+error);})`, function(result) { - - console.debug(`WC WalletConnectSDK.wcCall.init; response: ${JSON.stringify(result)}`) - - if (result && !!result.error) - { - console.error("init: ", result.error) - } - }) + d.engine.runJavaScript(` + wc.init("${root.projectId}") + .then(()=> { + wc.statusObject.sdkInitialized(""); + }) + .catch((error) => { + wc.statusObject.sdkInitialized("SDK init error: "+error) + }) + `) } function getPairings(callback) { @@ -259,7 +259,7 @@ WalletConnectSDKBase { WebChannel.id: "statusObject" - function bubbleConsoleMessage(type, message) { + function echo(type, message) { if (type === "warn") { console.warn(message) } else if (type === "debug") { @@ -272,7 +272,7 @@ WalletConnectSDKBase { } function sdkInitialized(error) { - console.debug(`WC WalletConnectSDK.sdkInitialized; error: ${error}`) + console.debug(`WC WalletConnectSDK.sdkInitialized: ${!!error ? "error: " + error : "success"}`) d.sdkReady = !error root.sdkInit(d.sdkReady, error) } diff --git a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js index 85b2fbc930..b67d98730c 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/helpers.js +++ b/ui/app/AppLayouts/Wallet/services/dapps/helpers.js @@ -54,8 +54,8 @@ function buildSupportedNamespacesFromModels(chainsModel, accountsModel, methods) } function buildSupportedNamespaces(chainIds, addresses, methods) { - var eipChainIds = [] - var eipAddresses = [] + let eipChainIds = [] + let eipAddresses = [] for (let i = 0; i < chainIds.length; i++) { let chainId = chainIds[i] eipChainIds.push(`"eip155:${chainId}"`) @@ -65,7 +65,13 @@ function buildSupportedNamespaces(chainIds, addresses, methods) { } let methodsStr = methods.map(method => `"${method}"`).join(',') return `{ - "eip155":{"chains": [${eipChainIds.join(',')}],"methods": [${methodsStr}],"events": ["accountsChanged", "chainChanged"],"accounts": [${eipAddresses.join(',')}]}}` + "eip155":{ + "chains": [${eipChainIds.join(',')}], + "methods": [${methodsStr}], + "events": ["accountsChanged", "chainChanged"], + "accounts": [${eipAddresses.join(',')}] + } + }` } function validURI(uri) { diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index e975fd9c82..da9da15b0f 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2191,7 +2191,7 @@ Item { id: walletConnectService wcSDK: WalletConnectSDK { - active: WalletStore.RootStore.walletSectionInst.walletReady + enableSdk: WalletStore.RootStore.walletSectionInst.walletReady projectId: WalletStore.RootStore.appSettings.walletConnectProjectID }