diff --git a/Makefile b/Makefile index 38f67cb158..809f4645f8 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,7 @@ statusq-tests: -DSTATUSQ_BUILD_SANDBOX=OFF \ -DSTATUSQ_BUILD_SANITY_CHECKER=OFF \ -DSTATUSQ_BUILD_TESTS=ON \ + -DSTATUSQ_SHADOW_BUILD=OFF \ -B $(STATUSQ_BUILD_PATH) \ -S $(STATUSQ_SOURCE_PATH) \ $(HANDLE_OUTPUT) diff --git a/ui/StatusQ/src/StatusQ/Components/StatusWebView.qml b/ui/StatusQ/src/StatusQ/Components/StatusWebView.qml new file mode 100644 index 0000000000..80d76b6244 --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Components/StatusWebView.qml @@ -0,0 +1,191 @@ +import QtQuick 2.15 +import QtWebView 1.15 + +// Specialization of \c WebView that provides a bridge between QML and JS code in the web page +// The bridge uses a polling mechanism for handing async responses +// TODO: stop timer when there is nothing to poll to +// TODO: simpler events +WebView { + id: root + + // object name under the window object that will be used to cache internal runtime state + property string globalObjectName: "statusq" + + signal contentReady(); + signal contentFailedLoading(string errorString); + + function asyncCall(callName, paramsStr) { + return d.call(callName, paramsStr, d.callTypeAsync) + } + + function call(callName, paramsStr) { + return d.call(callName, paramsStr, d.callTypeSync) + } + + // callback of type (result, error) => {} + function onCallback(callName, callback) { + d.call(callName, null, d.callTypeCallback).then(function(result) { + callback(result, false) + }).error(function(error) { + callback(error, true) + }); + } + + Timer { + id: timer + interval: 100 + repeat: true + running: false + + onTriggered: { + root.runJavaScript(`${d.ctx}.popCalls = ${d.ctx}.calls; ${d.ctx}.calls = null; ${d.ctx}.popCalls`, function(results) { + if (!results) { + return; + } + d.pendingResults = d.pendingResults.concat(results); + + d.processPendingResults(); + }); + } + } + + QtObject { + id: d + + readonly property int successState: 1 + readonly property int errorState: 2 + readonly property int exceptionState: 3 + + readonly property int callTypeAsync: 1 + readonly property int callTypeSync: 2 + readonly property int callTypeCallback: 3 + + readonly property string ctx: `window.${root.globalObjectName}` + + property int nextCallIndex: 0 + property var callbacks: ({}) + property var pendingResults: [] + + function call(callName, paramsStr, callType) { + const currentCallIndex = d.nextCallIndex++; + var jsCode = ` + if (!${d.ctx}) { + ${d.ctx} = {}; + } + function reportCallResult(callIndex, state, result) { + if (!${d.ctx}.calls) { + ${d.ctx}.calls = []; + } + const callRes = {state: state, result: result, callIndex: callIndex}; + ${d.ctx}.calls.push(callRes); + } + + try { + switch(${callType}) { + case ${d.callTypeAsync}: + ${callName}(${paramsStr}).then((callRes) => { + reportCallResult(${currentCallIndex}, ${d.successState}, callRes); + }).catch((error) => { + reportCallResult(${currentCallIndex}, ${d.errorState}, error); + }); + break; + case ${d.callTypeSync}: + const callRes = ${callName}(${paramsStr}); + reportCallResult(${currentCallIndex}, ${d.successState}, callRes); + break; + case ${d.callTypeCallback}: + ${callName}(${paramsStr}, function(callRes) { + reportCallResult(${currentCallIndex}, ${d.successState}, callRes); + }); + } + } catch (e) { + ${d.ctx}.errorRes = {state: ${d.exceptionState}, result: e.message, callIndex: ${currentCallIndex}}; + } + ${d.ctx}.errorRes + `; + + let promise = promiseComponent.createObject(null, {callIndex: currentCallIndex}) + root.runJavaScript(jsCode, function(result) { + d.callbacks[currentCallIndex] = promise; + if (!result) { + timer.restart(); + return; + } + + // Process it now + d.pendingResults = d.pendingResults.concat(result); + d.processPendingResults(); + }); + + return promise; + } + + function processPendingResults() { + while(pendingResults.length != 0) { + const res = pendingResults[0] + if(d.callbacks[res.callIndex]) { + const callback = d.callbacks[res.callIndex] + if (res.state === d.successState && callback.hasSuccess()) { + callback.successCallback(res.result) + } else if (res.state !== d.successState && callback.hasError()) { + callback.errorCallback(res.result) + } else { + callback.result = res + } + d.callbacks[res.callIndex] = null + } + + pendingResults.splice(0, 1) + } + } + } + + Component { + id: promiseComponent + + QtObject { + id: callbackObj + + function then(callback) { + successCallback = callback; + // If the callback is set after the result is available + if (result && result.state === d.successState) { + successCallback(result.result); + } + return this; + } + + function error(callback) { + errorCallback = callback; + // If the callback is set after the result is available + if (result && result.state !== d.successState) { + successCallback(result.result); + } + return this; + } + + function hasSuccess() { + return successCallback !== null + } + function hasError() { + return errorCallback !== null + } + + property var successCallback: null + property var errorCallback: null + property int callIndex: -1 + property var result: null + } + } + + onLoadingChanged: function(loadRequest) { + switch(loadRequest.status) { + case WebView.LoadSucceededStatus: + root.contentReady(); + break + case WebView.LoadFailedStatus: + root.contentFailedLoading(loadRequest.errorString); + break + } + } +} \ No newline at end of file diff --git a/ui/StatusQ/src/StatusQ/Components/qmldir b/ui/StatusQ/src/StatusQ/Components/qmldir index 617d87678b..6804c74ee6 100644 --- a/ui/StatusQ/src/StatusQ/Components/qmldir +++ b/ui/StatusQ/src/StatusQ/Components/qmldir @@ -66,3 +66,4 @@ StatusToastMessage 0.1 StatusToastMessage.qml StatusToolBar 0.1 StatusToolBar.qml StatusVideo 0.1 StatusVideo.qml StatusWizardStepper 0.1 StatusWizardStepper.qml +StatusWebView 0.1 StatusWebView.qml \ No newline at end of file diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 079e87408e..5c30cbd804 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -60,6 +60,7 @@ StatusQ/Components/StatusVideo.qml StatusQ/Components/StatusWizardStepper.qml StatusQ/Components/StatusSortableColumnHeader.qml + StatusQ/Components/StatusWebView.qml StatusQ/Controls/Validators/qmldir StatusQ/Controls/Validators/StatusAddressOrEnsValidator.qml StatusQ/Controls/Validators/StatusAddressValidator.qml diff --git a/ui/StatusQ/tests/CMakeLists.txt b/ui/StatusQ/tests/CMakeLists.txt index 9bdb83f08f..bbe72290ba 100644 --- a/ui/StatusQ/tests/CMakeLists.txt +++ b/ui/StatusQ/tests/CMakeLists.txt @@ -6,8 +6,8 @@ enable_testing() set(CMAKE_AUTOMOC ON) -find_package(QT NAMES Qt6 Qt5 COMPONENTS QuickTest Qml Quick REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS QuickTest Qml Quick REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS QuickTest Qml Quick WebView REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS QuickTest Qml Quick WebView REQUIRED) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -34,6 +34,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::QuickTest Qt${QT_VERSION_MAJOR}::Qml Qt${QT_VERSION_MAJOR}::Quick + Qt${QT_VERSION_MAJOR}::WebView ) target_compile_definitions(${PROJECT_NAME} PRIVATE diff --git a/ui/StatusQ/tests/TestComponents/StatusWebView/test.html b/ui/StatusQ/tests/TestComponents/StatusWebView/test.html new file mode 100644 index 0000000000..641cbd1e65 --- /dev/null +++ b/ui/StatusQ/tests/TestComponents/StatusWebView/test.html @@ -0,0 +1,39 @@ + + + + Test Page + + + +

Test Page

+ + diff --git a/ui/StatusQ/tests/TestComponents/tst_test-StatusWebView.qml b/ui/StatusQ/tests/TestComponents/tst_test-StatusWebView.qml new file mode 100644 index 0000000000..fbfc29464b --- /dev/null +++ b/ui/StatusQ/tests/TestComponents/tst_test-StatusWebView.qml @@ -0,0 +1,210 @@ +import QtQuick 2.15 +import QtTest 1.0 + +//import StatusQ 0.1 // https://github.com/status-im/status-desktop/issues/10218 + +import StatusQ.Components 0.1 + +import StatusQ.TestHelpers 0.1 + +TestCase { + id: root + name: "StatusWebView" + + Component { + id: webViewComponent + StatusWebView { + id: webView + url: "StatusWebView/test.html" + + anchors.fill: parent + } + } + + SignalSpy { + id: spyLoaded + target: webView + signalName: "contentReady" + } + + Component { + id: promiseResultComponent + QtObject { + property var result: null + property bool success: false + property bool completed: false + } + } + + property StatusWebView webView: null + + function init() { + webView = webViewComponent.createObject(root); + } + + function cleanup() { + webView.destroy(); + } + + function test_asyncFunction() { + spyLoaded.wait(1000); + const callbackInfo = promiseResultComponent.createObject(null); + var promise = webView.asyncCall("window.asyncFunction", "'asyncFunctionParam'"); + + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true; + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }); + tryCompare(callbackInfo, "completed", true, 1000, "The promise should complete"); + compare(callbackInfo.result, "asyncFunctionParam", "The promise should return the success result") + compare(callbackInfo.success, true, "The promise should succeed") + } + + function test_rejectAsyncFunction() { + spyLoaded.wait(1000); + const callbackInfo = promiseResultComponent.createObject(null); + var promise = webView.asyncCall("window.asyncRejectFunction", "'asyncRejectFunctionParam'"); + + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true; + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }); + tryCompare(callbackInfo, "completed", true, 1000, "The promise should complete"); + compare(callbackInfo.result, "asyncRejectFunctionParam", "The promise should return the success result") + compare(callbackInfo.success, false, "The promise should fail") + } + + function test_asyncCallMissing() { + spyLoaded.wait(1000); + const callbackInfo = promiseResultComponent.createObject(null); + var promise = webView.asyncCall("window.missingFunction", "'exceptionAsyncFunctionParam'"); + + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true; + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }); + tryCompare(callbackInfo, "completed", true, 1000, "The promise should complete"); + compare(callbackInfo.result, "window.missingFunction is not a function", "The promise should return a specific error message") + compare(callbackInfo.success, false, "The promise should fail") + } + + function test_registerLate() { + spyLoaded.wait(1000); + let asyncFinalized = {done: false} + var promise = webView.call("window.syncFunction", "'syncFunctionParam'"); + var asyncPromise = webView.asyncCall("window.asyncFunction", "'asyncFunctionParam'"); + + asyncPromise.then(function(result) { + asyncFinalized.done = true; + }); + tryCompare(asyncFinalized, "done", true, 1000, "The promise should complete"); + + const callbackInfo = promiseResultComponent.createObject(null); + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }) + compare(callbackInfo.completed, true, "The synchronous promise should complete serially"); + compare(callbackInfo.result, "syncFunctionParam", ".then should report the passed param") + compare(callbackInfo.success, true, "The promise should succeed") + } + + function test_synchronousCall() { + spyLoaded.wait(1000); + + const callbackInfo = promiseResultComponent.createObject(null); + + var promise = webView.call("window.syncFunction", "'syncFunctionParam'"); + + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }) + tryCompare(callbackInfo, "completed", true, 1000, "The synchronous promise should complete"); + compare(callbackInfo.result, "syncFunctionParam", ".then should report the passed param") + compare(callbackInfo.success, true, "The promise should succeed") + } + + function test_synchronousCallThrows() { + spyLoaded.wait(1000); + + const callbackInfo = promiseResultComponent.createObject(null); + + var promise = webView.call("window.syncFunctionThrows", "'syncFunctionThrowsParam'"); + + promise.then(function(result) { + callbackInfo.result = result; + callbackInfo.completed = true; + callbackInfo.success = true + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }) + tryCompare(callbackInfo, "completed", true, 1000, "The synchronous promise should complete"); + compare(callbackInfo.result, "Test exception", ".error should report a specific exception message") + compare(callbackInfo.success, false, "The promise should succeed") + } + + function test_synchronousCallMissing() { + spyLoaded.wait(1000); + + const callbackInfo = promiseResultComponent.createObject(null); + + var promise = webView.call("window.missingFunction", "'missingFunctionParam'"); + + promise.then(function(result) { + callbackInfo.completed = true; + callbackInfo.success = true + }).error(function(error) { + callbackInfo.result = error; + callbackInfo.completed = true; + callbackInfo.success = false; + }) + tryCompare(callbackInfo, "completed", true, 1000, "The synchronous promise should complete"); + compare(callbackInfo.success, false, "The promise should succeed") + compare(callbackInfo.result, "window.missingFunction is not a function", ".error should report a specific error message") + } + + // function test_onCallback() { + // spyLoaded.wait(1000); + + // const callbackInfo = promiseResultComponent.createObject(null); + + // let testInfo = {counter: 0} + + // webView.onCallback("document.addEventListener", `"testEvent"`, function(payload, error) { + // compare(payload, "testDetail", "The payload should be passed to the callback") + // testInfo.counter++; + // }) + + // webView.call("window.sendEvent", "'testDetail'"); + + // tryCompare(testInfo, "counter", 2, 1000, "Expects a specific number of calls"); + // } +} diff --git a/ui/StatusQ/tests/Testing/Temporary/CTestCostData.txt b/ui/StatusQ/tests/Testing/Temporary/CTestCostData.txt deleted file mode 100644 index ed97d539c0..0000000000 --- a/ui/StatusQ/tests/Testing/Temporary/CTestCostData.txt +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/ui/StatusQ/tests/Testing/Temporary/LastTest.log b/ui/StatusQ/tests/Testing/Temporary/LastTest.log deleted file mode 100644 index b3246465d5..0000000000 --- a/ui/StatusQ/tests/Testing/Temporary/LastTest.log +++ /dev/null @@ -1,3 +0,0 @@ -Start testing: Apr 18 18:49 CEST ----------------------------------------------------------- -End testing: Apr 18 18:49 CEST diff --git a/ui/StatusQ/tests/main.cpp b/ui/StatusQ/tests/main.cpp index 1204b6fec6..d520539b6e 100644 --- a/ui/StatusQ/tests/main.cpp +++ b/ui/StatusQ/tests/main.cpp @@ -3,6 +3,18 @@ #include "TestHelpers/MonitorQtOutput.h" +#include + +class RunBeforeQApplicationIsInitialized { +public: + RunBeforeQApplicationIsInitialized() + { + QtWebView::initialize(); + } +}; + +static RunBeforeQApplicationIsInitialized runBeforeQApplicationIsInitialized; + class TestSetup : public QObject { Q_OBJECT diff --git a/ui/app/AppLayouts/Wallet/views/walletconnect/RequestCodes.qml b/ui/app/AppLayouts/Wallet/views/walletconnect/RequestCodes.qml deleted file mode 100644 index bb0eaca7c8..0000000000 --- a/ui/app/AppLayouts/Wallet/views/walletconnect/RequestCodes.qml +++ /dev/null @@ -1,23 +0,0 @@ -import QtQuick 2.15 - -QtObject { - enum RequestCodes { - SdkInitSuccess, - SdkInitError, - - PairSuccess, - PairError, - ApprovePairSuccess, - ApprovePairError, - RejectPairSuccess, - RejectPairError, - - AcceptSessionSuccess, - AcceptSessionError, - RejectSessionSuccess, - RejectSessionError, - - GetPairings, - GetPairingsError - } -} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnect.qml b/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnect.qml index 6d061acf38..0a88b419be 100644 --- a/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnect.qml +++ b/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnect.qml @@ -148,46 +148,6 @@ Item { ColumnLayout { /* spacer */ } } - // TODO: DEBUG JS Loading in DMG - // RowLayout { - // TextField { - // id: urlInput - - // Layout.fillWidth: true - - // placeholderText: "Insert URL here" - // } - // Button { - // text: "Set URL" - // onClicked: { - // d.sdkView.url = urlInput.text - // } - // } - // } - - // Button { - // text: "Set HTML" - // onClicked: { - // d.sdkView.loadHtml(htmlContent.text, "http://status.im") - // } - // } - - // StatusInput { - // id: htmlContent - - // Layout.fillWidth: true - // Layout.minimumHeight: 200 - // Layout.maximumHeight: 300 - - // text: `TODO: Test\n\n\n` - - // multiline: true - // minimumHeight: Layout.minimumHeight - // maximumHeight: Layout.maximumHeight - - // } - // END DEBUGGING - // Separator ColumnLayout {} @@ -332,7 +292,6 @@ Item { target: root.controller function onRespondSessionRequest(sessionRequestJson, signedJson, error) { - console.log("@dd respondSessionRequest", sessionRequestJson, signedJson, error) if (error) { d.setStatusText("Session Request error", "red") d.sdkView.rejectSessionRequest(d.sessionRequest.topic, d.sessionRequest.id, true) diff --git a/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnectSDK.qml b/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnectSDK.qml index 890ad188aa..7cd62a728a 100644 --- a/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnectSDK.qml +++ b/ui/app/AppLayouts/Wallet/views/walletconnect/WalletConnectSDK.qml @@ -1,19 +1,16 @@ import QtQuick 2.15 import QtWebView 1.15 -// TODO #12434: remove debugging WebEngineView code -// import QtWebEngine 1.10 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import StatusQ.Core.Utils 0.1 as SQUtils +import StatusQ.Components 0.1 // Control used to instantiate and run the the WalletConnect web SDK // The view is not used to draw anything, but has to be visible to be able to run JS code // Use the \c backgroundColor property to blend in with the background // \warning A too smaller height might cause rendering errors -// TODO #12434: remove debugging WebEngineView code -WebView { -//WebEngineView { +StatusWebView { id: root implicitWidth: 1 @@ -33,59 +30,68 @@ WebView { signal sessionRequestUserAnswerResult(bool accept, string error) signal responseTimeout() - // TODO: proper report signal statusChanged(string message) function pair(pairLink) { - let callStr = d.generateSdkCall("pair", `"${pairLink}"`, RequestCodes.PairSuccess, RequestCodes.PairError) - d.requestSdkAsync(callStr) + root.asyncCall("wc.pair", `"${pairLink}"`).then((result) => { + root.pairSessionProposal(true, result) + d.getPairings() + }).error((error) => { + root.pairSessionProposal(false, error) + }) } function approvePairSession(sessionProposal, supportedNamespaces) { - let callStr = d.generateSdkCall("approvePairSession", `${JSON.stringify(sessionProposal)}, ${JSON.stringify(supportedNamespaces)}`, RequestCodes.ApprovePairSuccess, RequestCodes.ApprovePairSuccess) - - d.requestSdkAsync(callStr) + root.asyncCall("wc.approvePairSession", `${JSON.stringify(sessionProposal)}, ${JSON.stringify(supportedNamespaces)}`).then((result) => { + root.pairAcceptedResult(true, "") + d.getPairings() + }).error((error) => { + root.pairAcceptedResult(false, error) + d.getPairings() + }) } function rejectPairSession(id) { - let callStr = d.generateSdkCall("rejectPairSession", id, RequestCodes.RejectPairSuccess, RequestCodes.RejectPairError) - - d.requestSdkAsync(callStr) + root.asyncCall("wc.rejectPairSession", id).then((result) => { + root.pairRejectedResult(true, "") + }).error((error) => { + root.pairRejectedResult(false, error) + }) } function acceptSessionRequest(topic, id, signature) { - let callStr = d.generateSdkCall("respondSessionRequest", `"${topic}", ${id}, "${signature}"`, RequestCodes.AcceptSessionSuccess, RequestCodes.AcceptSessionError) - - d.requestSdkAsync(callStr) + root.asyncCall("wc.respondSessionRequest", `"${topic}", ${id}, "${signature}"`).then((result) => { + root.sessionRequestUserAnswerResult(true, "") + }).error((error) => { + root.sessionRequestUserAnswerResult(true, error) + }) } function rejectSessionRequest(topic, id, error) { - let callStr = d.generateSdkCall("rejectSessionRequest", `"${topic}", ${id}, ${error}`, RequestCodes.RejectSessionSuccess, RequestCodes.RejectSessionError) - - d.requestSdkAsync(callStr) + root.asyncCall("wc.rejectSessionRequest", `"${topic}", ${id}, ${error}`).then((result) => { + root.sessionRequestUserAnswerResult(false, "") + }).error((error) => { + root.sessionRequestUserAnswerResult(false, error) + }) } - // TODO #12434: remove debugging WebEngineView code - onLoadingChanged: function(loadRequest) { - console.debug(`@dd WalletConnectSDK.onLoadingChanged; status: ${loadRequest.status}; error: ${loadRequest.errorString}`) - switch(loadRequest.status) { - case WebView.LoadSucceededStatus: - // case WebEngineView.LoadSucceededStatus: - d.init(root.projectId) - break - case WebView.LoadFailedStatus: - // case WebEngineView.LoadFailedStatus: - root.statusChanged(`Failed loading SDK JS code; error: "${loadRequest.errorString}"`) - break - case WebView.LoadStartedStatus: - // case WebEngineView.LoadStartedStatus: - root.statusChanged(`Loading SDK JS code`) - break - } + onContentReady: { + root.asyncCall("wc.init", `"${projectId}"`).then((result) => { + d.sdkReady = true + root.sdkInit(true, "") + d.startListeningForEvents() + d.getPairings() + }).error((error) => { + d.sdkReady = false + root.sdkInit(false, error) + }) + } + + onContentFailedLoading: (errorString) => { + root.statusChanged(`Failed loading SDK JS code; error: "${errorString}"`) } Component.onCompleted: { - console.debug(`@dd WalletConnectSDK onCompleted`) var scriptSrc = SQUtils.StringUtils.readTextFile(":/app/AppLayouts/Wallet/views/walletconnect/sdk/generated/bundle.js") // Load bundle from disk if not found in resources (Storybook) if (scriptSrc === "") { @@ -96,142 +102,11 @@ WebView { } } - let htmlSrc = `` + let htmlSrc = `` - console.debug(`@dd WalletConnectSDK.loadHtml; htmlSrc len: ${htmlSrc.length}`) root.loadHtml(htmlSrc, "https://status.app") } - Timer { - id: timer - - interval: 100 - repeat: true - running: false - triggeredOnStart: true - - property int errorCount: 0 - - onTriggered: { - root.runJavaScript( - "wcResult", - function(wcResult) { - if (!wcResult) { - return - } - - let done = false - if (wcResult.error) { - console.debug(`WC JS error response - ${JSON.stringify(wcResult)}`) - done = true - if (!d.sdkReady) { - root.statusChanged(`[${timer.errorCount++}] Failed SDK init; error: ${wcResult.error}`) - } else { - root.statusChanged(`[${timer.errorCount++}] Operation error: ${wcResult.error}`) - } - } - - if (wcResult.state !== undefined) { - switch (wcResult.state) { - case RequestCodes.SdkInitSuccess: - d.sdkReady = true - root.sdkInit(true, "") - d.startListeningForEvents() - break - case RequestCodes.SdkInitError: - d.sdkReady = false - root.sdkInit(false, wcResult.error) - break - case RequestCodes.PairSuccess: - root.pairSessionProposal(true, wcResult.result) - d.getPairings() - break - case RequestCodes.PairError: - root.pairSessionProposal(false, wcResult.error) - break - case RequestCodes.ApprovePairSuccess: - root.pairAcceptedResult(true, "") - d.getPairings() - break - case RequestCodes.ApprovePairError: - root.pairAcceptedResult(false, wcResult.error) - d.getPairings() - break - case RequestCodes.RejectPairSuccess: - root.pairRejectedResult(true, "") - break - case RequestCodes.RejectPairError: - root.pairRejectedResult(false, wcResult.error) - break - case RequestCodes.AcceptSessionSuccess: - root.sessionRequestUserAnswerResult(true, "") - break - case RequestCodes.AcceptSessionError: - root.sessionRequestUserAnswerResult(true, wcResult.error) - break - case RequestCodes.RejectSessionSuccess: - root.sessionRequestUserAnswerResult(false, "") - break - case RequestCodes.RejectSessionError: - root.sessionRequestUserAnswerResult(false, wcResult.error) - break - case RequestCodes.GetPairings: - d.populatePairingsModel(wcResult.result) - break - case RequestCodes.GetPairingsError: - console.error(`WalletConnectSDK - getPairings error: ${wcResult.error}`) - break - default: { - root.statusChanged(`[${timer.errorCount++}] Unknown state: ${wcResult.state}`) - } - } - - done = true - } - - if (done) { - timer.stop() - } - } - ) - } - } - - Timer { - id: responseTimeoutTimer - - interval: 10000 - repeat: false - running: timer.running - - onTriggered: { - timer.stop() - root.responseTimeout() - } - } - - Timer { - id: eventsTimer - - interval: 100 - repeat: true - running: false - - onTriggered: { - root.runJavaScript("window.wcEventResult ? window.wcEventResult.shift() : null", function(event) { - if (event) { - switch(event.name) { - case "session_request": - root.sessionRequestEvent(event.payload) - break - default: - console.error("WC unknown event type: ", event.type) - break - } - } - }) - } - } QtObject { id: d @@ -242,12 +117,6 @@ WebView { property ListModel pairingsModel: pairings - onSdkReadyChanged: { - if (sdkReady) { - d.getPairings() - } - } - function populatePairingsModel(pairList) { pairings.clear(); for (let i = 0; i < pairList.length; i++) { @@ -259,31 +128,6 @@ WebView { } } - - function isWaitingForSdk() { - return timer.running - } - - function generateSdkCall(methodName, paramsStr, successState, errorState) { - return "wcResult = {}; try { wc." + methodName + "(" + paramsStr + ").then((callRes) => { wcResult = {state: " + successState + ", error: null, result: callRes}; }).catch((error) => { wcResult = {state: " + errorState + ", error: error}; }); } catch (e) { wcResult = {state: " + errorState + ", error: \"Exception: \" + e.message}; }; wcResult" - } - function requestSdkAsync(jsCode) { - root.runJavaScript(jsCode, - function(result) { - timer.restart() - } - ) - } - - function requestSdk(methodName, paramsStr, successState, errorState) { - const jsCode = "wcResult = {}; try { const callRes = wc." + methodName + "(" + (paramsStr ? (paramsStr) : "") + "); wcResult = {state: " + successState + ", error: null, result: callRes}; } catch (e) { wcResult = {state: " + errorState + ", error: \"Exception: \" + e.message}; }; wcResult" - root.runJavaScript(jsCode, - function(result) { - timer.restart() - } - ) - } - function startListeningForEvents() { const jsCode = " try { @@ -308,15 +152,14 @@ WebView { } } ) - eventsTimer.start() - } - - function init(projectId) { - d.requestSdkAsync(generateSdkCall("init", `"${projectId}"`, RequestCodes.SdkInitSuccess, RequestCodes.SdkInitError)) } function getPairings(projectId) { - d.requestSdk("getPairings", `null`, RequestCodes.GetPairings, RequestCodes.GetPairingsError) + root.call("wc.getPairings", "").then((result) => { + d.populatePairingsModel(result) + }).error((error) => { + console.error(`WalletConnectSDK - getPairings error: ${error}`) + }) } } diff --git a/ui/app/AppLayouts/Wallet/views/walletconnect/sdk/README.md b/ui/app/AppLayouts/Wallet/views/walletconnect/sdk/README.md index 274f695b0b..97ab334a4a 100644 --- a/ui/app/AppLayouts/Wallet/views/walletconnect/sdk/README.md +++ b/ui/app/AppLayouts/Wallet/views/walletconnect/sdk/README.md @@ -6,16 +6,7 @@ ### Design questions -- [ ] Do we report all chains and all accounts combination or let user select? - - Wallet Connect require to report all chainIDs that were requested - - Show error to user workflow. -- [ ] Can't respond to sign messages if the wallet-connect dialog/view is closed (app is minimized) - - Only apps that use deep links are expected to work seamlessly - [ ] Do we report **disabled chains**? **Update session** in case of enabled/disabled chains? -- [ ] Allow user to **disconnect session**? Manage sessions? -- [ ] Support update session if one account is added/removed? -- [ ] User awareness of session expiration? - - Support extend session? - [ ] User error workflow: retry? - [ ] Check the `Auth` request for verifyContext - [ ] What `description` and `icons` to use for the app? See `metadata` parameter in `Web3Wallet.init` call @@ -37,18 +28,6 @@ Install dependencies steps by executing commands in this directory: Use the web demo test client https://react-app.walletconnect.com/ for wallet pairing and https://react-auth-dapp.walletconnect.com/ for authentication -## Log - -Initial setup - -```sh -npm init -y -npm install --save-dev webpack webpack-cli webpack-dev-server -npm install --save @walletconnect/web3wallet -npm run build -# npm run build:dev # for development -``` - ## Dev - to be removed To test SDK loading add the following to `ui/app/mainui/AppMain.qml` @@ -75,3 +54,29 @@ StatusDialog { clip: true } ``` + +## Log + +Initial setup + +```sh +npm init -y +npm install --save-dev webpack webpack-cli webpack-dev-server +npm install --save @walletconnect/web3wallet +npm run build +# npm run build:dev # for development +``` + +- [x] Do we report all chains and all accounts combination or let user select? + - Wallet Connect require to report all chainIDs that were requested + - Answer: We only report the available chains for the current account. We will look into adding others to he same session instead of requiring a new link +- [x] Can't respond to sign messages if the wallet-connect dialog/view is closed (app is minimized) + - Only apps that use deep links are expected to work seamlessly + - Also the main workflow will be driven by user +- [x] Allow user to **disconnect session**? Manage sessions? + - Yes, in settings +- [x] Support update session if one account is added/removed? + - Not at first +- [X] User awareness of session expiration? + - Support extend session? + - Yes diff --git a/vendor/DOtherSide/lib/src/Status/AppDelegate.mm b/vendor/DOtherSide/lib/src/Status/AppDelegate.mm index 4101c0002c..1f1ac31d93 100644 --- a/vendor/DOtherSide/lib/src/Status/AppDelegate.mm +++ b/vendor/DOtherSide/lib/src/Status/AppDelegate.mm @@ -22,8 +22,7 @@ continueUserActivity:(NSUserActivity *)userActivity if (!url) return FALSE; QUrl deeplink = QUrl::fromNSURL(url); - // TODO #12434: Check if WalletConnect link and redirect the workflow - qDebug() << "@dd deeplink " << deeplink; + // TODO #12434: Check if WalletConnect link and redirect the workflow // TODO #12245: set it to nim return TRUE;