Files
self/packages/native-shell-ios/Sources/SelfNativeShell/WebView/SelfWebViewHost.swift
Seshanth.S 60de8d6c1e Feat/webview sdk (#1856)
* feat: add iOS native shell package (NSL-02)

Plain Swift implementation of the WebView host with bridge handlers
for secure storage (Keychain), crypto (EC P-256), and lifecycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Android native shell package (NSL-01)

Plain Kotlin implementation of the WebView host with bridge handlers
for secure storage (EncryptedSharedPreferences), crypto (Android Keystore
EC P-256), and lifecycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: integrate Sumsub Web SDK into ProviderLaunchScreen (WV-05)

Rewrites ProviderLaunchScreen to launch Sumsub Web SDK, adds KYC
provider types, result normalization, and a ProviderResultScreen
for displaying verification outcomes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update spec status for NSL-01, NSL-02, WV-05 to in-progress

All three items are code-complete but need integration testing
before marking done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add build-pipeline workstream specs, update NSL-03 and BP-01 status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add WebView bundle build pipeline (BP-01)

Build script copies webview-app dist into both native shell asset
directories. Gradle preBuild validation fails fast when bundle is
missing. Root package.json gets build:sdk-* scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add SDK test apps for Android and iOS (NSL-03)

Minimal test apps to exercise native shells end-to-end:
- Android: Jetpack Compose app using SelfSdk.launch() via composite build
- iOS: SwiftUI app using SelfSdk.createViewController() via local SPM dep

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update lockfile

* fix: address CodeRabbit PR review findings for native shells

- Fix iOS double callback: add hasEmittedResult flag to LifecycleHandler
  so dismiss() won't fire onCancelled after onResult already emitted
- Fix Android error result codes: use RESULT_FIRST_USER for failed
  verifications instead of always RESULT_OK; add dedicated handler in
  SelfSdk.handleResult
- Fix iOS production query params: append params to file URL via
  URLComponents so WebView receives teeUrl/verificationId/userId
- Fix build:sdk-ios false-green: chain swift build after bundle script
- Add expectedRequestCode param to handleResult for flexibility
- Upgrade security-crypto 1.1.0-alpha06 → 1.1.0 stable
- Improve callback type safety: onSuccess takes raw JSON string,
  onFailure takes SelfSdkException instead of generic Exception
- Add requireBiometric intent comments to both SecureStorageHandlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address remaining CodeRabbit PR review findings (round 2)

- iOS BridgeResponse: add requestId/success fields, rename result→data to match JS bridge contract
- iOS test app: fix callback deallocation with Coordinator pattern
- ProviderLaunchScreen: fail closed on missing verificationId, fix retry via retryCount state
- ProviderResultScreen: guard unknown status with fallback to error config
- build-webview-bundle.sh: validate index.html before deleting targets
- Package.swift: fix SPM resource path with target path/sources

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 02:26:40 +05:30

98 lines
3.0 KiB
Swift

// SPDX-License-Identifier: BUSL-1.1
import Foundation
import UIKit
import WebKit
final class SelfWebViewHost: NSObject {
private var webView: WKWebView?
private let router: MessageRouter
private let isDebugMode: Bool
init(router: MessageRouter, isDebugMode: Bool = false) {
self.router = router
self.isDebugMode = isDebugMode
super.init()
}
func createWebView() -> WKWebView {
let config = WKWebViewConfiguration()
let contentController = WKUserContentController()
contentController.add(WeakScriptMessageProxy(handler: self), name: "SelfNativeIOS")
config.userContentController = contentController
config.preferences.javaScriptCanOpenWindowsAutomatically = false
let webView = WKWebView(frame: .zero, configuration: config)
webView.scrollView.bounces = false
webView.isOpaque = false
webView.backgroundColor = .clear
if #available(iOS 16.4, *) {
webView.isInspectable = isDebugMode
}
self.webView = webView
return webView
}
func loadContent(queryParams: String) {
guard let webView = webView else { return }
if isDebugMode {
let urlString = "http://localhost:5173?\(queryParams)"
if let url = URL(string: urlString) {
webView.load(URLRequest(url: url))
}
} else {
guard let bundlePath = Bundle.main.path(forResource: "self-sdk-web", ofType: nil) else {
return
}
let fileURL = URL(fileURLWithPath: "\(bundlePath)/index.html")
let bundleURL = URL(fileURLWithPath: bundlePath)
var components = URLComponents(url: fileURL, resolvingAgainstBaseURL: false)
if !queryParams.isEmpty {
components?.query = queryParams
}
let targetURL = components?.url ?? fileURL
webView.loadFileURL(targetURL, allowingReadAccessTo: bundleURL)
}
}
func evaluateJs(_ js: String) {
DispatchQueue.main.async { [weak self] in
self?.webView?.evaluateJavaScript(js, completionHandler: nil)
}
}
}
extension SelfWebViewHost: WKScriptMessageHandler {
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard message.name == "SelfNativeIOS",
let body = message.body as? String else {
return
}
router.onMessageReceived(rawJson: body)
}
}
// Prevents WKWebView retain cycle with WKScriptMessageHandler
private final class WeakScriptMessageProxy: NSObject, WKScriptMessageHandler {
private weak var handler: WKScriptMessageHandler?
init(handler: WKScriptMessageHandler) {
self.handler = handler
}
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
handler?.userContentController(userContentController, didReceive: message)
}
}