Files
self/packages/native-shell-ios/Sources/SelfNativeShell/Bridge/MessageRouter.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

114 lines
3.8 KiB
Swift

// SPDX-License-Identifier: BUSL-1.1
import Foundation
final class MessageRouter {
private var handlers: [BridgeDomain: BridgeHandler] = [:]
private let sendToWebView: (String) -> Void
init(sendToWebView: @escaping (String) -> Void) {
self.sendToWebView = sendToWebView
}
func register(handler: BridgeHandler) {
handlers[handler.domain] = handler
}
func onMessageReceived(rawJson: String) {
guard let data = rawJson.data(using: .utf8) else {
return
}
let request: BridgeRequest
do {
request = try JSONDecoder().decode(BridgeRequest.self, from: data)
} catch {
return
}
guard request.version == 1 else {
let response = BridgeResponse.failure(
request: request,
code: "UNSUPPORTED_VERSION",
message: "Protocol version \(request.version) is not supported"
)
sendResponse(response)
return
}
guard let handler = handlers[request.domain] else {
let response = BridgeResponse.failure(
request: request,
code: "UNKNOWN_DOMAIN",
message: "No handler for domain: \(request.domain.rawValue)"
)
sendResponse(response)
return
}
let params = request.params?.mapValues { $0.value }
Task {
do {
let result = try await handler.handle(method: request.method, params: params)
let response = BridgeResponse.success(request: request, result: result)
await MainActor.run { sendResponse(response) }
} catch let error as BridgeHandlerError {
let response = BridgeResponse.failure(
request: request,
code: error.code,
message: error.errorDescription ?? "Unknown error"
)
await MainActor.run { sendResponse(response) }
} catch {
let response = BridgeResponse.failure(
request: request,
code: "HANDLER_ERROR",
message: error.localizedDescription
)
await MainActor.run { sendResponse(response) }
}
}
}
func pushEvent(domain: BridgeDomain, event: String, data: Any?) {
let eventDict: [String: Any] = [
"type": "event",
"version": 1,
"domain": domain.rawValue,
"event": event,
"data": data as Any,
"timestamp": Date().timeIntervalSince1970 * 1000
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: eventDict),
let jsonStr = String(data: jsonData, encoding: .utf8) else {
return
}
let escaped = escapeForJs(jsonStr)
let js = "window.SelfNativeBridge._handleEvent('\(escaped)')"
sendToWebView(js)
}
private func sendResponse(_ response: BridgeResponse) {
guard let data = try? JSONEncoder().encode(response),
let jsonStr = String(data: data, encoding: .utf8) else {
return
}
let escaped = escapeForJs(jsonStr)
let js = "window.SelfNativeBridge._handleResponse('\(escaped)')"
sendToWebView(js)
}
private func escapeForJs(_ str: String) -> String {
str.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
}
}