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
This commit is contained in:
Stefan 2024-07-29 16:50:08 +03:00 committed by Stefan Dunca
parent 910af539d6
commit 98c18901e0
11 changed files with 191 additions and 37 deletions

View File

@ -57,8 +57,7 @@ proc getActiveSessions*(validAtTimestamp: int): JsonNode =
return nil return nil
let jsonResultStr = rpcRes.result.getStr() let jsonResultStr = rpcRes.result.getStr()
if jsonResultStr == "null": if jsonResultStr == "null" or jsonResultStr == "":
# nil means error
return newJArray() return newJArray()
if rpcRes.result.kind != JArray: if rpcRes.result.kind != JArray:

View File

@ -83,6 +83,15 @@ Item {
font.bold: true 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 { CheckBox {
text: "Testnet Mode" text: "Testnet Mode"
@ -289,7 +298,7 @@ Item {
id: walletConnectService id: walletConnectService
wcSDK: WalletConnectSDK { wcSDK: WalletConnectSDK {
active: settings.enableSDK enableSdk: settings.enableSDK
projectId: projectIdText.projectId projectId: projectIdText.projectId
} }

View File

@ -108,6 +108,7 @@ add_library(StatusQ SHARED
include/StatusQ/modelsyncedcontainer.h include/StatusQ/modelsyncedcontainer.h
include/StatusQ/modelutilsinternal.h include/StatusQ/modelutilsinternal.h
include/StatusQ/movablemodel.h include/StatusQ/movablemodel.h
include/StatusQ/networkchecker.h
include/StatusQ/objectproxymodel.h include/StatusQ/objectproxymodel.h
include/StatusQ/permissionutilsinternal.h include/StatusQ/permissionutilsinternal.h
include/StatusQ/rolesrenamingmodel.h include/StatusQ/rolesrenamingmodel.h
@ -137,6 +138,7 @@ add_library(StatusQ SHARED
src/modelentry.cpp src/modelentry.cpp
src/modelutilsinternal.cpp src/modelutilsinternal.cpp
src/movablemodel.cpp src/movablemodel.cpp
src/networkchecker.cpp
src/objectproxymodel.cpp src/objectproxymodel.cpp
src/permissionutilsinternal.cpp src/permissionutilsinternal.cpp
src/plugin.cpp src/plugin.cpp

View File

@ -0,0 +1,41 @@
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QTimer>
#include <chrono>
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);
};

View File

@ -3,6 +3,8 @@ import QtQuick 2.15
import QtWebEngine 1.10 import QtWebEngine 1.10
import QtWebChannel 1.15 import QtWebChannel 1.15
import StatusQ 0.1
// Helper to load and setup an instance of \c WebEngineView // Helper to load and setup an instance of \c WebEngineView
// //
// The \c webChannelObjects property is used to register specific objects // 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 // qrc:/StatusQ/Components/private/qwebchannel/helpers.js will provide
// access to window.statusq APIs used to exchange data between the internal // access to window.statusq APIs used to exchange data between the internal
// web engine and the QML application // 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 { Item {
id: root id: root
required property url url required property url url
required property var webChannelObjects 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 alias instance: loader.item
property bool waitForInternet: true
signal engineLoaded(WebEngineView instance) signal engineLoaded(WebEngineView instance)
signal engineUnloaded() signal engineUnloaded()
signal pageLoaded() signal pageLoaded()
signal pageLoadingError(string errorString) signal pageLoadingError(string errorString)
Loader { Component {
id: loader id: webEngineViewComponent
active: false WebEngineView {
onStatusChanged: function() {
if (status === Loader.Ready) {
root.engineLoaded(loader.item)
} else if (status === Loader.Null) {
root.engineUnloaded()
}
}
sourceComponent: WebEngineView {
id: webEngineView id: webEngineView
anchors.fill: parent 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
}
} }

View File

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

View File

@ -17,6 +17,7 @@
#include "StatusQ/modelentry.h" #include "StatusQ/modelentry.h"
#include "StatusQ/modelutilsinternal.h" #include "StatusQ/modelutilsinternal.h"
#include "StatusQ/movablemodel.h" #include "StatusQ/movablemodel.h"
#include "StatusQ/networkchecker.h"
#include "StatusQ/objectproxymodel.h" #include "StatusQ/objectproxymodel.h"
#include "StatusQ/permissionutilsinternal.h" #include "StatusQ/permissionutilsinternal.h"
#include "StatusQ/rolesrenamingmodel.h" #include "StatusQ/rolesrenamingmodel.h"
@ -58,6 +59,7 @@ public:
qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel"); qmlRegisterType<SourceModel>("StatusQ", 0, 1, "SourceModel");
qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel"); qmlRegisterType<ConcatModel>("StatusQ", 0, 1, "ConcatModel");
qmlRegisterType<MovableModel>("StatusQ", 0, 1, "MovableModel"); qmlRegisterType<MovableModel>("StatusQ", 0, 1, "MovableModel");
qmlRegisterType<NetworkChecker>("StatusQ", 0, 1, "NetworkChecker");
qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter"); qmlRegisterType<FastExpressionFilter>("StatusQ", 0, 1, "FastExpressionFilter");
qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole"); qmlRegisterType<FastExpressionRole>("StatusQ", 0, 1, "FastExpressionRole");

View File

@ -38,6 +38,8 @@ TestCase {
sourceComponent: WebEngineLoader { sourceComponent: WebEngineLoader {
url: "./WebEngineLoader/test.html" url: "./WebEngineLoader/test.html"
webChannelObjects: [testObject] webChannelObjects: [testObject]
waitForInternet: false
} }
} }
SignalSpy { id: loadedSpy; target: loader; signalName: "loaded" } SignalSpy { id: loadedSpy; target: loader; signalName: "loaded" }
@ -67,13 +69,13 @@ TestCase {
compare(webEngine.instance, null, "By default the engine is not loaded") compare(webEngine.instance, null, "By default the engine is not loaded")
webEngine.active = true webEngine.active = true
webEngineLoadedSpy.wait(1000); webEngineLoadedSpy.wait(1000)
verify(webEngine.instance !== null , "The WebEngineView should be available") verify(webEngine.instance !== null , "The WebEngineView should be available")
if (Qt.platform.os === "linux") { if (Qt.platform.os === "linux") {
skip("fails to load page on linux") skip("fails to load page on linux")
} }
pageLoadedSpy.wait(1000); pageLoadedSpy.wait(1000)
webEngine.active = false webEngine.active = false
engineUnloadedSpy.wait(1000); engineUnloadedSpy.wait(1000);

View File

@ -14,10 +14,10 @@ WalletConnectSDKBase {
id: root id: root
readonly property alias sdkReady: d.sdkReady readonly property alias sdkReady: d.sdkReady
readonly property alias webEngineLoader: loader
property alias active: loader.active // Enable the WalletConnect SDK
property alias url: loader.url property alias enableSdk: loader.active
readonly property alias url: loader.url
implicitWidth: 1 implicitWidth: 1
implicitHeight: 1 implicitHeight: 1
@ -81,15 +81,15 @@ WalletConnectSDKBase {
function init() { function init() {
console.debug(`WC WalletConnectSDK.wcCall.init; root.projectId: ${root.projectId}`) 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) { d.engine.runJavaScript(`
wc.init("${root.projectId}")
console.debug(`WC WalletConnectSDK.wcCall.init; response: ${JSON.stringify(result)}`) .then(()=> {
wc.statusObject.sdkInitialized("");
if (result && !!result.error) })
{ .catch((error) => {
console.error("init: ", result.error) wc.statusObject.sdkInitialized("SDK init error: "+error)
} })
}) `)
} }
function getPairings(callback) { function getPairings(callback) {
@ -259,7 +259,7 @@ WalletConnectSDKBase {
WebChannel.id: "statusObject" WebChannel.id: "statusObject"
function bubbleConsoleMessage(type, message) { function echo(type, message) {
if (type === "warn") { if (type === "warn") {
console.warn(message) console.warn(message)
} else if (type === "debug") { } else if (type === "debug") {
@ -272,7 +272,7 @@ WalletConnectSDKBase {
} }
function sdkInitialized(error) { function sdkInitialized(error) {
console.debug(`WC WalletConnectSDK.sdkInitialized; error: ${error}`) console.debug(`WC WalletConnectSDK.sdkInitialized: ${!!error ? "error: " + error : "success"}`)
d.sdkReady = !error d.sdkReady = !error
root.sdkInit(d.sdkReady, error) root.sdkInit(d.sdkReady, error)
} }

View File

@ -54,8 +54,8 @@ function buildSupportedNamespacesFromModels(chainsModel, accountsModel, methods)
} }
function buildSupportedNamespaces(chainIds, addresses, methods) { function buildSupportedNamespaces(chainIds, addresses, methods) {
var eipChainIds = [] let eipChainIds = []
var eipAddresses = [] let eipAddresses = []
for (let i = 0; i < chainIds.length; i++) { for (let i = 0; i < chainIds.length; i++) {
let chainId = chainIds[i] let chainId = chainIds[i]
eipChainIds.push(`"eip155:${chainId}"`) eipChainIds.push(`"eip155:${chainId}"`)
@ -65,7 +65,13 @@ function buildSupportedNamespaces(chainIds, addresses, methods) {
} }
let methodsStr = methods.map(method => `"${method}"`).join(',') let methodsStr = methods.map(method => `"${method}"`).join(',')
return `{ 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) { function validURI(uri) {

View File

@ -2191,7 +2191,7 @@ Item {
id: walletConnectService id: walletConnectService
wcSDK: WalletConnectSDK { wcSDK: WalletConnectSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady enableSdk: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
} }