feat(wallet) quick show on how to make WebView-QML bridge robust
Updates: #12639
This commit is contained in:
parent
783a755230
commit
1c2c95288a
1
Makefile
1
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -60,6 +60,7 @@
|
|||
<file>StatusQ/Components/StatusVideo.qml</file>
|
||||
<file>StatusQ/Components/StatusWizardStepper.qml</file>
|
||||
<file>StatusQ/Components/StatusSortableColumnHeader.qml</file>
|
||||
<file>StatusQ/Components/StatusWebView.qml</file>
|
||||
<file>StatusQ/Controls/Validators/qmldir</file>
|
||||
<file>StatusQ/Controls/Validators/StatusAddressOrEnsValidator.qml</file>
|
||||
<file>StatusQ/Controls/Validators/StatusAddressValidator.qml</file>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
<script>
|
||||
window.asyncFunction = function (param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => resolve(param), 10);
|
||||
});
|
||||
}
|
||||
|
||||
window.asyncRejectFunction = function(param) {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(param), 1);
|
||||
});
|
||||
}
|
||||
|
||||
window.syncFunction = function(param) {
|
||||
return param;
|
||||
}
|
||||
|
||||
window.syncFunction = function(param) {
|
||||
return param;
|
||||
}
|
||||
|
||||
window.syncFunctionThrows = function(param) {
|
||||
throw new Error("Test exception");
|
||||
return param;
|
||||
}
|
||||
|
||||
window.sendEvent = function(param) {
|
||||
document.dispatchEvent(new CustomEvent("testEvent", { detail: param }));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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");
|
||||
// }
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
---
|
|
@ -1,3 +0,0 @@
|
|||
Start testing: Apr 18 18:49 CEST
|
||||
----------------------------------------------------------
|
||||
End testing: Apr 18 18:49 CEST
|
|
@ -3,6 +3,18 @@
|
|||
|
||||
#include "TestHelpers/MonitorQtOutput.h"
|
||||
|
||||
#include <QtWebView>
|
||||
|
||||
class RunBeforeQApplicationIsInitialized {
|
||||
public:
|
||||
RunBeforeQApplicationIsInitialized()
|
||||
{
|
||||
QtWebView::initialize();
|
||||
}
|
||||
};
|
||||
|
||||
static RunBeforeQApplicationIsInitialized runBeforeQApplicationIsInitialized;
|
||||
|
||||
class TestSetup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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: `<!DOCTYPE html><html><head><title>TODO: Test</title>\n<!--<script src="http://127.0.0.1:8080/bundle.js" defer></script>-->\n<script type='text/javascript'>\n console.log("@dd loaded dummy script!")\n</script>\n</head><body style='background-color: ${root.backgroundColor.toString()};'></body></html>`
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -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(`<font color="red">Failed loading SDK JS code; error: "${loadRequest.errorString}"</font>`)
|
||||
break
|
||||
case WebView.LoadStartedStatus:
|
||||
// case WebEngineView.LoadStartedStatus:
|
||||
root.statusChanged(`<font color="blue">Loading SDK JS code</font>`)
|
||||
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(`<font color="red">Failed loading SDK JS code; error: "${errorString}"</font>`)
|
||||
}
|
||||
|
||||
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 = `<!DOCTYPE html><html><head><!--<title>TODO: Test</title>--><script type='text/javascript'>${scriptSrc}</script></head><body style='background-color: ${root.backgroundColor.toString()};'></body></html>`
|
||||
let htmlSrc = `<!DOCTYPE html><html><head><script type='text/javascript'>${scriptSrc}</script></head><body style='background-color: ${root.backgroundColor.toString()};'></body></html>`
|
||||
|
||||
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(`<font color="red">[${timer.errorCount++}] Failed SDK init; error: ${wcResult.error}</font>`)
|
||||
} else {
|
||||
root.statusChanged(`<font color="red">[${timer.errorCount++}] Operation error: ${wcResult.error}</font>`)
|
||||
}
|
||||
}
|
||||
|
||||
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(`<font color="red">[${timer.errorCount++}] Unknown state: ${wcResult.state}</font>`)
|
||||
}
|
||||
}
|
||||
|
||||
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}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <https://docs.walletconnect.com/web3wallet/verify>
|
||||
- [ ] 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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue