catalyst-core 0.1.0-canary.9 → 0.2.0-beta.2

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 (148) hide show
  1. package/README.md +1 -1
  2. package/bin/catalyst.js +8 -1
  3. package/changelog.md +6 -0
  4. package/dist/native/androidProject/app/build.gradle.kts +34 -8
  5. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
  28. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  29. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  30. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
  31. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  32. package/dist/native/androidSetup.js +1 -0
  33. package/dist/native/bridge/WebBridge.js +5 -5
  34. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  35. package/dist/native/bridge/hooks/index.js +1 -0
  36. package/dist/native/bridge/hooks/useCamera.js +9 -0
  37. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  38. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  39. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  40. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  41. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  42. package/dist/native/bridge/hooks/useIntent.js +1 -0
  43. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  44. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  45. package/dist/native/bridge/hooks/useNotification.js +2 -0
  46. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  47. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  48. package/dist/native/bridge/hooks/utils.js +1 -0
  49. package/dist/native/bridge/hooks.js +1 -137
  50. package/dist/native/bridge/useBaseHook.js +12 -7
  51. package/dist/native/bridge/utils/NativeBridge.js +41 -6
  52. package/dist/native/buildAndroid/assets.js +52 -0
  53. package/dist/native/buildAndroid/build.js +19 -0
  54. package/dist/native/buildAndroid/config.js +1 -0
  55. package/dist/native/buildAndroid/index.js +1 -0
  56. package/dist/native/buildAppAndroid.js +2 -150
  57. package/dist/native/buildAppIos.js +2 -312
  58. package/dist/native/buildIos/assets.js +8 -0
  59. package/dist/native/buildIos/build.js +40 -0
  60. package/dist/native/buildIos/config.js +71 -0
  61. package/dist/native/buildIos/index.js +6 -0
  62. package/dist/native/buildIos/plugins.js +45 -0
  63. package/dist/native/internalPluginUtils.js +7 -0
  64. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  66. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  67. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  68. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  69. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  70. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  71. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  72. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  73. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  74. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  75. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
  76. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  77. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  78. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  89. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
  90. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
  91. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
  92. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  93. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  94. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
  95. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
  96. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  97. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  98. package/dist/native/pluginComposerAndroid.js +28 -0
  99. package/dist/native/pluginComposerIos.js +16 -0
  100. package/dist/native/renameAndroidProject.js +5 -5
  101. package/dist/native/setupEmulatorIos.js +6 -4
  102. package/dist/native/setupServer.js +2 -3
  103. package/dist/native/utils.js +4 -1
  104. package/dist/offline/catalyst-sw.js +1 -0
  105. package/dist/offline/registerServiceWorker.js +1 -0
  106. package/dist/otel.js +70 -2
  107. package/dist/router/ClientRouter.js +1 -1
  108. package/dist/scripts/build.js +1 -1
  109. package/dist/scripts/cleanCache.js +1 -0
  110. package/dist/scripts/devBuild.js +1 -1
  111. package/dist/scripts/devServe.js +1 -1
  112. package/dist/scripts/generateOfflineManifest.js +1 -0
  113. package/dist/scripts/loadEnvironmentVariables.js +1 -2
  114. package/dist/scripts/plugins.js +1 -0
  115. package/dist/scripts/registerAliases.js +6 -2
  116. package/dist/scripts/scriptUtils.js +2 -1
  117. package/dist/scripts/serve.js +2 -1
  118. package/dist/scripts/start.js +7 -2
  119. package/dist/server/expressServer.js +4 -8
  120. package/dist/server/renderer/document/Body.js +4 -4
  121. package/dist/server/renderer/document/Head.js +2 -2
  122. package/dist/server/renderer/extract.js +31 -27
  123. package/dist/server/renderer/handler.js +47 -24
  124. package/dist/server/renderer/index.js +1 -1
  125. package/dist/server/startServer.js +57 -7
  126. package/dist/server/utils/userAgentUtil.js +9 -3
  127. package/dist/server/utils/validator.js +1 -1
  128. package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
  129. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
  130. package/dist/webpack/babel.config.client.js +1 -1
  131. package/dist/webpack/babel.config.ssr.js +1 -1
  132. package/dist/webpack/base.babel.js +7 -8
  133. package/dist/webpack/development.client.babel.js +7 -16
  134. package/dist/webpack/production.client.babel.js +1 -1
  135. package/dist/webpack/production.ssr.babel.js +5 -1
  136. package/mcp_v2/knowledge-base.json +206 -100
  137. package/mcp_v2/lib/helpers.js +42 -8
  138. package/mcp_v2/lib/seed.js +88 -0
  139. package/mcp_v2/mcp.js +150 -4
  140. package/mcp_v2/setup.js +30 -193
  141. package/mcp_v2/tools/build.js +1 -1
  142. package/mcp_v2/tools/config.js +16 -4
  143. package/mcp_v2/tools/conversion.js +11 -11
  144. package/mcp_v2/tools/github.js +1117 -0
  145. package/mcp_v2/tools/knowledge.js +16 -3
  146. package/mcp_v2/tools/sync.js +179 -10
  147. package/mcp_v2/tools/tasks.js +13 -4
  148. package/package.json +20 -8
@@ -7,6 +7,7 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app"
7
7
  public struct WebView: UIViewRepresentable, Equatable {
8
8
  let urlString: String
9
9
  @ObservedObject var viewModel: WebViewModel
10
+ let cameraManager: NativeCameraManager
10
11
  private let navigationDelegate: WebViewNavigationDelegate
11
12
 
12
13
  // Equatable conformance - only recreate if URL changes
@@ -14,12 +15,13 @@ public struct WebView: UIViewRepresentable, Equatable {
14
15
  return lhs.urlString == rhs.urlString
15
16
  }
16
17
 
17
- public init(urlString: String, viewModel: WebViewModel) {
18
+ public init(urlString: String, viewModel: WebViewModel, cameraManager: NativeCameraManager) {
18
19
  let start = CFAbsoluteTimeGetCurrent()
19
20
  self.urlString = urlString
20
21
  self.viewModel = viewModel
22
+ self.cameraManager = cameraManager
21
23
  let initialURL = URL(string: urlString)
22
- self.navigationDelegate = WebViewNavigationDelegate(viewModel: viewModel, initialURL: initialURL)
24
+ self.navigationDelegate = WebViewNavigationDelegate(viewModel: viewModel, initialURL: initialURL, cameraManager: cameraManager)
23
25
 
24
26
  // Register our custom URL protocol for caching
25
27
  let protocolStart = CFAbsoluteTimeGetCurrent()
@@ -36,13 +38,18 @@ public struct WebView: UIViewRepresentable, Equatable {
36
38
  logWithTimestamp("🔨 makeUIView() started")
37
39
 
38
40
  let configuration = WKWebViewConfiguration()
41
+ configuration.setURLSchemeHandler(
42
+ OfflineURLSchemeHandler.shared,
43
+ forURLScheme: OfflineCacheService.offlineHTTPScheme
44
+ )
45
+ configuration.setURLSchemeHandler(
46
+ OfflineURLSchemeHandler.shared,
47
+ forURLScheme: OfflineCacheService.offlineHTTPSScheme
48
+ )
39
49
 
40
50
  // Hook kept for legacy behavior; currently a no-op on iOS 15+.
41
51
  WebKitConfig.applySharedProcessPoolIfNeeded(to: configuration)
42
52
 
43
- #if DEBUG
44
- configuration.preferences.setValue(true, forKey: "developerExtrasEnabled")
45
- #endif
46
53
  let preferences = WKWebpagePreferences()
47
54
  preferences.allowsContentJavaScript = true
48
55
  configuration.defaultWebpagePreferences = preferences
@@ -54,6 +61,15 @@ public struct WebView: UIViewRepresentable, Equatable {
54
61
 
55
62
  webView.navigationDelegate = navigationDelegate
56
63
  webView.allowsBackForwardNavigationGestures = true
64
+ webView.isOpaque = false
65
+ webView.backgroundColor = .clear
66
+ webView.scrollView.backgroundColor = .clear
67
+
68
+ // Add our pinch recognizer to route camera zoom when streaming.
69
+ // Web page zoom is disabled via user-scalable=no in the viewport meta tag.
70
+ let pinchGesture = UIPinchGestureRecognizer(target: context.coordinator,
71
+ action: #selector(Coordinator.handleCameraPinch(_:)))
72
+ webView.addGestureRecognizer(pinchGesture)
57
73
 
58
74
  #if DEBUG
59
75
  // Enable Safari Web Inspector (only available in iOS 16.4+)
@@ -90,7 +106,13 @@ public struct WebView: UIViewRepresentable, Equatable {
90
106
  logWithTimestamp("✅ webView.load() returned (took \(String(format: "%.2f", loadTime))ms)")
91
107
  } else {
92
108
  logWithTimestamp("📴 Device offline on launch, showing offline page")
93
- _ = navigationDelegate.showOfflinePage(in: webView)
109
+ if OfflineCacheService.shared.loadSnapshot(in: webView, for: url) {
110
+ viewModel.setLoading(false, fromCache: true)
111
+ } else if navigationDelegate.showOfflinePage(in: webView) {
112
+ viewModel.setLoading(false, fromCache: true)
113
+ } else {
114
+ viewModel.setLoading(false, fromCache: false)
115
+ }
94
116
  }
95
117
  }
96
118
 
@@ -101,7 +123,11 @@ public struct WebView: UIViewRepresentable, Equatable {
101
123
  }
102
124
 
103
125
  public func updateUIView(_ webView: WKWebView, context: Context) {
104
- // Intentionally empty to prevent reloading
126
+ #if DEBUG
127
+ if #available(iOS 16.4, *) {
128
+ webView.isInspectable = true
129
+ }
130
+ #endif
105
131
  }
106
132
 
107
133
  public func makeCoordinator() -> Coordinator {
@@ -121,6 +147,8 @@ public struct WebView: UIViewRepresentable, Equatable {
121
147
  // Clean up native bridge
122
148
  coordinator.nativeBridge?.unregister()
123
149
  coordinator.nativeBridge = nil
150
+ coordinator.pluginBridge?.unregister()
151
+ coordinator.pluginBridge = nil
124
152
  coordinator.hostingController = nil
125
153
 
126
154
  // Unregister custom URL protocol
@@ -132,6 +160,7 @@ public struct WebView: UIViewRepresentable, Equatable {
132
160
  public class Coordinator: NSObject {
133
161
  var parent: WebView
134
162
  var nativeBridge: NativeBridge?
163
+ var pluginBridge: PluginBridge?
135
164
  var hostingController: UIViewController?
136
165
  var isObserverAdded = false
137
166
 
@@ -145,7 +174,8 @@ public struct WebView: UIViewRepresentable, Equatable {
145
174
  self.hostingController = hostingController
146
175
 
147
176
  // Create and register the native bridge
148
- let bridge = NativeBridge(webView: webView, viewController: hostingController)
177
+ let bridge = NativeBridge(webView: webView, viewController: hostingController, cameraManager: parent.cameraManager)
178
+ let pluginBridge = PluginBridge(webView: webView, viewController: hostingController)
149
179
 
150
180
  // Inject WebViewModel for safe area handling
151
181
  Task { @MainActor in
@@ -156,9 +186,24 @@ public struct WebView: UIViewRepresentable, Equatable {
156
186
  bridge.setNotificationHandler(NotificationHandlerProvider.shared)
157
187
 
158
188
  bridge.register()
189
+ pluginBridge.register()
159
190
  self.nativeBridge = bridge
191
+ self.pluginBridge = pluginBridge
160
192
  }
161
193
 
194
+ @objc func handleCameraPinch(_ gesture: UIPinchGestureRecognizer) {
195
+ // No-op when camera is not streaming — don't accidentally zoom the page
196
+ guard parent.cameraManager.isStreaming else { return }
197
+ switch gesture.state {
198
+ case .began:
199
+ parent.cameraManager.handlePinchBegan()
200
+ case .changed:
201
+ parent.cameraManager.handlePinchChanged(scale: gesture.scale)
202
+ default:
203
+ break
204
+ }
205
+ }
206
+
162
207
  override public func observeValue(forKeyPath keyPath: String?,
163
208
  of object: Any?,
164
209
  change: [NSKeyValueChangeKey : Any]?,
@@ -9,13 +9,16 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
9
9
  private let offlineFileName = "offline.html"
10
10
  private let offlineSubdirectory = "offline"
11
11
  private var offlinePageVisible = false
12
+ private var offlineSnapshotVisibleURL: String?
12
13
  private var lastTargetURL: URL?
13
14
  private let initialURL: URL?
14
-
15
- init(viewModel: WebViewModel, initialURL: URL?) {
15
+ private weak var cameraManager: NativeCameraManager?
16
+
17
+ init(viewModel: WebViewModel, initialURL: URL?, cameraManager: NativeCameraManager? = nil) {
16
18
  self.viewModel = viewModel
17
19
  self.initialURL = initialURL
18
20
  self.lastTargetURL = initialURL
21
+ self.cameraManager = cameraManager
19
22
  super.init()
20
23
  }
21
24
 
@@ -44,6 +47,7 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
44
47
  // Only inject on main frame to match Android behavior (not on subresources)
45
48
  let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? false
46
49
  let isHttpScheme = ["http", "https"].contains(url.scheme?.lowercased() ?? "")
50
+ let isCatalystOfflineURL = OfflineCacheService.shared.originalURL(forOfflineURL: url) != nil
47
51
 
48
52
  if isMainFrame && isHttpScheme && httpMethod == "GET" {
49
53
  let safeAreaHeaders = viewModel.getSafeAreaHeaders()
@@ -84,7 +88,7 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
84
88
  return
85
89
  }
86
90
 
87
- if URLWhitelistManager.shared.isAccessControlEnabled {
91
+ if URLWhitelistManager.shared.isAccessControlEnabled && !isCatalystOfflineURL {
88
92
 
89
93
  // Check if URL is an external domain
90
94
  let isExternal = URLWhitelistManager.shared.isExternalDomain(url)
@@ -105,8 +109,10 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
105
109
  }
106
110
 
107
111
  logger.info("✅ URL passed whitelist checks, allowing navigation: \(url.absoluteString)")
108
- } else {
112
+ } else if !isCatalystOfflineURL {
109
113
  logger.info("⚠️ Access control disabled, allowing all navigation: \(url.absoluteString)")
114
+ } else {
115
+ logger.info("📴 Catalyst offline URL allowed: \(url.absoluteString)")
110
116
  }
111
117
 
112
118
  if ["http", "https"].contains(url.scheme?.lowercased() ?? "") {
@@ -118,6 +124,36 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
118
124
  let isCacheableMethod = httpMethod == "GET"
119
125
  logger.info("🔍 Cache check - Method: \(httpMethod), isCacheable: \(isCacheableMethod)")
120
126
 
127
+ if isMainFrame && isHttpScheme && isCacheableMethod {
128
+ if NetworkMonitor.shared.currentStatus.isOnline {
129
+ let isOfflineRoute = await OfflineCacheService.shared.prepareActiveOfflineRoute(
130
+ for: navigationAction.request,
131
+ webView: webView
132
+ )
133
+ offlineSnapshotVisibleURL = nil
134
+ if !isOfflineRoute {
135
+ logger.info("⏭️ Main-frame route is not currently offline eligible: \(url.absoluteString)")
136
+ }
137
+ OfflineCacheService.shared.storeRouteSnapshot(
138
+ for: navigationAction.request,
139
+ webView: webView
140
+ )
141
+ } else {
142
+ let loadedSnapshot = await MainActor.run {
143
+ loadCachedSnapshotIfNeeded(in: webView, for: url)
144
+ }
145
+
146
+ if loadedSnapshot {
147
+ logger.info("📴 Serving cached offline route snapshot: \(url.absoluteString)")
148
+ decisionHandler(.cancel)
149
+ return
150
+ }
151
+ OfflineCacheService.shared.clearActiveOfflineRoute()
152
+ }
153
+ } else if isMainFrame && isHttpScheme {
154
+ OfflineCacheService.shared.clearActiveOfflineRoute()
155
+ }
156
+
121
157
  if isCacheableMethod && CacheManager.shared.shouldCacheURL(url) {
122
158
  logger.info("🎯 URL matches cache pattern: \(url.absoluteString)")
123
159
 
@@ -199,6 +235,8 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
199
235
 
200
236
  func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
201
237
  logWithTimestamp("📡 didStartProvisionalNavigation - loading started")
238
+ // Stop camera on navigation — mirrors Android CustomWebview onPageStarted lambda
239
+ cameraManager?.stop()
202
240
  Task { @MainActor in
203
241
  if !isOfflinePageURL(webView.url) {
204
242
  offlinePageVisible = false
@@ -255,6 +293,18 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
255
293
  logger.error("Navigation failed: \(error.localizedDescription)")
256
294
 
257
295
  guard shouldShowOfflinePage(for: error), let webView else { return }
296
+ if let targetURL = lastTargetURL ?? webView.url,
297
+ isOfflineSnapshotVisible(for: targetURL) {
298
+ logger.info("📴 Ignoring repeated error for visible offline route snapshot")
299
+ return
300
+ }
301
+
302
+ if let targetURL = lastTargetURL ?? webView.url,
303
+ loadCachedSnapshotIfNeeded(in: webView, for: targetURL) {
304
+ logger.info("📴 Showing cached offline route snapshot")
305
+ return
306
+ }
307
+
258
308
  if showOfflinePage(in: webView) {
259
309
  logger.info("📴 Showing offline fallback page")
260
310
  } else {
@@ -316,6 +366,8 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
316
366
  }
317
367
 
318
368
  offlinePageVisible = false
369
+ offlineSnapshotVisibleURL = nil
370
+ OfflineCacheService.shared.clearActiveOfflineRoute()
319
371
  logWithTimestamp("🔄 Retry requested, online. Reloading: \(targetURL.absoluteString)")
320
372
  webView.load(URLRequest(url: targetURL))
321
373
  }
@@ -347,8 +399,11 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
347
399
  }
348
400
 
349
401
  offlinePageVisible = true
402
+ offlineSnapshotVisibleURL = nil
403
+ OfflineCacheService.shared.clearActiveOfflineRoute()
350
404
  let readAccessURL = offlineURL.deletingLastPathComponent()
351
405
  webView.loadFileURL(offlineURL, allowingReadAccessTo: readAccessURL)
406
+ viewModel.setLoading(false, fromCache: true)
352
407
  return true
353
408
  }
354
409
 
@@ -392,4 +447,33 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
392
447
 
393
448
  return true
394
449
  }
450
+
451
+ private func loadCachedSnapshotIfNeeded(in webView: WKWebView, for url: URL) -> Bool {
452
+ let key = snapshotKey(for: url)
453
+ if offlineSnapshotVisibleURL == key {
454
+ return true
455
+ }
456
+
457
+ if OfflineCacheService.shared.loadSnapshot(in: webView, for: url) {
458
+ offlinePageVisible = false
459
+ offlineSnapshotVisibleURL = key
460
+ viewModel.setLoading(false, fromCache: true)
461
+ return true
462
+ }
463
+
464
+ offlineSnapshotVisibleURL = nil
465
+ return false
466
+ }
467
+
468
+ private func isOfflineSnapshotVisible(for url: URL) -> Bool {
469
+ offlineSnapshotVisibleURL == snapshotKey(for: url)
470
+ }
471
+
472
+ private func snapshotKey(for url: URL) -> String {
473
+ guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
474
+ return url.absoluteString
475
+ }
476
+ components.fragment = nil
477
+ return components.url?.absoluteString ?? url.absoluteString
478
+ }
395
479
  }
@@ -7,26 +7,44 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app"
7
7
 
8
8
  public struct ContentView: View {
9
9
  @StateObject private var webViewModel = WebViewModel()
10
-
10
+ // @StateObject ensures cameraManager survives SwiftUI view rebuilds.
11
+ @StateObject private var cameraManager = NativeCameraManager(
12
+ onEvent: { _, _ in }, // placeholder; real handler set in NativeBridge
13
+ onError: { _ in }
14
+ )
15
+
16
+ private static var startURL: String {
17
+ let base = ConfigConstants.url
18
+ let initial = ConfigConstants.initial_url
19
+ guard !initial.isEmpty else { return base }
20
+ let path = initial.hasPrefix("/") ? initial : "/\(initial)"
21
+ return base + path
22
+ }
23
+
11
24
  public init() {}
12
25
 
13
26
  public var body: some View {
14
27
  ZStack {
28
+ // Camera preview layer — sits behind the WebView (index 0)
29
+ CameraPreviewView(cameraManager: cameraManager)
30
+ .ignoresSafeArea()
31
+ .allowsHitTesting(false) // touches pass through to WebView
32
+
15
33
  // Normal remote URL - isolated from state changes
16
34
  // Conditionally apply edge-to-edge based on config (matches Android behavior)
17
35
  if ConfigConstants.EdgeToEdge.enabled {
18
- WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel)
36
+ WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
19
37
  .ignoresSafeArea()
20
38
  .onAppear {
21
- logger.info("WebView appeared with URL: \(ConfigConstants.url) [Edge-to-edge: enabled]")
39
+ logger.info("WebView appeared with URL: \(ContentView.startURL) [Edge-to-edge: enabled]")
22
40
  }
23
41
  } else {
24
- WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel)
42
+ WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
25
43
  .onAppear {
26
- logger.info("WebView appeared with URL: \(ConfigConstants.url) [Edge-to-edge: disabled, respecting safe areas]")
44
+ logger.info("WebView appeared with URL: \(ContentView.startURL) [Edge-to-edge: disabled]")
27
45
  }
28
46
  }
29
-
47
+
30
48
  // Show splash screen if enabled in configuration
31
49
  if ConfigConstants.splashScreenEnabled {
32
50
  SplashView(webViewModel: webViewModel).zIndex(1)
@@ -36,11 +54,11 @@ public struct ContentView: View {
36
54
  ProgressView()
37
55
  .scaleEffect(1.5)
38
56
  .progressViewStyle(CircularProgressViewStyle(tint: .blue))
39
-
57
+
40
58
  Text("\(Int(webViewModel.loadingProgress * 100))%")
41
59
  .foregroundColor(.blue)
42
60
  .padding(.top, 8)
43
-
61
+
44
62
  if webViewModel.isLoadingFromCache {
45
63
  Text("Loading from cache...")
46
64
  .foregroundColor(.blue)
@@ -62,9 +80,10 @@ public struct ContentView: View {
62
80
  struct WebViewContainer: View {
63
81
  let urlString: String
64
82
  @ObservedObject var viewModel: WebViewModel
83
+ let cameraManager: NativeCameraManager
65
84
 
66
85
  var body: some View {
67
- WebView(urlString: urlString, viewModel: viewModel)
86
+ WebView(urlString: urlString, viewModel: viewModel, cameraManager: cameraManager)
68
87
  .onAppear {
69
88
  logWithTimestamp("🌐 WebView appeared with URL: \(urlString)")
70
89
  }
@@ -100,11 +119,11 @@ struct LoadingOverlay: View {
100
119
  // UIViewControllerRepresentable for hosting native view controllers
101
120
  struct HostingController: UIViewControllerRepresentable {
102
121
  var viewController: UIViewController
103
-
122
+
104
123
  func makeUIViewController(context: Context) -> UIViewController {
105
124
  return viewController
106
125
  }
107
-
126
+
108
127
  func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
109
128
  // No update needed
110
129
  }
@@ -2,27 +2,21 @@
2
2
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
3
  <plist version="1.0">
4
4
  <dict>
5
+ <key>FirebaseAppDelegateProxyEnabled</key>
6
+ <false/>
5
7
  <key>NSAppTransportSecurity</key>
6
8
  <dict>
7
9
  <key>NSAllowsArbitraryLoads</key>
8
10
  <true/>
9
11
  </dict>
10
- <key>NSLocalNetworkUsageDescription</key>
11
- <string>Debug web content in Safari Web Inspector</string>
12
- <key>NSCameraUsageDescription</key>
13
- <string>This app requires camera access to capture images</string>
14
- <key>NSPhotoLibraryUsageDescription</key>
15
- <string>This app requires photo library access to select images from your gallery</string>
16
- <key>NSUserNotificationUsageDescription</key>
17
- <string>This app needs notification permission to send you important updates</string>
18
- <key>NSLocalNotificationUsageDescription</key>
19
- <string>This app uses notifications to alert you about important events</string>
20
- <key>UIBackgroundModes</key>
21
- <array>
22
- <string>remote-notification</string>
23
- <string>background-fetch</string>
24
- </array>
25
- <key>FirebaseAppDelegateProxyEnabled</key>
26
- <false/>
12
+ <key>NSLocalNotificationUsageDescription</key>
13
+ <string>This app uses notifications to alert you about important events</string>
14
+ <key>NSUserNotificationUsageDescription</key>
15
+ <string>This app needs notification permission to send you important updates</string>
16
+ <key>UIBackgroundModes</key>
17
+ <array>
18
+ <string>remote-notification</string>
19
+ <string>background-fetch</string>
20
+ </array>
27
21
  </dict>
28
22
  </plist>
@@ -23,6 +23,7 @@
23
23
  E9F699E02EE065B0005E972E /* NotificationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F699DF2EE065B0005E972E /* NotificationHandlerTests.swift */; };
24
24
  E9F699E22EE06650005E972E /* FrameworkServerUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F699E12EE06650005E972E /* FrameworkServerUtilsTests.swift */; };
25
25
  E9F699E42EE0696A005E972E /* BootTimingUtilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F699E32EE0696A005E972E /* BootTimingUtilityTests.swift */; };
26
+ F2B100012F11111100AAA001 /* PluginBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B100022F11111100AAA001 /* PluginBridgeTests.swift */; };
26
27
  F1234AC22E990180008C7F58 /* localhost.p12 in Resources */ = {isa = PBXBuildFile; fileRef = F1234AC32E990180008C7F58 /* localhost.p12 */; };
27
28
  /* End PBXBuildFile section */
28
29
 
@@ -55,7 +56,7 @@
55
56
  C99974382E97D69E00C25611 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
56
57
  C9DAB6492E950DDC00A02A44 /* iosnativeWebView.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iosnativeWebView.entitlements; sourceTree = "<group>"; };
57
58
  E90872B32E8D00D80076627C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
58
- E9F699CF2EDEFCF3005E972E /* iosnativeWebView.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = file; path = iosnativeWebView.xctestplan; sourceTree = "<group>"; };
59
+ E9F699CF2EDEFCF3005E972E /* iosnativeWebView.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iosnativeWebView.xctestplan; sourceTree = "<group>"; };
59
60
  E9F699D02EDEFEF2005E972E /* URLWhitelistManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLWhitelistManagerTests.swift; sourceTree = "<group>"; };
60
61
  E9F699D32EE05984005E972E /* ConfigMappingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMappingTests.swift; sourceTree = "<group>"; };
61
62
  E9F699D52EE05D74005E972E /* BridgeMessageValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgeMessageValidatorTests.swift; sourceTree = "<group>"; };
@@ -66,6 +67,7 @@
66
67
  E9F699DF2EE065B0005E972E /* NotificationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandlerTests.swift; sourceTree = "<group>"; };
67
68
  E9F699E12EE06650005E972E /* FrameworkServerUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameworkServerUtilsTests.swift; sourceTree = "<group>"; };
68
69
  E9F699E32EE0696A005E972E /* BootTimingUtilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootTimingUtilityTests.swift; sourceTree = "<group>"; };
70
+ F2B100022F11111100AAA001 /* PluginBridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginBridgeTests.swift; sourceTree = "<group>"; };
69
71
  F1234AC32E990180008C7F58 /* localhost.p12 */ = {isa = PBXFileReference; lastKnownFileType = file; path = localhost.p12; sourceTree = "<group>"; };
70
72
  XCCONFIG001000000001 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
71
73
  XCCONFIG001000000002 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
@@ -152,6 +154,7 @@
152
154
  E9F699DB2EE06482005E972E /* CacheManagerTests.swift */,
153
155
  E9F699D92EE063D5005E972E /* WebViewTests.swift */,
154
156
  E9F699D72EE06068005E972E /* NativeBridgeTests.swift */,
157
+ F2B100022F11111100AAA001 /* PluginBridgeTests.swift */,
155
158
  E9F699D52EE05D74005E972E /* BridgeMessageValidatorTests.swift */,
156
159
  E9F699D32EE05984005E972E /* ConfigMappingTests.swift */,
157
160
  E9F699D02EDEFEF2005E972E /* URLWhitelistManagerTests.swift */,
@@ -321,6 +324,7 @@
321
324
  E9F699DC2EE06483005E972E /* CacheManagerTests.swift in Sources */,
322
325
  E9F699D42EE05984005E972E /* ConfigMappingTests.swift in Sources */,
323
326
  E9F699D82EE06068005E972E /* NativeBridgeTests.swift in Sources */,
327
+ F2B100012F11111100AAA001 /* PluginBridgeTests.swift in Sources */,
324
328
  E9F699DA2EE063D5005E972E /* WebViewTests.swift in Sources */,
325
329
  E9F699E42EE0696A005E972E /* BootTimingUtilityTests.swift in Sources */,
326
330
  E9F699E22EE06650005E972E /* FrameworkServerUtilsTests.swift in Sources */,
@@ -478,6 +482,7 @@
478
482
  baseConfigurationReference = XCCONFIG001000000001 /* Debug.xcconfig */;
479
483
  buildSettings = {
480
484
  CODE_SIGN_ENTITLEMENTS = iosnativeWebView/iosnativeWebView.entitlements;
485
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = BHQQRZ69D3;
481
486
  INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera access to capture images";
482
487
  INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Debug web content in Safari Web Inspector";
483
488
  INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library access to select images from your gallery";
@@ -486,8 +491,9 @@
486
491
  INFOPLIST_KEY_UILaunchScreen_Generation = YES;
487
492
  INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
488
493
  INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
494
+ PRODUCT_BUNDLE_IDENTIFIER = com.1mg.pos;
489
495
  PRODUCT_NAME = "$(TARGET_NAME)";
490
- "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Pfizer Development Profile";
496
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Tata1mgPos;
491
497
  };
492
498
  name = Debug;
493
499
  };
@@ -200,7 +200,7 @@ final class BootTimingUtilityTests: XCTestCase {
200
200
  }
201
201
  }
202
202
 
203
- waitForExpectations(timeout: 5.0) { error in
203
+ waitForExpectations(timeout: 10.0) { error in
204
204
  XCTAssertNil(error, "Concurrent logging should complete without errors")
205
205
  }
206
206
  }
@@ -250,10 +250,9 @@ final class ConfigMappingTests: XCTestCase {
250
250
  // This test validates that ConfigConstants structure is correctly defined
251
251
 
252
252
  // Verify String fields are accessible and non-nil
253
- let stringFields = [
253
+ let stringFields: [String] = [
254
254
  ConfigConstants.url,
255
255
  ConfigConstants.LOCAL_IP,
256
- ConfigConstants.port,
257
256
  ConfigConstants.appInfo,
258
257
  ConfigConstants.appBundleId,
259
258
  ConfigConstants.appName,
@@ -266,6 +265,9 @@ final class ConfigMappingTests: XCTestCase {
266
265
  XCTAssertNotNil(field, "String field should not be nil")
267
266
  }
268
267
 
268
+ // Verify Int fields are accessible
269
+ _ = ConfigConstants.port
270
+
269
271
  // Verify Array fields are accessible
270
272
  XCTAssertNotNil(ConfigConstants.cachePattern, "cachePattern should be accessible")
271
273
  XCTAssertNotNil(ConfigConstants.allowedUrls, "allowedUrls should be accessible")