From 91442af73ce6d78701df17134cec8be4b7ac2f9b Mon Sep 17 00:00:00 2001 From: scorsin-oai Date: Thu, 23 Apr 2026 11:42:40 -0500 Subject: [PATCH] Added webview native template element (#16) ## Description This change adds webview as a builtin native template element. It is implemented through the new polyglot module system we introduced for open sourcing, which allows to provide new native views with their TS API in their own isolated modules and have them being automatically discoverable by the Valdi runtime. It works on both iOS and Android. The API is exposed through a WebViewController API which is code generated. The controller can be attached when rendered under a `` element. Added also a sample app which showcases the API. (cherry picked from commit 73f32a0c05fe45dcc69a020ea59fa96b0b367a20) --- apps/webview_example/BUILD.bazel | 28 +++ apps/webview_example/IconButton.tsx | 36 ++++ apps/webview_example/WebViewExample.tsx | 165 +++++++++++++++ apps/webview_example/res/back.svg | 4 + apps/webview_example/res/forward.svg | 4 + apps/webview_example/res/reload.svg | 4 + docs/api/api-quick-reference.md | 12 ++ docs/api/api-reference-elements.md | 27 +++ .../src/valdi/valdi_core/src/JSXBootstrap.ts | 1 + .../valdi_test/test/ElementForViewClass.d.ts | 5 +- .../test/IRenderedElementViewClass.ts | 1 + .../valdi/valdi_test/test/JSXTest.spec.tsx | 19 ++ .../src/valdi/valdi_tsx/src/JSX.ts | 2 + .../valdi_tsx/src/NativeTemplateElements.d.ts | 21 ++ .../src/valdi/valdi_webview/BUILD.bazel | 54 +++++ .../android/WebViewNativeModuleFactoryImpl.kt | 14 ++ .../modules/webview/AndroidWebViewHolder.kt | 194 +++++++++++++++++ .../valdi/modules/webview/ValdiWebView.kt | 62 ++++++ .../webview/ValdiWebViewAttributesBinder.kt | 25 +++ .../modules/webview/WebViewControllerImpl.kt | 158 ++++++++++++++ .../ios/SCValdiWKWebViewHolder.h | 24 +++ .../ios/SCValdiWKWebViewHolder.m | 161 ++++++++++++++ .../valdi/valdi_webview/ios/SCValdiWebView.h | 11 + .../valdi/valdi_webview/ios/SCValdiWebView.m | 63 ++++++ .../ios/SCValdiWebViewControllerImpl.h | 6 + .../ios/SCValdiWebViewControllerImpl.m | 200 ++++++++++++++++++ .../ios/SCValdiWebViewNativeModule.m | 31 +++ .../src/valdi/valdi_webview/module.yaml | 7 + .../src/valdi/valdi_webview/src/WebView.ts | 15 ++ .../valdi_webview/src/WebViewNative.d.ts | 77 +++++++ .../src/valdi/valdi_webview/tsconfig.json | 8 + .../SCValdiUnsetAndReleaseInMainThread.h | 14 ++ .../SCValdiUnsetAndReleaseInMainThread.m | 23 ++ 33 files changed, 1474 insertions(+), 2 deletions(-) create mode 100644 apps/webview_example/BUILD.bazel create mode 100644 apps/webview_example/IconButton.tsx create mode 100644 apps/webview_example/WebViewExample.tsx create mode 100644 apps/webview_example/res/back.svg create mode 100644 apps/webview_example/res/forward.svg create mode 100644 apps/webview_example/res/reload.svg create mode 100644 src/valdi_modules/src/valdi/valdi_webview/BUILD.bazel create mode 100644 src/valdi_modules/src/valdi/valdi_webview/android/WebViewNativeModuleFactoryImpl.kt create mode 100644 src/valdi_modules/src/valdi/valdi_webview/android/com/snap/valdi/modules/webview/AndroidWebViewHolder.kt create mode 100644 src/valdi_modules/src/valdi/valdi_webview/android/com/snap/valdi/modules/webview/ValdiWebView.kt create mode 100644 src/valdi_modules/src/valdi/valdi_webview/android/com/snap/valdi/modules/webview/ValdiWebViewAttributesBinder.kt create mode 100644 src/valdi_modules/src/valdi/valdi_webview/android/com/snap/valdi/modules/webview/WebViewControllerImpl.kt create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWKWebViewHolder.h create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWKWebViewHolder.m create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWebView.h create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWebView.m create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWebViewControllerImpl.h create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWebViewControllerImpl.m create mode 100644 src/valdi_modules/src/valdi/valdi_webview/ios/SCValdiWebViewNativeModule.m create mode 100644 src/valdi_modules/src/valdi/valdi_webview/module.yaml create mode 100644 src/valdi_modules/src/valdi/valdi_webview/src/WebView.ts create mode 100644 src/valdi_modules/src/valdi/valdi_webview/src/WebViewNative.d.ts create mode 100644 src/valdi_modules/src/valdi/valdi_webview/tsconfig.json create mode 100644 valdi_core/src/valdi_core/ios/valdi_core/SCValdiUnsetAndReleaseInMainThread.h create mode 100644 valdi_core/src/valdi_core/ios/valdi_core/SCValdiUnsetAndReleaseInMainThread.m diff --git a/apps/webview_example/BUILD.bazel b/apps/webview_example/BUILD.bazel new file mode 100644 index 00000000..2debf2f3 --- /dev/null +++ b/apps/webview_example/BUILD.bazel @@ -0,0 +1,28 @@ +load("//bzl/valdi:valdi_application.bzl", "valdi_application") +load("//bzl/valdi:valdi_module.bzl", "valdi_module") + +valdi_module( + name = "webview_example", + srcs = glob([ + "**/*.ts", + "**/*.tsx", + ]), + res = glob([ + "res/**/*.svg", + ]), + visibility = ["//visibility:public"], + deps = [ + "//src/valdi_modules/src/valdi/valdi_core", + "//src/valdi_modules/src/valdi/valdi_tsx", + "//src/valdi_modules/src/valdi/valdi_webview", + ], +) + +valdi_application( + name = "webview_example_app", + ios_bundle_id = "com.snap.valdi.webviewexample", + ios_families = ["iphone"], + root_component_path = "App@webview_example/WebViewExample", + title = "Valdi WebView Example", + deps = [":webview_example"], +) diff --git a/apps/webview_example/IconButton.tsx b/apps/webview_example/IconButton.tsx new file mode 100644 index 00000000..ed25ead3 --- /dev/null +++ b/apps/webview_example/IconButton.tsx @@ -0,0 +1,36 @@ +import { Component } from 'valdi_core/src/Component'; +import { Style } from 'valdi_core/src/Style'; +import { Asset } from 'valdi_tsx/src/Asset'; +import { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements'; + +export interface IconButtonViewModel { + icon: Asset; + onTap: () => void; +} + +export class IconButton extends Component { + onRender(): void { + + + ; + } +} + +const styles = { + button: new Style({ + width: 40, + height: 40, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + marginRight: 6, + backgroundColor: '#eef1f5', + }), + + icon: new Style({ + width: 22, + height: 22, + objectFit: 'contain', + touchEnabled: false, + }), +}; diff --git a/apps/webview_example/WebViewExample.tsx b/apps/webview_example/WebViewExample.tsx new file mode 100644 index 00000000..34cb8c7d --- /dev/null +++ b/apps/webview_example/WebViewExample.tsx @@ -0,0 +1,165 @@ +import { StatefulComponent } from 'valdi_core/src/Component'; +import { Device } from 'valdi_core/src/Device'; +import { Style } from 'valdi_core/src/Style'; +import { systemFont } from 'valdi_core/src/SystemFont'; +import { TextField, View, WebViewElement } from 'valdi_tsx/src/NativeTemplateElements'; +import { IWebViewController, IWebViewListener, WebView as WebViewModule } from 'valdi_webview/src/WebView'; + +import { IconButton } from './IconButton'; +import res from './res'; + +const SNAPCHAT_URL = 'https://www.snapchat.com'; +const BAR_HEIGHT = 56; + +interface State { + urlText: string; +} + +export class App extends StatefulComponent<{}, State> { + state: State = { + urlText: SNAPCHAT_URL, + }; + + private readonly webViewController: IWebViewController = WebViewModule.createController(); + + private readonly webViewListener: IWebViewListener = { + onMessage(message: string): void { + console.log(`WebView message: ${message}`); + }, + onLoadFailed(errorMessage: string): void { + console.log(`WebView load failed: ${errorMessage}`); + }, + onLoadCompleted(): void { + console.log('WebView load completed'); + }, + }; + + onCreate(): void { + this.registerDisposable(() => { + this.webViewController.dispose(); + }); + + this.webViewController.setListener(this.webViewListener); + this.webViewController.load({ url: SNAPCHAT_URL }); + } + + onDestroy(): void { + this.webViewController.setListener(undefined); + } + + onRender(): void { + + + + + + + + + + + ; + } + + private goBack = (): void => { + this.webViewController.getState().then(state => { + if (state.canGoBack) { + this.webViewController.goBack(); + } + }); + }; + + private goForward = (): void => { + this.webViewController.getState().then(state => { + if (state.canGoForward) { + this.webViewController.goForward(); + } + }); + }; + + private reload = (): void => { + this.webViewController.reload(); + }; + + private readonly onUrlTextChange: NonNullable = event => { + this.setState({ urlText: event.text }); + }; + + private readonly onUrlEditEnd: NonNullable = event => { + this.loadUrl(event.text); + }; + + private loadUrl(rawUrl: string): void { + const url = this.normalizeUrl(rawUrl); + this.setState({ urlText: url }); + this.webViewController.load({ url }); + } + + private normalizeUrl(rawUrl: string): string { + const trimmedUrl = rawUrl.trim(); + if (trimmedUrl.length === 0) { + return SNAPCHAT_URL; + } + if (trimmedUrl.indexOf('://') >= 0) { + return trimmedUrl; + } + return `https://${trimmedUrl}`; + } +} + +const styles = { + root: new Style({ + backgroundColor: 'white', + width: '100%', + height: '100%', + }), + + navigationBar: new Style({ + width: '100%', + paddingLeft: 10, + paddingRight: 10, + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#f8fafc', + }), + + urlFieldContainer: new Style({ + flexGrow: 1, + height: 40, + borderRadius: 10, + backgroundColor: 'white', + borderWidth: 1, + borderColor: '#cbd5e1', + paddingLeft: 12, + paddingRight: 12, + justifyContent: 'center', + }), + + urlField: new Style({ + width: '100%', + height: 38, + color: '#0f172a', + tintColor: '#2563eb', + font: systemFont(15), + placeholder: 'Enter a URL', + contentType: 'url', + returnKeyText: 'go', + autocapitalization: 'none', + autocorrection: 'none', + closesWhenReturnKeyPressed: true, + }), + + webview: new Style({ + width: '100%', + flexGrow: 1, + }), +}; diff --git a/apps/webview_example/res/back.svg b/apps/webview_example/res/back.svg new file mode 100644 index 00000000..86e67dad --- /dev/null +++ b/apps/webview_example/res/back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/webview_example/res/forward.svg b/apps/webview_example/res/forward.svg new file mode 100644 index 00000000..2076dcd0 --- /dev/null +++ b/apps/webview_example/res/forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/webview_example/res/reload.svg b/apps/webview_example/res/reload.svg new file mode 100644 index 00000000..8775c671 --- /dev/null +++ b/apps/webview_example/res/reload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/api/api-quick-reference.md b/docs/api/api-quick-reference.md index 644a1b4a..06553490 100644 --- a/docs/api/api-quick-reference.md +++ b/docs/api/api-quick-reference.md @@ -477,6 +477,18 @@ class MyComponent extends Component { ``` +### Web Content + +```tsx + +``` + +Create the controller with `WebView.createController()` from `valdi_webview`, then call controller methods such as `load`, `reload`, and `goBack` imperatively. + ### Form Input ```tsx diff --git a/docs/api/api-reference-elements.md b/docs/api/api-reference-elements.md index ee86c36c..4ff71b69 100644 --- a/docs/api/api-reference-elements.md +++ b/docs/api/api-reference-elements.md @@ -8,6 +8,7 @@ This document provides a comprehensive reference for all native template element - [View](#view) - [ScrollView](#scrollview) - [ImageView](#imageview) +- [WebView](#webview) - [VideoView](#videoview) - [Label](#label) - [TextField](#textfield) @@ -825,6 +826,32 @@ All view properties from [View](#view) including: --- +## WebView + +**JSX Element:** `` + +**iOS Native:** `SCValdiWebView` +**Android Native:** `com.snap.valdi.views.ValdiWebView` + +A host element for a native webview controller created by the `valdi_webview` module. + +### Properties + +All properties from [Layout Attributes](#layout), plus: + +#### Controller + +**`controller`**: `IWebViewNativeController` +- Native webview controller created by `WebView.createController()` from `valdi_webview`. +- The controller owns the platform webview and is attached to this host element when the attribute is applied. + +#### Styling + +**`style`**: `IStyle` +- See [View Style Attributes](api-style-attributes.md#view-styles) for common view styling attributes. + +--- + ## VideoView **JSX Element:** `