catalyst-core 0.1.0-beta.3 → 0.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/bin/catalyst.js +8 -1
  2. package/changelog.md +6 -0
  3. package/dist/native/androidProject/app/build.gradle.kts +9 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +17 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +31 -3
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +148 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  22. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  23. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  24. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  25. package/dist/native/bridge/WebBridge.js +5 -5
  26. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  27. package/dist/native/bridge/hooks/index.js +1 -0
  28. package/dist/native/bridge/hooks/useCamera.js +9 -0
  29. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  30. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  31. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  32. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  33. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  34. package/dist/native/bridge/hooks/useIntent.js +1 -0
  35. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  36. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  37. package/dist/native/bridge/hooks/useNotification.js +2 -0
  38. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  39. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  40. package/dist/native/bridge/hooks/utils.js +1 -0
  41. package/dist/native/bridge/hooks.js +1 -137
  42. package/dist/native/bridge/useBaseHook.js +8 -3
  43. package/dist/native/bridge/utils/NativeBridge.js +36 -1
  44. package/dist/native/buildAppAndroid.js +2 -2
  45. package/dist/native/buildAppIos.js +10 -17
  46. package/dist/native/internalPluginUtils.js +1 -0
  47. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  48. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  49. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  50. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  51. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  52. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  53. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  54. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  55. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  56. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  57. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  58. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  59. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  60. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  61. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  62. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  63. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  64. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +38 -7
  66. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
  67. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +15 -4
  68. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  69. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  70. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  71. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  72. package/dist/native/pluginComposerAndroid.js +9 -0
  73. package/dist/native/pluginComposerIos.js +7 -0
  74. package/dist/otel.js +70 -2
  75. package/dist/scripts/plugins.js +1 -0
  76. package/dist/server/renderer/extract.js +3 -3
  77. package/dist/server/renderer/handler.js +15 -14
  78. package/dist/webpack/development.client.babel.js +5 -1
  79. package/dist/webpack/production.ssr.babel.js +5 -1
  80. package/mcp_v2/knowledge-base.json +1564 -1067
  81. package/package.json +1 -1
package/bin/catalyst.js CHANGED
@@ -13,6 +13,8 @@ const validCommands = [
13
13
  "serve",
14
14
  "devBuild",
15
15
  "devServe",
16
+ "plugin",
17
+ "plugins",
16
18
  "buildApp",
17
19
  "buildApp:ios",
18
20
  "buildApp:android",
@@ -67,11 +69,14 @@ const scriptIndex = args.findIndex(
67
69
  x === "serve" ||
68
70
  x === "devBuild" ||
69
71
  x === "devServe" ||
72
+ x === "plugin" ||
73
+ x === "plugins" ||
70
74
  isPlatformCommand(x, "buildApp") ||
71
75
  isPlatformCommand(x, "setupEmulator")
72
76
  )
73
77
  const script = scriptIndex === -1 ? args[0] : args[scriptIndex]
74
78
  const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []
79
+ const resolvedScript = script === "plugin" ? "plugins" : script
75
80
 
76
81
  if (validCommands.includes(script)) {
77
82
  // Handle platform-specific or combined commands
@@ -90,7 +95,9 @@ if (validCommands.includes(script)) {
90
95
  // Original commands
91
96
  const result = spawnSync(
92
97
  process.execPath,
93
- nodeArgs.concat(require.resolve("../dist/scripts/" + script)).concat(args.slice(scriptIndex + 1)),
98
+ nodeArgs
99
+ .concat(require.resolve("../dist/scripts/" + resolvedScript))
100
+ .concat(args.slice(scriptIndex + 1)),
94
101
  { stdio: "inherit" }
95
102
  )
96
103
  handleProcessResult(result)
package/changelog.md CHANGED
@@ -5,6 +5,12 @@
5
5
  - Moved Catalyst into a monorepo structure with `catalyst-core`, `create-catalyst-app`, the Catalyst docs app, and the Catalyst core test app managed from one repository.
6
6
  - Replaced the internal-package pre-release flow with a local release sandbox that scaffolds a real app using current-branch CCA and a locally packed current-branch `catalyst-core`.
7
7
 
8
+ ## [0.1.0-canary.8] - 2026-04-15
9
+
10
+ - Introduced Catalyst’s internal plugin architecture, adding the `PluginBridge` web API, native plugin runtimes for Android and iOS, and manifest-driven build composition for modular native capabilities.
11
+ - Added config-based plugin enablement through `WEBVIEW_CONFIG.plugins` and a `catalyst plugins` CLI for discovering and managing internal framework plugins from the app layer.
12
+ - Shipped the first cross-platform migrated plugin, `device_info`
13
+
8
14
  ## [0.1.0-beta.1] - 2026-04-15
9
15
 
10
16
  - Promoted `0.1.0-canary.7` to the first proper beta release after stabilization.
@@ -208,6 +208,15 @@ dependencies {
208
208
  // SLF4J simple logger for Ktor (optional, can be excluded if needed)
209
209
  implementation("org.slf4j:slf4j-simple:2.0.9")
210
210
 
211
+ // CameraX
212
+ implementation("androidx.camera:camera-core:1.3.4")
213
+ implementation("androidx.camera:camera-camera2:1.3.4")
214
+ implementation("androidx.camera:camera-lifecycle:1.3.4")
215
+ implementation("androidx.camera:camera-view:1.3.4")
216
+
217
+ // ML Kit Barcode Scanning
218
+ implementation("com.google.mlkit:barcode-scanning:17.3.0")
219
+
211
220
  // Notification dependencies - conditional based on config
212
221
  if (isNotificationsEnabled()) {
213
222
  implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
@@ -60,7 +60,7 @@ data class SchemaDefinition(
60
60
  object BridgeMessageValidator {
61
61
 
62
62
  private const val TAG = "BridgeMessageValidator"
63
- private const val MAX_MESSAGE_SIZE = 10 * 1024 * 1024 // 10MB (match iOS)
63
+ private const val MAX_MESSAGE_SIZE = CatalystConstants.Bridge.MAX_MESSAGE_SIZE
64
64
 
65
65
  // Valid commands — single source of truth from CatalystConstants
66
66
  private val validCommands = CatalystConstants.Bridge.VALID_COMMANDS
@@ -51,6 +51,7 @@ class CustomWebView(
51
51
  private var offlinePageVisible = false
52
52
  private var lastTargetUrl: String? = null
53
53
  private var defaultRequestHeaders: Map<String, String> = emptyMap()
54
+ var onPageStarted: (() -> Unit)? = null
54
55
 
55
56
  // Counters for asset loading statistics
56
57
  private var assetLoadAttempts = 0
@@ -233,7 +234,7 @@ class CustomWebView(
233
234
  }
234
235
 
235
236
  // Additional security check: only allow whitelisted interface names
236
- val allowedInterfaces = setOf("NativeBridge", "AndroidBridge")
237
+ val allowedInterfaces = setOf("NativeBridge", "AndroidBridge", "PluginBridge")
237
238
  if (name !in allowedInterfaces) {
238
239
  Log.e(TAG, "❌ Security: Interface name '$name' is not in whitelist. Refusing to add interface.")
239
240
  return
@@ -246,6 +247,17 @@ class CustomWebView(
246
247
  }
247
248
  }
248
249
 
250
+ fun removeJavascriptInterface(name: String) {
251
+ try {
252
+ webView.removeJavascriptInterface(name)
253
+ if (BuildConfig.DEBUG) {
254
+ Log.d(TAG, "🔌 Removed JavaScript interface: $name")
255
+ }
256
+ } catch (e: Exception) {
257
+ Log.w(TAG, "⚠️ Failed to remove JavaScript interface '$name': ${e.message}")
258
+ }
259
+ }
260
+
249
261
  fun destroy() {
250
262
  job.cancel()
251
263
  webView.destroy()
@@ -606,6 +618,9 @@ class CustomWebView(
606
618
  domStorageEnabled = true
607
619
  allowFileAccess = true
608
620
  allowContentAccess = true
621
+ setSupportZoom(false)
622
+ builtInZoomControls = false
623
+ displayZoomControls = false
609
624
 
610
625
  // TODO: Enable these when build optimization feature is implemented
611
626
  // These are deprecated but may be needed for local file access in development
@@ -882,6 +897,7 @@ class CustomWebView(
882
897
  super.onPageStarted(view, url, favicon)
883
898
  if (url != null && url != offlineAssetUrl) {
884
899
  offlinePageVisible = false
900
+ onPageStarted?.invoke()
885
901
  }
886
902
  progressBar.visibility = View.VISIBLE
887
903
  val startTime = System.currentTimeMillis()
@@ -4,6 +4,7 @@ import android.content.Intent
4
4
  import android.os.Build
5
5
  import android.os.Bundle
6
6
  import android.util.Log
7
+ import android.view.MotionEvent
7
8
  import android.view.WindowManager
8
9
  import androidx.activity.OnBackPressedCallback
9
10
  import androidx.appcompat.app.AppCompatActivity
@@ -14,6 +15,7 @@ import org.json.JSONObject
14
15
  import java.util.Properties
15
16
  import io.yourname.androidproject.databinding.ActivityMainBinding
16
17
  import io.yourname.androidproject.NativeBridge
18
+ import io.yourname.androidproject.plugins.PluginBridge
17
19
  import io.yourname.androidproject.utils.BridgeUtils
18
20
  import io.yourname.androidproject.utils.KeyboardUtil
19
21
  import io.yourname.androidproject.utils.NetworkUtils
@@ -39,6 +41,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
39
41
 
40
42
  private lateinit var binding: ActivityMainBinding
41
43
  private lateinit var nativeBridge: NativeBridge
44
+ private lateinit var pluginBridge: PluginBridge
42
45
  private lateinit var customWebView: CustomWebView
43
46
  lateinit var properties: Properties
44
47
  private lateinit var metricsMonitor: MetricsMonitor
@@ -324,10 +327,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
324
327
  try {
325
328
  nativeBridge = NativeBridge(this, customWebView.getWebView(), properties)
326
329
  customWebView.addJavascriptInterface(nativeBridge, "NativeBridge")
330
+
331
+ // Wire NativeCameraManager
332
+ val cameraManager = NativeCameraManager(this, binding.cameraPreview, customWebView.getWebView(), binding.debugViewfinderOverlay, binding.debugViewfinderOverlay.findViewById(R.id.debug_qr_status), binding.debugBarcodeOverlay)
333
+ nativeBridge.setCameraManager(cameraManager)
334
+ customWebView.onPageStarted = { cameraManager.stop() }
327
335
  } catch (e: Exception) {
328
336
  Log.e(TAG, "Failed to initialize NativeBridge: ${e.message}")
329
337
  }
330
338
 
339
+ // Setup isolated PluginBridge
340
+ try {
341
+ pluginBridge = PluginBridge(this, customWebView.getWebView(), properties)
342
+ customWebView.addJavascriptInterface(pluginBridge, "PluginBridge")
343
+ } catch (e: Exception) {
344
+ Log.e(TAG, "Failed to initialize PluginBridge: ${e.message}")
345
+ }
346
+
331
347
  setupSafeAreaHandling()
332
348
 
333
349
  // Setup back press handler (modern API)
@@ -421,6 +437,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
421
437
  customWebView.onResume()
422
438
  }
423
439
 
440
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
441
+ if (::nativeBridge.isInitialized) {
442
+ nativeBridge.getCameraManager()?.onTouchEvent(ev)
443
+ }
444
+ return super.dispatchTouchEvent(ev)
445
+ }
446
+
424
447
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
425
448
  super.onRequestPermissionsResult(requestCode, permissions, grantResults)
426
449
  if (::nativeBridge.isInitialized) {
@@ -445,11 +468,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
445
468
  if (::keyboardUtil.isInitialized) {
446
469
  keyboardUtil.cleanup()
447
470
  }
448
- if (::nativeBridge.isInitialized) {
449
- nativeBridge.cleanup()
471
+ if (::customWebView.isInitialized) {
472
+ if (::pluginBridge.isInitialized) {
473
+ customWebView.removeJavascriptInterface("PluginBridge")
474
+ }
475
+ if (::nativeBridge.isInitialized) {
476
+ customWebView.removeJavascriptInterface("NativeBridge")
477
+ }
478
+ customWebView.destroy()
450
479
  }
451
480
  coroutineContext.cancelChildren()
452
- customWebView.destroy()
453
481
  super.onDestroy()
454
482
  }
455
483
 
@@ -151,9 +151,16 @@ class NativeBridge(
151
151
 
152
152
  private var networkMonitor: NetworkMonitor? = null
153
153
 
154
+ // Video stream manager
155
+ private var nativeCameraManager: NativeCameraManager? = null
156
+ private var webViewOriginalBackground: Int = android.graphics.Color.WHITE
157
+
154
158
  // Unified notification manager
155
159
  private val notificationManager = AppNotificationManager(mainActivity, mainActivity.properties)
156
160
 
161
+ // Transition manager — snapshot overlay pattern for native page transitions
162
+ private val transitionManager = TransitionManager(mainActivity, webView)
163
+
157
164
  companion object {
158
165
  private const val TAG = "NativeBridge"
159
166
  private const val GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION = "start Google Sign-In"
@@ -239,6 +246,90 @@ class NativeBridge(
239
246
  }
240
247
  }
241
248
 
249
+ fun setCameraManager(manager: NativeCameraManager) {
250
+ nativeCameraManager = manager
251
+ }
252
+
253
+ fun getCameraManager(): NativeCameraManager? = nativeCameraManager
254
+
255
+ @JavascriptInterface
256
+ fun startVideoStream(optionsRaw: String?) {
257
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "start video stream") {
258
+ android.util.Log.d("NativeBridge", "startVideoStream raw options: $optionsRaw")
259
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
260
+ val facing = options.optString("facing", "back")
261
+ val viewfinderRect = options.optJSONObject("viewfinderRect")
262
+ val zoomOptions = options.optJSONObject("zoom")
263
+ val scanFormat = options.optString("format", "all")
264
+ val showQrDetected = options.optBoolean("showQrDetected", false)
265
+ android.util.Log.d("NativeBridge", "startVideoStream parsed: facing=$facing zoom=$zoomOptions scanFormat=$scanFormat viewfinderRect=$viewfinderRect showQrDetected=$showQrDetected")
266
+ mainActivity.runOnUiThread {
267
+ webViewOriginalBackground = webView.solidColor.takeIf { it != 0 } ?: android.graphics.Color.WHITE
268
+ webView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
269
+ nativeCameraManager?.start(facing, viewfinderRect, zoomOptions, scanFormat, showQrDetected = showQrDetected)
270
+ }
271
+ }
272
+ }
273
+
274
+ @JavascriptInterface
275
+ fun stopVideoStream(optionsRaw: String?) {
276
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "stop video stream") {
277
+ mainActivity.runOnUiThread {
278
+ webView.setBackgroundColor(webViewOriginalBackground)
279
+ nativeCameraManager?.stop()
280
+ }
281
+ }
282
+ }
283
+
284
+ @JavascriptInterface
285
+ fun setVideoStreamZoom(multiplierRaw: String?) {
286
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream zoom") {
287
+ val multiplier = multiplierRaw?.trim()?.toFloatOrNull() ?: run {
288
+ android.util.Log.w("NativeBridge", "setVideoStreamZoom — invalid value: $multiplierRaw, ignoring")
289
+ return@safeExecute
290
+ }
291
+ android.util.Log.d("NativeBridge", "setVideoStreamZoom — multiplier=${multiplier}x")
292
+ mainActivity.runOnUiThread {
293
+ nativeCameraManager?.setZoom(multiplier)
294
+ }
295
+ }
296
+ }
297
+
298
+ @JavascriptInterface
299
+ fun setVideoStreamTorch(optionsRaw: String?) {
300
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream torch") {
301
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
302
+ val on = options.optBoolean("on", false)
303
+ android.util.Log.d("NativeBridge", "setVideoStreamTorch — on=$on")
304
+ mainActivity.runOnUiThread {
305
+ nativeCameraManager?.setTorch(on)
306
+ }
307
+ }
308
+ }
309
+
310
+ @JavascriptInterface
311
+ fun setVideoStreamFps(optionsRaw: String?) {
312
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream fps") {
313
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
314
+ val min = if (options.isNull("min")) null else options.optInt("min")
315
+ val max = if (options.isNull("max")) null else options.optInt("max")
316
+ android.util.Log.d("NativeBridge", "setVideoStreamFps — min=$min max=$max")
317
+ mainActivity.runOnUiThread {
318
+ nativeCameraManager?.setFps(min, max)
319
+ }
320
+ }
321
+ }
322
+
323
+ @JavascriptInterface
324
+ fun flipVideoStream(optionsRaw: String?) {
325
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "flip video stream") {
326
+ android.util.Log.d("NativeBridge", "flipVideoStream called")
327
+ mainActivity.runOnUiThread {
328
+ nativeCameraManager?.flip()
329
+ }
330
+ }
331
+ }
332
+
242
333
  @JavascriptInterface
243
334
  fun openCamera(options: String?) {
244
335
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "open camera") {
@@ -1023,6 +1114,17 @@ class NativeBridge(
1023
1114
  * Handle permission request results
1024
1115
  */
1025
1116
  fun handlePermissionResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
1117
+ if (requestCode == NativeCameraManager.PERMISSION_REQUEST_CODE) {
1118
+ val granted = grantResults.isNotEmpty() &&
1119
+ grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED
1120
+ val cam = nativeCameraManager
1121
+ if (cam == null) {
1122
+ Log.w(TAG, "handlePermissionResult — nativeCameraManager is null, ignoring camera permission result")
1123
+ return
1124
+ }
1125
+ cam.onPermissionResult(granted)
1126
+ return
1127
+ }
1026
1128
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NOTIFICATION_PERMISSION_STATUS, "handle permission result") {
1027
1129
  // Delegate to notification manager which handles notification permissions
1028
1130
  notificationManager.getNotificationUtils().handlePermissionResult(requestCode, permissions, grantResults)
@@ -1133,6 +1235,45 @@ class NativeBridge(
1133
1235
  }
1134
1236
  }
1135
1237
 
1238
+ @JavascriptInterface
1239
+ fun startTransition(optionsRaw: String?) {
1240
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "start transition") {
1241
+ val options = try {
1242
+ JSONObject(optionsRaw ?: "{}")
1243
+ } catch (e: Exception) {
1244
+ Log.w(TAG, "startTransition — malformed JSON, using defaults: $optionsRaw")
1245
+ JSONObject()
1246
+ }
1247
+
1248
+ val type = options.optString("type", "slide")
1249
+ val direction = options.optString("direction", "left")
1250
+ val duration = options.optInt("duration", 300).coerceIn(0, 2000)
1251
+ val timeout = options.optInt("timeout", maxOf(duration * 3, 800))
1252
+
1253
+ mainActivity.runOnUiThread {
1254
+ transitionManager.startTransition(type, direction, duration, timeout)
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ @JavascriptInterface
1260
+ fun commitTransition(optionsRaw: String?) {
1261
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "commit transition") {
1262
+ mainActivity.runOnUiThread {
1263
+ transitionManager.commitTransition()
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ @JavascriptInterface
1269
+ fun cancelTransition(optionsRaw: String?) {
1270
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "cancel transition") {
1271
+ mainActivity.runOnUiThread {
1272
+ transitionManager.cancelTransition()
1273
+ }
1274
+ }
1275
+ }
1276
+
1136
1277
  @JavascriptInterface
1137
1278
  fun getSafeArea(data: String? = null) {
1138
1279
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_SAFE_AREA_INSETS_UPDATED, "get safe area") {
@@ -1164,6 +1305,13 @@ class NativeBridge(
1164
1305
  // Cleanup NotificationManager
1165
1306
  notificationManager.cleanup()
1166
1307
 
1308
+ // Cancel any in-flight transition and release overlay
1309
+ transitionManager.cleanup()
1310
+
1311
+ // Stop video stream
1312
+ nativeCameraManager?.cleanup()
1313
+ nativeCameraManager = null
1314
+
1167
1315
  // Stop network monitoring
1168
1316
  networkMonitor?.stop()
1169
1317
  networkMonitor = null