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.
- package/README.md +1 -1
- package/bin/catalyst.js +8 -1
- package/changelog.md +6 -0
- package/dist/native/androidProject/app/build.gradle.kts +34 -8
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
- package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -0
- package/dist/native/bridge/WebBridge.js +5 -5
- package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
- package/dist/native/bridge/hooks/index.js +1 -0
- package/dist/native/bridge/hooks/useCamera.js +9 -0
- package/dist/native/bridge/hooks/useDataProtection.js +2 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
- package/dist/native/bridge/hooks/useFilePicker.js +2 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
- package/dist/native/bridge/hooks/useIntent.js +1 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
- package/dist/native/bridge/hooks/useNotification.js +2 -0
- package/dist/native/bridge/hooks/useSafeArea.js +2 -0
- package/dist/native/bridge/hooks/useVideoStream.js +6 -0
- package/dist/native/bridge/hooks/utils.js +1 -0
- package/dist/native/bridge/hooks.js +1 -137
- package/dist/native/bridge/useBaseHook.js +12 -7
- package/dist/native/bridge/utils/NativeBridge.js +41 -6
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +19 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -150
- package/dist/native/buildAppIos.js +2 -312
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +40 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/internalPluginUtils.js +7 -0
- package/dist/native/iosnativeWebView/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +28 -0
- package/dist/native/pluginComposerIos.js +16 -0
- package/dist/native/renameAndroidProject.js +5 -5
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/otel.js +70 -2
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/cleanCache.js +1 -0
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/scripts/loadEnvironmentVariables.js +1 -2
- package/dist/scripts/plugins.js +1 -0
- package/dist/scripts/registerAliases.js +6 -2
- package/dist/scripts/scriptUtils.js +2 -1
- package/dist/scripts/serve.js +2 -1
- package/dist/scripts/start.js +7 -2
- package/dist/server/expressServer.js +4 -8
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/document/Head.js +2 -2
- package/dist/server/renderer/extract.js +31 -27
- package/dist/server/renderer/handler.js +47 -24
- package/dist/server/renderer/index.js +1 -1
- package/dist/server/startServer.js +57 -7
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
- package/dist/webpack/babel.config.client.js +1 -1
- package/dist/webpack/babel.config.ssr.js +1 -1
- package/dist/webpack/base.babel.js +7 -8
- package/dist/webpack/development.client.babel.js +7 -16
- package/dist/webpack/production.client.babel.js +1 -1
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +206 -100
- package/mcp_v2/lib/helpers.js +42 -8
- package/mcp_v2/lib/seed.js +88 -0
- package/mcp_v2/mcp.js +150 -4
- package/mcp_v2/setup.js +30 -193
- package/mcp_v2/tools/build.js +1 -1
- package/mcp_v2/tools/config.js +16 -4
- package/mcp_v2/tools/conversion.js +11 -11
- package/mcp_v2/tools/github.js +1117 -0
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/sync.js +179 -10
- package/mcp_v2/tools/tasks.js +13 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
36
|
+
WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
|
|
19
37
|
.ignoresSafeArea()
|
|
20
38
|
.onAppear {
|
|
21
|
-
logger.info("WebView appeared with URL: \(
|
|
39
|
+
logger.info("WebView appeared with URL: \(ContentView.startURL) [Edge-to-edge: enabled]")
|
|
22
40
|
}
|
|
23
41
|
} else {
|
|
24
|
-
WebViewContainer(urlString:
|
|
42
|
+
WebViewContainer(urlString: ContentView.startURL, viewModel: webViewModel, cameraManager: cameraManager)
|
|
25
43
|
.onAppear {
|
|
26
|
-
logger.info("WebView appeared with URL: \(
|
|
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>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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*]" =
|
|
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:
|
|
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")
|