diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index bdcbb783f30..a4a2834310a 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -2,6 +2,11 @@ cmake_minimum_required(VERSION 3.19) project(StatusQ) +# Enable Objective-C++ for Apple platforms (needed for CustomWebViewLib) +if(APPLE) + enable_language(OBJCXX) +endif() + option(STATUSQ_BUILD_SANITY_CHECKER "Enable to build StatusQ Sanity Checker application" ON) option(STATUSQ_BUILD_TESTS "Enable to build StatusQ UI auto tests" ON) option(STATUSQ_STATIC_LIB "Enable to build StatusQ as a static library" OFF) @@ -37,7 +42,7 @@ endif() find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS - Core Qml Gui Quick QuickControls2 REQUIRED) + Core Qml Gui Quick QuickControls2 WebChannel REQUIRED) # --- QML test hooks flag and TestConfig generation --- option(STATUSQ_TESTMODE "Enable QML test hooks (TestConfig.testMode)" OFF) @@ -210,6 +215,13 @@ add_library(StatusQ ${LIB_TYPE} src/l10n/languagemodel.cpp src/l10n/languageservice.h src/l10n/languageservice.cpp + + # CustomWebView + src/CustomWebView/nativewebview.h + src/CustomWebView/nativewebview.cpp + src/CustomWebView/nativewebviewbackend.h + src/CustomWebView/nativewebviewtransport.h + src/CustomWebView/nativewebviewtransport.cpp ) target_compile_features(StatusQ PRIVATE cxx_std_17) @@ -219,19 +231,26 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") find_library(Foundation Foundation) find_library(Security Security) find_library(LocalAuthentication LocalAuthentication) - target_link_libraries(${PROJECT_NAME} PRIVATE ${AppKit} ${Foundation} ${Security} ${LocalAuthentication}) + find_library(WebKit WebKit) + target_link_libraries(${PROJECT_NAME} PRIVATE ${AppKit} ${Foundation} ${Security} ${LocalAuthentication} ${WebKit}) target_sources(StatusQ PRIVATE src/keychain_apple.mm + src/CustomWebView/darwinwebviewbackend.mm ) elseif (${CMAKE_SYSTEM_NAME} MATCHES "iOS") + find_library(WebKit WebKit) + find_library(UIKit UIKit) + target_link_libraries(${PROJECT_NAME} PRIVATE ${WebKit} ${UIKit}) target_sources(StatusQ PRIVATE src/ios_utils.mm src/keychain_apple.mm + src/CustomWebView/darwinwebviewbackend.mm ) elseif (${CMAKE_SYSTEM_NAME} MATCHES "Android") target_sources(StatusQ PRIVATE src/keychain_android.cpp src/safutils_android.cpp + src/CustomWebView/androidwebviewbackend.cpp ) else () target_sources(StatusQ PRIVATE @@ -262,6 +281,7 @@ target_link_libraries(StatusQ PRIVATE Qt::Gui Qt::Quick Qt::QuickControls2 + Qt::WebChannel SortFilterProxyModel QtModelsToolkit MobileUI diff --git a/ui/StatusQ/src/CustomWebView/androidwebviewbackend.cpp b/ui/StatusQ/src/CustomWebView/androidwebviewbackend.cpp new file mode 100644 index 00000000000..ec871d2b736 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/androidwebviewbackend.cpp @@ -0,0 +1,366 @@ +// Uses Android WebView with JavaScriptInterface for WebChannel IPC +#if defined(__ANDROID__) + +#include "nativewebviewbackend.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +// Forward declarations for JNI types +Q_DECLARE_JNI_CLASS(WebView, "android/webkit/WebView") +Q_DECLARE_JNI_CLASS(WebViewController, "org/qtproject/qt/android/webview/QtAndroidWebViewController") +Q_DECLARE_JNI_CLASS(QtBridge, "org/qtproject/qt/android/webview/QtBridge") + +using namespace QtJniTypes; + +// Global registry for tracking instances (for JNI callbacks) +typedef QSet WebViewBackends; +Q_GLOBAL_STATIC(WebViewBackends, g_webViewBackends) + +class AndroidWebViewBackend : public NativeWebViewBackend +{ + Q_OBJECT + +public: + explicit AndroidWebViewBackend(QObject *parent = nullptr) + : NativeWebViewBackend(parent) + , m_viewController(nullptr) + , m_webView(nullptr) + { + // QtAndroidWebViewController constructor blocks the Qt GUI thread until + // the WebView is created and configured in the UI thread. + while (!QtAndroidPrivate::acquireAndroidDeadlockProtector()) { + auto eventDispatcher = QThread::currentThread()->eventDispatcher(); + if (eventDispatcher) { + eventDispatcher->processEvents( + QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); + } + } + + m_viewController = WebViewController( + QtAndroidPrivate::activity(), + reinterpret_cast(this) + ); + + QtAndroidPrivate::releaseAndroidDeadlockProtector(); + + m_webView = m_viewController.callMethod("getWebView"); + + g_webViewBackends->insert(this); + + connect(qApp, &QGuiApplication::applicationStateChanged, + this, &AndroidWebViewBackend::onApplicationStateChanged); + + qDebug() << "AndroidWebViewBackend: Created"; + } + + ~AndroidWebViewBackend() override + { + g_webViewBackends->remove(this); + m_viewController.callMethod("destroy"); + } + + void loadUrl(const QUrl &url) override + { + m_viewController.callMethod("loadUrl", url.toString()); + } + + void loadHtml(const QString &html, const QUrl &baseUrl) override + { + const QString mimeType = QStringLiteral("text/html;charset=UTF-8"); + + if (baseUrl.isEmpty() || baseUrl.scheme() == QLatin1String("data")) { + const QString encoded = QUrl::toPercentEncoding(html); + m_viewController.callMethod("loadData", encoded, mimeType, jstring(nullptr)); + } else { + m_viewController.callMethod("loadDataWithBaseURL", + baseUrl.toString(), html, mimeType, + jstring(nullptr), jstring(nullptr)); + } + } + + void* nativeHandle() const override + { + return m_webView.object(); + } + + void runJavaScript(const QString &script) override + { + m_viewController.callMethod("runJavaScript", script, jlong(-1)); + } + + bool installMessageBridge(const QString &ns, + const QStringList &allowedOrigins, + const QString &invokeKey, + const QString &webChannelScriptPath = QString(), + const QStringList &userScripts = QStringList()) override + { + m_bridgeNs = ns; + m_allowedOrigins = allowedOrigins; + m_invokeKey = invokeKey; + + // Load qwebchannel.js - try user-provided path first, then fallback + QString qwebchannelJs; + QStringList paths; + if (!webChannelScriptPath.isEmpty()) { + paths << webChannelScriptPath; + } + paths << QStringLiteral(":/qtwebchannel/qwebchannel.js"); + + for (const QString &path : paths) { + QFile file(path); + if (file.open(QIODevice::ReadOnly)) { + qwebchannelJs = QString::fromUtf8(file.readAll()); + qDebug() << "AndroidWebViewBackend: Loaded qwebchannel.js from" << path; + break; + } + } + if (qwebchannelJs.isEmpty()) { + qWarning() << "AndroidWebViewBackend: Failed to load qwebchannel.js from paths:" << paths; + } + + // Load user scripts from resources + QString userScriptsContent; + for (const QString &scriptPath : userScripts) { + QFile scriptFile(scriptPath); + if (scriptFile.open(QIODevice::ReadOnly)) { + userScriptsContent += QString::fromUtf8(scriptFile.readAll()) + QStringLiteral("\n"); + qDebug() << "AndroidWebViewBackend: Loaded user script from" << scriptPath; + } else { + qWarning() << "AndroidWebViewBackend: Failed to load user script:" << scriptPath; + } + } + + // Generate bootstrap script + // Post hook for Android - uses qtbridge.postMessage (WebMessageListener) or QtBridge.postMessage (fallback) + QString postHook = QString::fromLatin1( + "function(pkt){" + " if(window.qtbridge && window.qtbridge.postMessage) {" + " window.qtbridge.postMessage(pkt);" + " } else if(window.QtBridge && window.QtBridge.postMessage) {" + " window.QtBridge.postMessage(pkt);" + " }" + "}"); + + QString bootstrapJs = generateBootstrapScript(ns, invokeKey, postHook); + + // Install via QtBridge Java helper + QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=, this]() { + if (!m_javaBridge.isValid()) { + m_javaBridge = QJniObject("org/qtproject/qt/android/webview/QtBridge", + "(JLjava/lang/Object;)V", + jlong(this), + m_webView.object()); + } + + m_javaBridge.callMethod("installBridge", + QJniObject::fromString(qwebchannelJs).object(), + QJniObject::fromString(bootstrapJs).object(), + QJniObject::fromString(m_bridgeNs).object(), + QJniObject::fromString(m_allowedOrigins.join(u',')).object(), + QJniObject::fromString(userScriptsContent).object()); + }); + + qDebug() << "AndroidWebViewBackend: Message bridge installed with namespace:" << ns; + return true; + } + + void postMessageToJavaScript(const QString &json) override + { + // Deliver to transport.onmessage + QString deliverScript = QString::fromLatin1( + "(function(ns, msg) {" + " var t = window[ns] && window[ns].webChannelTransport;" + " if (t && typeof t.onmessage === 'function') {" + " t.onmessage({data: msg});" + " }" + "})('%1', %2);") + .arg(m_bridgeNs, json); + + m_viewController.callMethod("runJavaScript", + QJniObject::fromString(deliverScript).object(), + jlong(-1)); + } + + void setupInItem(QQuickItem *item) override + { + if (!item) return; + + QQuickWindow *window = item->window(); + if (!window) { + qWarning() << "AndroidWebViewBackend: No window available"; + return; + } + + // On Android, the WebView is managed by the system + // We just need to make sure our window is set as parent + // The actual parenting is handled by Qt's Android platform plugin + + qDebug() << "AndroidWebViewBackend: WebView setup in item"; + } + + void updateGeometry(QQuickItem *item) override + { + if (!item) return; + + // On Android, geometry is handled through Qt's platform integration + // The WebViewController manages the native view positioning + + QPointF pos = item->mapToScene(QPointF(0, 0)); + QRectF rect(pos.x(), pos.y(), item->width(), item->height()); + + // Call into Java to update geometry if needed + // This is typically handled automatically by Qt's Android embedding + } + + // Called from JNI when a message is received from JavaScript + void onMessageFromJavaScript(const QString &envelope, + const QString &origin, + bool isMainFrame) + { + emit webMessageReceived(envelope, origin, isMainFrame); + } + + // Called from JNI on page events + void onPageStarted(const QUrl &url) + { + emit loadStarted(); + emit loadingChanged(true); + emit urlChanged(url); + } + + void onPageFinished(const QUrl &url) + { + emit loadingChanged(false); + emit urlChanged(url); + } + +private slots: + void onApplicationStateChanged(Qt::ApplicationState state) + { + if (state == Qt::ApplicationActive) { + m_viewController.callMethod("onResume"); + } else { + m_viewController.callMethod("onPause"); + } + } + +private: + QString generateBootstrapScript(const QString &ns, + const QString &invokeKey, + const QString &postHook) + { + return QString::fromLatin1( + "(function(ns, key) {" + " window[ns] = window[ns] || {};" + " window[ns].__qtbridge_postMessage = %3;" + " var t = window[ns].webChannelTransport = {" + " send: function(msg) {" + " var pkt = JSON.stringify({" + " origin: (location.origin || 'null')," + " invokeKey: key," + " data: String(msg)" + " });" + " window[ns].__qtbridge_postMessage(pkt);" + " }," + " onmessage: null" + " };" + "})('%1', '%2');") + .arg(ns, invokeKey, postHook); + } + + WebViewController m_viewController; + WebView m_webView; + QJniObject m_javaBridge; + QString m_bridgeNs; + QStringList m_allowedOrigins; + QString m_invokeKey; +}; + +// ===== JNI Callbacks ===== + +static void c_onBridgeMessage(JNIEnv *env, + jclass thiz, + jlong nativePtr, + jstring envelope, + jstring origin, + jboolean isMainFrame) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + AndroidWebViewBackend *backend = reinterpret_cast(nativePtr); + if (!g_webViewBackends->contains(backend)) + return; + + QString qEnvelope = QJniObject(envelope).toString(); + QString qOrigin = QJniObject(origin).toString(); + + // Post to Qt event loop + QMetaObject::invokeMethod(backend, [=]() { + backend->onMessageFromJavaScript(qEnvelope, qOrigin, isMainFrame); + }, Qt::QueuedConnection); +} + +static void c_onPageStarted(JNIEnv *env, + jclass thiz, + jlong nativePtr, + jstring url) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + AndroidWebViewBackend *backend = reinterpret_cast(nativePtr); + if (!g_webViewBackends->contains(backend)) + return; + + QUrl qUrl(QJniObject(url).toString()); + + QMetaObject::invokeMethod(backend, [=]() { + backend->onPageStarted(qUrl); + }, Qt::QueuedConnection); +} + +static void c_onPageFinished(JNIEnv *env, + jclass thiz, + jlong nativePtr, + jstring url) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + AndroidWebViewBackend *backend = reinterpret_cast(nativePtr); + if (!g_webViewBackends->contains(backend)) + return; + + QUrl qUrl(QJniObject(url).toString()); + + QMetaObject::invokeMethod(backend, [=]() { + backend->onPageFinished(qUrl); + }, Qt::QueuedConnection); +} + +// Factory function implementation for Android +NativeWebViewBackend* createPlatformBackend(QObject *parent) +{ + return new AndroidWebViewBackend(parent); +} + +#include "androidwebviewbackend.moc" + +QT_END_NAMESPACE + +#endif // __ANDROID__ + diff --git a/ui/StatusQ/src/CustomWebView/darwinwebviewbackend.mm b/ui/StatusQ/src/CustomWebView/darwinwebviewbackend.mm new file mode 100644 index 00000000000..9a05c44d890 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/darwinwebviewbackend.mm @@ -0,0 +1,423 @@ +// Uses WKWebView with WKUserContentController for WebChannel IPC + +#if defined(__APPLE__) +#include +#endif + +#if defined(__APPLE__) + +#include "nativewebviewbackend.h" + +#include +#include +#include +#include +#include + +#import + +#ifdef Q_OS_IOS +#import +typedef UIView PlatformView; +#else +#import +typedef NSView PlatformView; +#endif + +// Forward declarations +@class QtBridgeHandler; + +// ===== QtBridgeHandler ===== +// WKScriptMessageHandler implementation for receiving messages from JavaScript + +@interface QtBridgeHandler : NSObject +@property (nonatomic, assign) NativeWebViewBackend *owner; +@end + +@implementation QtBridgeHandler + +- (void)userContentController:(WKUserContentController *)userContentController + didReceiveScriptMessage:(WKScriptMessage *)message +{ + Q_UNUSED(userContentController); + + NSLog(@"QtBridgeHandler: Received message from JS, name=%@", message.name); + + if (!self.owner) { + NSLog(@"QtBridgeHandler: No owner!"); + return; + } + + // Extract message body + NSString *body = [message.body isKindOfClass:[NSString class]] + ? (NSString *)message.body + : [NSString stringWithFormat:@"%@", message.body]; + + NSLog(@"QtBridgeHandler: Body=%@", [body substringToIndex:MIN(200, body.length)]); + + // Determine if this is the main frame + BOOL isMainFrame = message.frameInfo && message.frameInfo.isMainFrame; + + NSLog(@"QtBridgeHandler: Emitting webMessageReceived signal"); + + // Extract origin from security origin + NSString *origin = @""; + if (message.frameInfo && message.frameInfo.securityOrigin) { + WKSecurityOrigin *so = message.frameInfo.securityOrigin; + if (so.port > 0) { + origin = [NSString stringWithFormat:@"%@://%@:%ld", so.protocol, so.host, (long)so.port]; + } else { + origin = [NSString stringWithFormat:@"%@://%@", so.protocol, so.host]; + } + } + + // Emit signal to Qt + Q_EMIT self.owner->webMessageReceived( + QString::fromNSString(body), + QString::fromNSString(origin), + isMainFrame + ); +} + +@end + +// ===== QtNavigationDelegate ===== +// WKNavigationDelegate for tracking loading state + +@interface QtNavigationDelegate : NSObject +@property (nonatomic, assign) NativeWebViewBackend *owner; +@end + +@implementation QtNavigationDelegate + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation +{ + Q_UNUSED(webView); + Q_UNUSED(navigation); + + if (self.owner) { + Q_EMIT self.owner->loadStarted(); + Q_EMIT self.owner->loadingChanged(true); + } +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation +{ + Q_UNUSED(navigation); + + if (self.owner) { + Q_EMIT self.owner->loadingChanged(false); + Q_EMIT self.owner->urlChanged(QUrl::fromNSURL(webView.URL)); + } +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + Q_UNUSED(webView); + Q_UNUSED(navigation); + Q_UNUSED(error); + + if (self.owner) { + Q_EMIT self.owner->loadingChanged(false); + } +} + +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + Q_UNUSED(webView); + Q_UNUSED(navigation); + Q_UNUSED(error); + + if (self.owner) { + Q_EMIT self.owner->loadingChanged(false); + } +} + +@end + +// ===== DarwinWebViewBackend ===== +// Platform-specific implementation using WKWebView + +class DarwinWebViewBackend : public NativeWebViewBackend +{ + Q_OBJECT + +public: + explicit DarwinWebViewBackend(QObject *parent = nullptr) + : NativeWebViewBackend(parent) + , m_webView(nil) + , m_bridgeHandler(nil) + , m_navigationDelegate(nil) + { + // Create WKWebView configuration + WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; + + // Enable developer extras for debugging + [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; + + // Create WebView + CGRect frame = CGRectMake(0, 0, 400, 400); + m_webView = [[WKWebView alloc] initWithFrame:frame configuration:config]; + + // Set up navigation delegate + m_navigationDelegate = [[QtNavigationDelegate alloc] init]; + m_navigationDelegate.owner = this; + m_webView.navigationDelegate = m_navigationDelegate; + + qDebug() << "DarwinWebViewBackend: Created WKWebView"; + } + + ~DarwinWebViewBackend() override + { + if (m_webView) { + [m_webView stopLoading]; + [m_webView removeFromSuperview]; + m_webView.navigationDelegate = nil; + m_webView = nil; + } + m_bridgeHandler = nil; + m_navigationDelegate = nil; + } + + void loadUrl(const QUrl &url) override + { + if (!m_webView) return; + + NSURL *nsUrl = url.toNSURL(); + NSURLRequest *request = [NSURLRequest requestWithURL:nsUrl]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [m_webView loadRequest:request]; + }); + } + + void loadHtml(const QString &html, const QUrl &baseUrl) override + { + if (!m_webView) return; + + NSString *nsHtml = html.toNSString(); + NSURL *nsBaseUrl = baseUrl.isValid() ? baseUrl.toNSURL() : nil; + + dispatch_async(dispatch_get_main_queue(), ^{ + [m_webView loadHTMLString:nsHtml baseURL:nsBaseUrl]; + }); + } + + void* nativeHandle() const override + { + return (__bridge void *)m_webView; + } + + void runJavaScript(const QString &script) override + { + if (!m_webView) return; + + NSString *nsScript = script.toNSString(); + + dispatch_async(dispatch_get_main_queue(), ^{ + [m_webView evaluateJavaScript:nsScript completionHandler:^(id result, NSError *error) { + if (error) { + qWarning() << "DarwinWebViewBackend: JavaScript error:" + << QString::fromNSString(error.localizedDescription); + } + }]; + }); + } + + bool installMessageBridge(const QString &ns, + const QStringList &allowedOrigins, + const QString &invokeKey, + const QString &webChannelScriptPath = QString(), + const QStringList &userScripts = QStringList()) override + { + Q_UNUSED(allowedOrigins); // Security enforced in transport layer + + if (!m_webView) return false; + + m_bridgeNs = ns; + m_invokeKey = invokeKey; + + WKUserContentController *ucc = m_webView.configuration.userContentController; + + // Remove previous handlers and scripts (fresh state for new navigation) + [ucc removeAllScriptMessageHandlers]; + [ucc removeAllUserScripts]; + + // Create and register message handler + m_bridgeHandler = [[QtBridgeHandler alloc] init]; + m_bridgeHandler.owner = this; + [ucc addScriptMessageHandler:m_bridgeHandler name:@"qtbridge"]; + + // Load and inject qwebchannel.js + // Try user-provided path first, then fallback paths + QStringList qwcPaths; + if (!webChannelScriptPath.isEmpty()) { + qwcPaths << webChannelScriptPath; + } + qwcPaths << QStringLiteral(":/qtwebchannel/qwebchannel.js"); + + bool loaded = false; + for (const QString &qwcPath : qwcPaths) { + QFile qwcFile(qwcPath); + if (qwcFile.open(QIODevice::ReadOnly)) { + QString qwcSource = QString::fromUtf8(qwcFile.readAll()); + NSString *nsQwc = qwcSource.toNSString(); + WKUserScript *qwcScript = [[WKUserScript alloc] + initWithSource:nsQwc + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + [ucc addUserScript:qwcScript]; + qDebug() << "DarwinWebViewBackend: Injected qwebchannel.js from" << qwcPath; + loaded = true; + break; + } + } + + if (!loaded) { + qWarning() << "DarwinWebViewBackend: Failed to load qwebchannel.js from any path:" << qwcPaths; + } + + // Generate and inject bootstrap script + QString bootstrap = generateBootstrapScript(ns, invokeKey); + NSString *nsBootstrap = bootstrap.toNSString(); + WKUserScript *bootScript = [[WKUserScript alloc] + initWithSource:nsBootstrap + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + [ucc addUserScript:bootScript]; + + // Inject user scripts from resources (AtDocumentStart for EIP-1193 provider availability) + for (const QString &scriptPath : userScripts) { + QFile scriptFile(scriptPath); + if (scriptFile.open(QIODevice::ReadOnly)) { + QString scriptSource = QString::fromUtf8(scriptFile.readAll()); + NSString *nsScript = scriptSource.toNSString(); + WKUserScript *userScript = [[WKUserScript alloc] + initWithSource:nsScript + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:YES]; + [ucc addUserScript:userScript]; + qDebug() << "DarwinWebViewBackend: Injected user script from" << scriptPath; + } else { + qWarning() << "DarwinWebViewBackend: Failed to load user script:" << scriptPath; + } + } + + qDebug() << "DarwinWebViewBackend: Message bridge installed with namespace:" << ns; + return true; + } + + void postMessageToJavaScript(const QString &json) override + { + if (!m_webView) return; + + // Generate JavaScript code to deliver the message + QString deliverScript = QString::fromLatin1( + "(function(ns, msg) {" + " var t = window[ns] && window[ns].webChannelTransport;" + " if (t && typeof t.onmessage === 'function') {" + " t.onmessage({data: msg});" + " }" + "})('%1', %2);") + .arg(m_bridgeNs, json); + + NSString *nsScript = deliverScript.toNSString(); + + dispatch_async(dispatch_get_main_queue(), ^{ + [m_webView evaluateJavaScript:nsScript completionHandler:nil]; + }); + } + + void setupInItem(QQuickItem *item) override + { + if (!m_webView || !item) return; + + QQuickWindow *window = item->window(); + if (!window) { + qWarning() << "DarwinWebViewBackend: No window available"; + return; + } + +#ifdef Q_OS_IOS + UIView *hostView = (__bridge UIView *)reinterpret_cast(window->winId()); +#else + NSView *hostView = (__bridge NSView *)reinterpret_cast(window->winId()); +#endif + + if (!hostView) { + qWarning() << "DarwinWebViewBackend: Could not get native view from window"; + return; + } + + // Add WebView as subview + [hostView addSubview:m_webView]; + + qDebug() << "DarwinWebViewBackend: WebView added to window"; + } + + void updateGeometry(QQuickItem *item) override + { + if (!m_webView || !item) return; + + QQuickWindow *window = item->window(); + if (!window) return; + + // Convert QML coordinates to native coordinates + QPointF pos = item->mapToScene(QPointF(0, 0)); + QRectF rect(pos.x(), pos.y(), item->width(), item->height()); + +#ifdef Q_OS_IOS + // iOS uses top-left origin (same as Qt) + CGRect frame = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); +#else + // macOS uses bottom-left origin + qreal windowHeight = window->height(); + CGFloat y = windowHeight - rect.y() - rect.height(); + CGRect frame = CGRectMake(rect.x(), y, rect.width(), rect.height()); +#endif + + [m_webView setFrame:frame]; + } + +private: + QString generateBootstrapScript(const QString &ns, const QString &invokeKey) + { + // This script sets up the WebChannel transport on the JS side + return QString::fromLatin1( + "(function(ns, key) {" + " window[ns] = window[ns] || {};" + " window[ns].__qtbridge_postMessage = function(pkt) {" + " webkit.messageHandlers.qtbridge.postMessage(pkt);" + " };" + " var t = window[ns].webChannelTransport = {" + " send: function(msg) {" + " var pkt = JSON.stringify({" + " origin: (location.origin || 'null')," + " invokeKey: key," + " data: String(msg)" + " });" + " window[ns].__qtbridge_postMessage(pkt);" + " }," + " onmessage: null" + " };" + "})('%1', '%2');") + .arg(ns, invokeKey); + } + + WKWebView *m_webView; + QtBridgeHandler *m_bridgeHandler; + QtNavigationDelegate *m_navigationDelegate; + QString m_bridgeNs; + QString m_invokeKey; +}; + +// Factory function implementation for Darwin +NativeWebViewBackend* createPlatformBackend(QObject *parent) +{ + return new DarwinWebViewBackend(parent); +} + +#include "darwinwebviewbackend.moc" + +#endif // __APPLE__ + diff --git a/ui/StatusQ/src/CustomWebView/nativewebview.cpp b/ui/StatusQ/src/CustomWebView/nativewebview.cpp new file mode 100644 index 00000000000..69655873631 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/nativewebview.cpp @@ -0,0 +1,318 @@ +#include "nativewebview.h" +#include "nativewebviewbackend.h" +#include "nativewebviewtransport.h" + +#include +#include +#include +#include + +NativeWebView::NativeWebView(QQuickItem *parent) + : QQuickItem(parent) + , m_backend(nullptr) + , m_channel(nullptr) + , m_transport(nullptr) + , m_namespace(QStringLiteral("qt")) + , m_loading(false) + , m_viewSetup(false) +{ + setFlag(ItemHasContents, true); + + // Create platform-specific backend + m_backend = createPlatformBackend(this); + + if (m_backend) { + // Connect backend signals + connect(m_backend, &NativeWebViewBackend::webMessageReceived, + this, &NativeWebView::onWebMessageReceived); + connect(m_backend, &NativeWebViewBackend::loadStarted, + this, &NativeWebView::onLoadStarted); + connect(m_backend, &NativeWebViewBackend::loadingChanged, + this, &NativeWebView::onLoadingChanged); + connect(m_backend, &NativeWebViewBackend::urlChanged, + this, &NativeWebView::onBackendUrlChanged); + } + + qDebug() << "NativeWebView: Created"; +} + +NativeWebView::~NativeWebView() +{ + // Backend and transport are children, will be deleted automatically +} + +void NativeWebView::setHtmlContent(const QString &html) +{ + if (m_htmlContent == html) + return; + + m_htmlContent = html; + emit htmlContentChanged(); + + loadHtml(html, QUrl(QStringLiteral("http://localhost"))); +} + +void NativeWebView::setUrl(const QUrl &url) +{ + if (m_url == url) + return; + + m_url = url; + emit urlChanged(); + + loadUrl(url); +} + +void NativeWebView::setWebChannel(QWebChannel *channel) +{ + qDebug() << "NativeWebView::setWebChannel called, channel=" << channel; + + if (m_channel == channel) + return; + + m_channel = channel; + + // Create transport if needed + if (m_channel && !m_transport) { + qDebug() << "NativeWebView: Creating transport"; + m_transport = new NativeWebViewTransport(this, m_namespace, this); + m_transport->setAllowedOrigins(m_allowedOrigins); + } + + // Connect channel to transport + if (m_channel && m_transport) { + qDebug() << "NativeWebView: Connecting channel to transport"; + m_channel->connectTo(m_transport); + } + + // Install bridge if we don't have an invoke key yet + if (m_invokeKey.isEmpty()) { + installWebChannelBridge(); + } + + emit webChannelChanged(); +} + +void NativeWebView::setWebChannelNamespace(const QString &ns) +{ + QString newNs = ns.isEmpty() ? QStringLiteral("qt") : ns; + + if (m_namespace == newNs) + return; + + m_namespace = newNs; + + // Reinstall bridge with new namespace + if (m_channel) { + installWebChannelBridge(); + } + + emit webChannelNamespaceChanged(); +} + +void NativeWebView::setAllowedOrigins(const QStringList &origins) +{ + if (m_allowedOrigins == origins) + return; + + m_allowedOrigins = origins; + + if (m_transport) { + m_transport->setAllowedOrigins(origins); + } + + // Reinstall bridge with new origins + if (m_channel) { + installWebChannelBridge(); + } + + emit allowedOriginsChanged(); +} + +void NativeWebView::setWebChannelScriptPath(const QString &path) +{ + if (m_webChannelScriptPath == path) + return; + + m_webChannelScriptPath = path; + + // Reinstall bridge with new script path + if (m_channel) { + installWebChannelBridge(); + } + + emit webChannelScriptPathChanged(); +} + +void NativeWebView::setUserScripts(const QStringList &scripts) +{ + if (m_userScripts == scripts) + return; + + m_userScripts = scripts; + + // Reinstall bridge with new user scripts + if (m_channel) { + installWebChannelBridge(); + } + + emit userScriptsChanged(); +} + +void NativeWebView::loadHtml(const QString &html, const QUrl &baseUrl) +{ + qDebug() << "NativeWebView::loadHtml, baseUrl:" << baseUrl; + + if (m_backend) { + // Install bridge BEFORE loading content + generateNewInvokeKey(); + installWebChannelBridge(); + + m_backend->loadHtml(html, baseUrl); + } else { + qWarning() << "NativeWebView: No backend available"; + } +} + +void NativeWebView::loadUrl(const QUrl &url) +{ + qDebug() << "NativeWebView::loadUrl:" << url; + + if (m_backend) { + // Install bridge BEFORE loading content + generateNewInvokeKey(); + installWebChannelBridge(); + + m_backend->loadUrl(url); + } else { + qWarning() << "NativeWebView: No backend available"; + } +} + +void NativeWebView::runJavaScript(const QString &script) +{ + if (m_backend) { + m_backend->runJavaScript(script); + } +} + +void NativeWebView::postMessageToJavaScript(const QString &json) +{ + if (m_backend) { + m_backend->postMessageToJavaScript(json); + } +} + +void NativeWebView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateNativeViewGeometry(); +} + +void NativeWebView::itemChange(ItemChange change, const ItemChangeData &value) +{ + QQuickItem::itemChange(change, value); + + if (change == ItemSceneChange && value.window) { + setupNativeView(); + } +} + +void NativeWebView::onWebMessageReceived(const QString &message, + const QString &origin, + bool isMainFrame) +{ + qDebug() << "NativeWebView::onWebMessageReceived:" << message.left(100); + + // Route message to transport for QWebChannel + if (m_transport) { + m_transport->handleJsEnvelope(message, origin, isMainFrame); + } else { + qWarning() << "NativeWebView: No transport available!"; + } +} + +void NativeWebView::onLoadStarted() +{ + // Bridge is now installed before loadHtml/loadUrl + // This handler is kept for potential future use +} + +void NativeWebView::onLoadingChanged(bool loading) +{ + if (m_loading == loading) + return; + + m_loading = loading; + emit loadingChanged(); +} + +void NativeWebView::onBackendUrlChanged(const QUrl &url) +{ + if (m_url == url) + return; + + m_url = url; + emit urlChanged(); +} + +void NativeWebView::setupNativeView() +{ + if (m_viewSetup) + return; + + QQuickWindow *qmlWindow = window(); + if (!qmlWindow) { + qWarning() << "NativeWebView: No window available"; + return; + } + + if (m_backend) { + m_backend->setupInItem(this); + m_viewSetup = true; + + updateNativeViewGeometry(); + + qDebug() << "NativeWebView: Native view set up"; + emit bridgeReady(); + } +} + +void NativeWebView::updateNativeViewGeometry() +{ + if (!m_viewSetup || !m_backend) + return; + + m_backend->updateGeometry(this); +} + +void NativeWebView::installWebChannelBridge() +{ + if (!m_backend) + return; + + // Generate invoke key if not set + if (m_invokeKey.isEmpty()) { + generateNewInvokeKey(); + } + + // Update transport with current key + if (m_transport) { + m_transport->setInvokeKey(m_invokeKey); + } + + // Install bridge in backend + m_backend->installMessageBridge(m_namespace, m_allowedOrigins, m_invokeKey, m_webChannelScriptPath, m_userScripts); + + qDebug() << "NativeWebView: WebChannel bridge installed"; +} + +void NativeWebView::generateNewInvokeKey() +{ + m_invokeKey = QUuid::createUuid().toString(QUuid::WithoutBraces); + + if (m_transport) { + m_transport->setInvokeKey(m_invokeKey); + } +} + diff --git a/ui/StatusQ/src/CustomWebView/nativewebview.h b/ui/StatusQ/src/CustomWebView/nativewebview.h new file mode 100644 index 00000000000..ac13edb635f --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/nativewebview.h @@ -0,0 +1,146 @@ +#ifndef NATIVEWEBVIEW_H +#define NATIVEWEBVIEW_H + +#include +#include +#include +#include +#include +class NativeWebViewBackend; +class NativeWebViewTransport; + +/** + * @brief QML component for displaying web content using native platform WebView. + * + * This component provides a platform-independent interface to native WebViews + * (WKWebView on Darwin, android.webkit.WebView on Android) with support for + * QWebChannel communication. + * + * Example usage in QML: + * @code + * import QtWebChannel 1.0 + * + * NativeWebView { + * id: webView + * anchors.fill: parent + * url: "https://example.com" + * + * webChannel: WebChannel { + * registeredObjects: [myObject] + * } + * } + * @endcode + */ +class NativeWebView : public QQuickItem +{ + Q_OBJECT + QML_NAMED_ELEMENT(NativeWebView) + + // Content properties + Q_PROPERTY(QString htmlContent READ htmlContent WRITE setHtmlContent NOTIFY htmlContentChanged) + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + + // WebChannel properties + Q_PROPERTY(QWebChannel* webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged) + Q_PROPERTY(QString webChannelNamespace READ webChannelNamespace WRITE setWebChannelNamespace NOTIFY webChannelNamespaceChanged) + Q_PROPERTY(QStringList allowedOrigins READ allowedOrigins WRITE setAllowedOrigins NOTIFY allowedOriginsChanged) + Q_PROPERTY(QString webChannelScriptPath READ webChannelScriptPath WRITE setWebChannelScriptPath NOTIFY webChannelScriptPathChanged) + Q_PROPERTY(QStringList userScripts READ userScripts WRITE setUserScripts NOTIFY userScriptsChanged) + + // Status properties + Q_PROPERTY(bool loading READ isLoading NOTIFY loadingChanged) + +public: + explicit NativeWebView(QQuickItem *parent = nullptr); + ~NativeWebView() override; + + // Content accessors + QString htmlContent() const { return m_htmlContent; } + void setHtmlContent(const QString &html); + + QUrl url() const { return m_url; } + void setUrl(const QUrl &url); + + // WebChannel accessors + QWebChannel* webChannel() const { return m_channel; } + void setWebChannel(QWebChannel *channel); + + QString webChannelNamespace() const { return m_namespace; } + void setWebChannelNamespace(const QString &ns); + + QStringList allowedOrigins() const { return m_allowedOrigins; } + void setAllowedOrigins(const QStringList &origins); + + QString webChannelScriptPath() const { return m_webChannelScriptPath; } + void setWebChannelScriptPath(const QString &path); + + QStringList userScripts() const { return m_userScripts; } + void setUserScripts(const QStringList &scripts); + + // Status + bool isLoading() const { return m_loading; } + + // Methods + Q_INVOKABLE void loadHtml(const QString &html, const QUrl &baseUrl = QUrl()); + Q_INVOKABLE void loadUrl(const QUrl &url); + Q_INVOKABLE void runJavaScript(const QString &script); + + /** + * @brief Send a message to JavaScript via the WebChannel transport. + * + * This is called by NativeWebViewTransport to send QWebChannel messages. + */ + void postMessageToJavaScript(const QString &json); + + /** + * @brief Get the platform-specific backend. + */ + NativeWebViewBackend* backend() const { return m_backend; } + +signals: + void htmlContentChanged(); + void urlChanged(); + void webChannelChanged(); + void webChannelNamespaceChanged(); + void allowedOriginsChanged(); + void webChannelScriptPathChanged(); + void userScriptsChanged(); + void loadingChanged(); + void bridgeReady(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void itemChange(ItemChange change, const ItemChangeData &value) override; + +private slots: + void onWebMessageReceived(const QString &message, const QString &origin, bool isMainFrame); + void onLoadStarted(); + void onLoadingChanged(bool loading); + void onBackendUrlChanged(const QUrl &url); + +private: + void setupNativeView(); + void updateNativeViewGeometry(); + void installWebChannelBridge(); + void generateNewInvokeKey(); + + // Backend (platform-specific implementation) + NativeWebViewBackend *m_backend; + + // WebChannel + QWebChannel *m_channel; + NativeWebViewTransport *m_transport; + QString m_namespace; + QStringList m_allowedOrigins; + QString m_invokeKey; + + // Content state + QString m_htmlContent; + QUrl m_url; + QString m_webChannelScriptPath; + QStringList m_userScripts; + bool m_loading; + bool m_viewSetup; +}; + +#endif // NATIVEWEBVIEW_H diff --git a/ui/StatusQ/src/CustomWebView/nativewebviewbackend.h b/ui/StatusQ/src/CustomWebView/nativewebviewbackend.h new file mode 100644 index 00000000000..cfead115fe4 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/nativewebviewbackend.h @@ -0,0 +1,131 @@ +#ifndef NATIVEWEBVIEWBACKEND_H +#define NATIVEWEBVIEWBACKEND_H + +#include +#include +#include +#include + +class QQuickItem; + +/** + * @brief Abstract interface for platform-specific WebView implementations. + * + * This interface defines the contract for WebView backends that support + * WebChannel communication through platform-native IPC mechanisms + * (WKUserContentController on Darwin, JavaScriptInterface on Android). + */ +class NativeWebViewBackend : public QObject +{ + Q_OBJECT + +public: + explicit NativeWebViewBackend(QObject *parent = nullptr) : QObject(parent) {} + virtual ~NativeWebViewBackend() = default; + + // ===== WebView Operations ===== + + /** + * @brief Load a URL in the WebView. + */ + virtual void loadUrl(const QUrl &url) = 0; + + /** + * @brief Load HTML content with an optional base URL. + */ + virtual void loadHtml(const QString &html, const QUrl &baseUrl = QUrl()) = 0; + + /** + * @brief Get the native WebView handle (WKWebView* on Darwin, jobject on Android). + */ + virtual void* nativeHandle() const = 0; + + /** + * @brief Execute JavaScript in the WebView. + */ + virtual void runJavaScript(const QString &script) = 0; + + // ===== WebChannel Bridge ===== + + /** + * @brief Install the message bridge for WebChannel communication. + * + * This sets up the native IPC mechanism (WKScriptMessageHandler on Darwin, + * JavaScriptInterface on Android) and injects the necessary JavaScript. + * + * @param ns The JavaScript namespace for the bridge (e.g., "qt") + * @param allowedOrigins List of allowed origins for security + * @param invokeKey Unique key for this navigation session + * @param webChannelScriptPath Path to qwebchannel.js resource (optional) + * @param userScripts List of resource paths to additional scripts to inject + * @return true if bridge was installed successfully + */ + virtual bool installMessageBridge(const QString &ns, + const QStringList &allowedOrigins, + const QString &invokeKey, + const QString &webChannelScriptPath = QString(), + const QStringList &userScripts = QStringList()) = 0; + + /** + * @brief Send a message to JavaScript via the WebChannel transport. + * + * @param json JSON-encoded message to send + */ + virtual void postMessageToJavaScript(const QString &json) = 0; + + // ===== View Setup ===== + + /** + * @brief Set up the native view within the given QQuickItem's window. + * + * @param item The QQuickItem that will host the native view + */ + virtual void setupInItem(QQuickItem *item) = 0; + + /** + * @brief Update the native view's geometry to match the QQuickItem. + * + * @param item The QQuickItem whose geometry to match + */ + virtual void updateGeometry(QQuickItem *item) = 0; + +signals: + /** + * @brief Emitted when a message is received from JavaScript. + * + * @param message The message content (JSON string) + * @param origin The origin of the message + * @param isMainFrame Whether the message came from the main frame + */ + void webMessageReceived(const QString &message, + const QString &origin, + bool isMainFrame); + + /** + * @brief Emitted when loading state changes. + * + * @param loading Whether the WebView is currently loading + */ + void loadingChanged(bool loading); + + /** + * @brief Emitted when the URL changes. + */ + void urlChanged(const QUrl &url); + + /** + * @brief Emitted when loading starts (for bridge reinstallation). + */ + void loadStarted(); +}; + +/** + * @brief Factory function to create the platform-specific backend. + * + * @param parent Parent QObject + * @return Platform-specific NativeWebViewBackend implementation + */ +NativeWebViewBackend* createPlatformBackend(QObject *parent = nullptr); + +#endif // NATIVEWEBVIEWBACKEND_H + diff --git a/ui/StatusQ/src/CustomWebView/nativewebviewtransport.cpp b/ui/StatusQ/src/CustomWebView/nativewebviewtransport.cpp new file mode 100644 index 00000000000..10ce8d53597 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/nativewebviewtransport.cpp @@ -0,0 +1,80 @@ +#include "nativewebviewtransport.h" +#include "nativewebview.h" + +#include +#include +#include + +NativeWebViewTransport::NativeWebViewTransport(NativeWebView *view, const QString &ns, QObject *parent) + : QWebChannelAbstractTransport(parent) + , m_view(view) + , m_ns(ns) +{ +} + +void NativeWebViewTransport::sendMessage(const QJsonObject &message) +{ + if (!m_view) { + qWarning() << "NativeWebViewTransport: No view available"; + return; + } + + const QString json = QString::fromUtf8(QJsonDocument(message).toJson(QJsonDocument::Compact)); + qDebug() << "NativeWebViewTransport: Sending to JS:" << json.left(200); + m_view->postMessageToJavaScript(json); +} + +void NativeWebViewTransport::setAllowedOrigins(const QStringList &origins) +{ + m_allowedOrigins = origins; +} + +void NativeWebViewTransport::setInvokeKey(const QString &key) +{ + m_invokeKey = key; +} + +void NativeWebViewTransport::handleJsEnvelope(const QString &envelopeJson, + const QString &reportedOrigin, + bool /*isMainFrame*/) +{ + qDebug() << "NativeWebViewTransport: Received envelope:" << envelopeJson.left(200); + + // Envelope format (stringified JSON object): + // { "origin": "", "invokeKey": "", "data": "" } + + const QJsonDocument doc = QJsonDocument::fromJson(envelopeJson.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + qWarning() << "NativeWebViewTransport: Invalid envelope JSON"; + return; + } + + const QJsonObject obj = doc.object(); + const QString origin = obj.value(QLatin1String("origin")).toString(reportedOrigin); + const QString key = obj.value(QLatin1String("invokeKey")).toString(); + const QString data = obj.value(QLatin1String("data")).toString(); + + qDebug() << "NativeWebViewTransport: origin=" << origin << "key=" << key << "data=" << data.left(100); + + // Validate invoke key (prevents stale messages from previous navigations) + if (!m_invokeKey.isEmpty() && key != m_invokeKey) { + qDebug() << "NativeWebViewTransport: Ignoring message with stale invoke key, expected:" << m_invokeKey; + return; + } + + // Validate origin (security check) + if (!m_allowedOrigins.isEmpty() && !m_allowedOrigins.contains(origin)) { + qDebug() << "NativeWebViewTransport: Ignoring message from disallowed origin:" << origin; + return; + } + + // Parse the actual QWebChannel message + const QJsonDocument payload = QJsonDocument::fromJson(data.toUtf8()); + if (!payload.isNull() && payload.isObject()) { + qDebug() << "NativeWebViewTransport: Emitting messageReceived"; + emit messageReceived(payload.object(), this); + } else { + qWarning() << "NativeWebViewTransport: Failed to parse payload"; + } +} + diff --git a/ui/StatusQ/src/CustomWebView/nativewebviewtransport.h b/ui/StatusQ/src/CustomWebView/nativewebviewtransport.h new file mode 100644 index 00000000000..61fb9041d58 --- /dev/null +++ b/ui/StatusQ/src/CustomWebView/nativewebviewtransport.h @@ -0,0 +1,68 @@ +#ifndef NATIVEWEBVIEWTRANSPORT_H +#define NATIVEWEBVIEWTRANSPORT_H + +#include +#include +#include +#include + +class NativeWebView; + +/** + * @brief QWebChannel transport implementation for NativeWebView. + * + * This transport bridges QWebChannel with the native WebView's IPC mechanism. + * Messages from QWebChannel are sent to JavaScript via postMessageToJavaScript(), + * and messages from JavaScript are received via handleJsEnvelope(). + */ +class NativeWebViewTransport : public QWebChannelAbstractTransport +{ + Q_OBJECT + +public: + explicit NativeWebViewTransport(NativeWebView *view, const QString &ns, QObject *parent = nullptr); + ~NativeWebViewTransport() override = default; + + /** + * @brief Send a message from QWebChannel to JavaScript. + * + * Implements QWebChannelAbstractTransport::sendMessage(). + */ + void sendMessage(const QJsonObject &message) override; + + /** + * @brief Set allowed origins for security filtering. + */ + void setAllowedOrigins(const QStringList &origins); + + /** + * @brief Set the invoke key for the current navigation session. + * + * The invoke key is regenerated on each navigation to prevent + * stale messages from previous pages. + */ + void setInvokeKey(const QString &key); + + /** + * @brief Handle an envelope received from JavaScript. + * + * The envelope format is: + * { "origin": "", "invokeKey": "", "data": "" } + * + * @param envelopeJson The JSON envelope from JavaScript + * @param reportedOrigin The origin reported by the native layer + * @param isMainFrame Whether the message came from the main frame + */ + void handleJsEnvelope(const QString &envelopeJson, + const QString &reportedOrigin, + bool isMainFrame); + +private: + NativeWebView *m_view; + QString m_ns; + QString m_invokeKey; + QStringList m_allowedOrigins; +}; + +#endif // NATIVEWEBVIEWTRANSPORT_H + diff --git a/ui/StatusQ/src/typesregistration.cpp b/ui/StatusQ/src/typesregistration.cpp index 2105eb215f3..73fcc4c211b 100644 --- a/ui/StatusQ/src/typesregistration.cpp +++ b/ui/StatusQ/src/typesregistration.cpp @@ -29,6 +29,8 @@ #include "wallet/managetokensmodel.h" #include "onboarding/enums.h" +#include "CustomWebView/nativewebview.h" + #include #include @@ -36,6 +38,7 @@ void registerStatusQTypes() { qmlRegisterType("StatusQ", 0, 1, "StatusSyntaxHighlighter"); qmlRegisterType("StatusQ", 0, 1, "RXValidator"); + qmlRegisterType("StatusQ", 0, 1, "NativeWebView"); qmlRegisterUncreatableType( "StatusQ", 0, 1,