catalyst-core 0.0.3-canary.9 → 0.1.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 +4 -5
- package/bin/catalyst.js +28 -15
- package/changelog.md +139 -30
- package/dist/native/androidProject/app/build.gradle.kts +125 -15
- package/dist/native/androidProject/app/proguard-rules.pro +85 -14
- package/dist/native/androidProject/app/src/debug/res/xml/network_security_config.xml +8 -0
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +10 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +552 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +109 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +605 -124
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +405 -47
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +341 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +954 -71
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +111 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +49 -55
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +81 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +47 -11
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +38 -13
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +36 -14
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FileUtils.kt +3 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +57 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/IntentUtils.kt +31 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +82 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +28 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +639 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/SafeAreaUtils.kt +62 -0
- package/dist/native/androidProject/app/src/main/res/drawable/splashscreen_fallback.png +0 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +1 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_splash.xml +20 -0
- package/dist/native/androidProject/app/src/main/res/values/themes.xml +23 -0
- package/dist/native/androidProject/app/src/main/res/values-night/themes.xml +11 -0
- package/dist/native/androidProject/app/src/main/res/xml/network_security_config.xml +12 -0
- package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +166 -0
- package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +94 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeMessageValidatorTest.kt +267 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +224 -0
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +399 -0
- package/dist/native/androidProject/build.gradle.kts +6 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +8 -2
- package/dist/native/androidSetup.js +4 -3
- package/dist/native/assets/catalyst.png +0 -0
- package/dist/native/bridge/WebBridge.js +16 -3
- package/dist/native/bridge/constants/NativeInterfaces.js +28 -10
- package/dist/native/bridge/errors.js +1 -1
- package/dist/native/bridge/hooks.js +76 -16
- package/dist/native/bridge/safeArea.js +5 -0
- package/dist/native/bridge/standardizedHooks.js +23 -5
- package/dist/native/bridge/useBaseHook.js +7 -9
- package/dist/native/bridge/utils/CameraUtils.js +58 -0
- package/dist/native/bridge/utils/FileObjectConverter.js +40 -0
- package/dist/native/bridge/utils/NativeBridge.js +57 -8
- package/dist/native/buildAppAndroid.js +115 -43
- package/dist/native/buildAppIos.js +291 -26
- package/dist/native/iosnativeWebView/Configurations/Debug.xcconfig +21 -0
- package/dist/native/iosnativeWebView/Configurations/Release.xcconfig +24 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +50 -0
- package/dist/native/iosnativeWebView/Package.resolved +32 -0
- package/dist/native/iosnativeWebView/Package.swift +37 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +293 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +299 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +199 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +125 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +70 -0
- package/dist/native/iosnativeWebView/Sources/Core/Notifications/NotificationModels.swift +156 -0
- package/dist/native/iosnativeWebView/Sources/Core/Protocols/NotificationHandlerProtocol.swift +35 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +26 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/CacheManager.swift +69 -36
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +363 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +67 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +1053 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FrameworkServerUtils.swift +901 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ImageHandler.swift +206 -20
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NotificationHandlerProvider.swift +20 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NullNotificationHandler.swift +85 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ResourceURLProtocol.swift +60 -25
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +132 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +224 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +43 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +991 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +166 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +340 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +149 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +611 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +334 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +95 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +86 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +181 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +182 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +395 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +84 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AccentColor.colorset/Contents.json +8 -8
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AppIcon.appiconset/Contents.json +10 -10
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/Contents.json +4 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +65 -10
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +21 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +13 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Preview Content/Preview Assets.xcassets/Contents.json +4 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +12 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +80 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +118 -100
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +69 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
- package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +47 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +391 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +440 -0
- package/dist/native/renameAndroidProject.js +80 -70
- package/dist/native/setupEmulatorIos.js +22 -5
- package/dist/native/setupServer.js +11 -10
- package/dist/native/terminalProgress.js +10 -10
- package/dist/native/utils.js +4 -5
- package/dist/otel.js +4 -0
- package/dist/scripts/build.js +2 -2
- package/dist/scripts/devBuild.js +2 -2
- package/dist/scripts/loadEnvironmentVariables.js +3 -1
- package/dist/scripts/scriptUtils.js +2 -2
- package/dist/sentry.js +4 -1
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/handler.js +7 -5
- package/dist/webpack/development.client.babel.js +1 -1
- package/mcp_v2/conversion-tasks.json +326 -0
- package/mcp_v2/knowledge-base.json +1068 -0
- package/mcp_v2/lib/helpers.js +165 -0
- package/mcp_v2/mcp.js +563 -0
- package/mcp_v2/package.json +13 -0
- package/mcp_v2/schema.sql +88 -0
- package/mcp_v2/setup.js +278 -0
- package/mcp_v2/tools/build.js +686 -0
- package/mcp_v2/tools/config.js +453 -0
- package/mcp_v2/tools/conversion.js +799 -0
- package/mcp_v2/tools/debug.js +113 -0
- package/mcp_v2/tools/knowledge.js +219 -0
- package/mcp_v2/tools/sync.js +23 -0
- package/mcp_v2/tools/tasks.js +945 -0
- package/package.json +11 -15
- package/dist/native/androidProject/.idea/.name +0 -1
- package/dist/native/androidProject/.idea/AndroidProjectSystem.xml +0 -6
- package/dist/native/androidProject/.idea/compiler.xml +0 -6
- package/dist/native/androidProject/.idea/deploymentTargetSelector.xml +0 -10
- package/dist/native/androidProject/.idea/gradle.xml +0 -18
- package/dist/native/androidProject/.idea/migrations.xml +0 -10
- package/dist/native/androidProject/.idea/misc.xml +0 -10
- package/dist/native/androidProject/.idea/runConfigurations.xml +0 -17
- package/dist/native/androidProject/.idea/vcs.xml +0 -6
- package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -217
- package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -96
- package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
- package/dist/native/iosnativeWebView/iosnativeWebView/WebViewNavigationDelegate.swift +0 -140
- package/license +0 -10
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//
|
|
2
|
+
// SafeAreaUtils.swift
|
|
3
|
+
// iosnativeWebView
|
|
4
|
+
//
|
|
5
|
+
// Safe area inset calculation utilities for iOS
|
|
6
|
+
// Equivalent to Android's SafeAreaUtils.kt
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import UIKit
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "SafeAreaUtils")
|
|
13
|
+
|
|
14
|
+
// MARK: - SafeAreaInsets Data Structure
|
|
15
|
+
|
|
16
|
+
/// Represents safe area insets for all edges of the screen
|
|
17
|
+
/// Matches Android's SafeAreaInsets data class
|
|
18
|
+
public struct SafeAreaInsets: Codable, Equatable, CustomStringConvertible {
|
|
19
|
+
public let top: CGFloat
|
|
20
|
+
public let right: CGFloat
|
|
21
|
+
public let bottom: CGFloat
|
|
22
|
+
public let left: CGFloat
|
|
23
|
+
|
|
24
|
+
/// Zero insets (no safe area)
|
|
25
|
+
public static let zero = SafeAreaInsets(top: 0, right: 0, bottom: 0, left: 0)
|
|
26
|
+
|
|
27
|
+
/// Public initializer
|
|
28
|
+
public init(top: CGFloat, right: CGFloat, bottom: CGFloat, left: CGFloat) {
|
|
29
|
+
self.top = top
|
|
30
|
+
self.right = right
|
|
31
|
+
self.bottom = bottom
|
|
32
|
+
self.left = left
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Convert to dictionary for JSON serialization
|
|
36
|
+
public func toDict() -> [String: Any] {
|
|
37
|
+
return [
|
|
38
|
+
"top": Int(top),
|
|
39
|
+
"right": Int(right),
|
|
40
|
+
"bottom": Int(bottom),
|
|
41
|
+
"left": Int(left)
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Convert to header dictionary for HTTP requests
|
|
46
|
+
public func toHeaders() -> [String: String] {
|
|
47
|
+
return [
|
|
48
|
+
"X-Safe-Area-Top": String(Int(top)),
|
|
49
|
+
"X-Safe-Area-Right": String(Int(right)),
|
|
50
|
+
"X-Safe-Area-Bottom": String(Int(bottom)),
|
|
51
|
+
"X-Safe-Area-Left": String(Int(left)),
|
|
52
|
+
// Prevent caching of SSR response so updated headers are always used
|
|
53
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
54
|
+
"Pragma": "no-cache"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Check if all insets are zero
|
|
59
|
+
public var isZero: Bool {
|
|
60
|
+
return top == 0 && right == 0 && bottom == 0 && left == 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// String representation for logging and debugging
|
|
64
|
+
public var description: String {
|
|
65
|
+
return "SafeAreaInsets(top: \(Int(top)), right: \(Int(right)), bottom: \(Int(bottom)), left: \(Int(left)))"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - SafeAreaUtils
|
|
70
|
+
|
|
71
|
+
public class SafeAreaUtils {
|
|
72
|
+
public static func getSafeAreaInsets(from window: UIWindow?, edgeToEdgeEnabled: Bool) -> SafeAreaInsets {
|
|
73
|
+
guard let window = window else {
|
|
74
|
+
#if DEBUG
|
|
75
|
+
logger.debug("⚠️ No window available, returning zero insets")
|
|
76
|
+
#endif
|
|
77
|
+
return .zero
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If edge-to-edge is disabled, SwiftUI is already respecting safe areas
|
|
81
|
+
// The system handles padding automatically, so return zero
|
|
82
|
+
// (app doesn't need to apply manual padding)
|
|
83
|
+
guard edgeToEdgeEnabled else {
|
|
84
|
+
#if DEBUG
|
|
85
|
+
logger.debug("🎨 Edge-to-edge disabled - SwiftUI respects safe areas, returning zero")
|
|
86
|
+
#endif
|
|
87
|
+
return .zero
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Edge-to-edge enabled: Content draws behind system UI
|
|
91
|
+
// Return actual insets so the app can apply padding manually
|
|
92
|
+
let safeArea = window.safeAreaInsets
|
|
93
|
+
|
|
94
|
+
#if DEBUG
|
|
95
|
+
logger.debug("📐 Window safe area insets - top: \(safeArea.top), right: \(safeArea.right), bottom: \(safeArea.bottom), left: \(safeArea.left)")
|
|
96
|
+
logger.debug("🎨 Edge-to-edge enabled - returning insets for manual padding")
|
|
97
|
+
#endif
|
|
98
|
+
|
|
99
|
+
// On iOS, safeAreaInsets already includes:
|
|
100
|
+
// - Status bar
|
|
101
|
+
// - Home indicator
|
|
102
|
+
// - Notch/Dynamic Island
|
|
103
|
+
// - Any other screen intrusions
|
|
104
|
+
//
|
|
105
|
+
// Unlike Android, we don't need to manually combine system bars and cutouts
|
|
106
|
+
|
|
107
|
+
let insets = SafeAreaInsets(
|
|
108
|
+
top: max(0, safeArea.top),
|
|
109
|
+
right: max(0, safeArea.right),
|
|
110
|
+
bottom: max(0, safeArea.bottom),
|
|
111
|
+
left: max(0, safeArea.left)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
#if DEBUG
|
|
115
|
+
logger.debug("✅ Computed safe area insets: \(insets)")
|
|
116
|
+
#endif
|
|
117
|
+
|
|
118
|
+
return insets
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Get safe area insets from the key window of a scene
|
|
122
|
+
/// Convenience method that finds the active window automatically
|
|
123
|
+
public static func getSafeAreaInsetsFromKeyWindow(edgeToEdgeEnabled: Bool) -> SafeAreaInsets {
|
|
124
|
+
// Get the key window from active scene
|
|
125
|
+
let window = UIApplication.shared.connectedScenes
|
|
126
|
+
.compactMap { $0 as? UIWindowScene }
|
|
127
|
+
.flatMap { $0.windows }
|
|
128
|
+
.first { $0.isKeyWindow }
|
|
129
|
+
|
|
130
|
+
return getSafeAreaInsets(from: window, edgeToEdgeEnabled: edgeToEdgeEnabled)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ScreenSecureManager.swift
|
|
2
|
+
// Manages screen security on iOS by overlaying a blank view when the app
|
|
3
|
+
// moves to the background — preventing the app switcher thumbnail from
|
|
4
|
+
// capturing sensitive content.
|
|
5
|
+
//
|
|
6
|
+
// iOS limitation: unlike Android FLAG_SECURE, iOS cannot block system or
|
|
7
|
+
// third-party screenshots. This only protects the app switcher snapshot.
|
|
8
|
+
|
|
9
|
+
import UIKit
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
private let secureLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "ScreenSecureManager")
|
|
13
|
+
|
|
14
|
+
final class ScreenSecureManager {
|
|
15
|
+
|
|
16
|
+
public static let shared = ScreenSecureManager()
|
|
17
|
+
private init() {
|
|
18
|
+
setupSceneObservers()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Track whether screen security is requested by the web layer
|
|
22
|
+
private(set) var isScreenSecure: Bool = false
|
|
23
|
+
|
|
24
|
+
// The overlay view installed over the window when backgrounding
|
|
25
|
+
private var overlayWindow: UIWindow?
|
|
26
|
+
// Guard flag set before async dispatch to prevent double-install race
|
|
27
|
+
private var overlayInstalling: Bool = false
|
|
28
|
+
|
|
29
|
+
// MARK: - Public API
|
|
30
|
+
|
|
31
|
+
/// Enable or disable screen-secure mode.
|
|
32
|
+
/// When enabled, a blank overlay is shown whenever the app moves to background
|
|
33
|
+
/// so the app-switcher snapshot does not capture sensitive content.
|
|
34
|
+
func setScreenSecure(_ enable: Bool) {
|
|
35
|
+
isScreenSecure = enable
|
|
36
|
+
secureLogger.debug("setScreenSecure called: enable=\(enable)")
|
|
37
|
+
if !enable {
|
|
38
|
+
removeOverlay()
|
|
39
|
+
}
|
|
40
|
+
// If enabling while already in background, install immediately
|
|
41
|
+
if enable && UIApplication.shared.applicationState == .background {
|
|
42
|
+
secureLogger.debug("setScreenSecure: app already in background, installing overlay immediately")
|
|
43
|
+
installOverlay()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// MARK: - Scene lifecycle
|
|
48
|
+
|
|
49
|
+
private func setupSceneObservers() {
|
|
50
|
+
NotificationCenter.default.addObserver(
|
|
51
|
+
self,
|
|
52
|
+
selector: #selector(sceneWillDeactivate),
|
|
53
|
+
name: UIScene.willDeactivateNotification,
|
|
54
|
+
object: nil
|
|
55
|
+
)
|
|
56
|
+
NotificationCenter.default.addObserver(
|
|
57
|
+
self,
|
|
58
|
+
selector: #selector(sceneDidActivate),
|
|
59
|
+
name: UIScene.didActivateNotification,
|
|
60
|
+
object: nil
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@objc private func sceneWillDeactivate() {
|
|
65
|
+
secureLogger.debug("sceneWillDeactivate fired: isScreenSecure=\(self.isScreenSecure)")
|
|
66
|
+
guard isScreenSecure else { return }
|
|
67
|
+
installOverlay()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@objc private func sceneDidActivate() {
|
|
71
|
+
secureLogger.debug("sceneDidActivate fired — removing overlay")
|
|
72
|
+
removeOverlay()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - Overlay management
|
|
76
|
+
|
|
77
|
+
private func installOverlay() {
|
|
78
|
+
// Set flag before async dispatch to prevent double-install race
|
|
79
|
+
guard overlayWindow == nil && !overlayInstalling else {
|
|
80
|
+
secureLogger.debug("installOverlay: skipped (already installing or installed)")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
overlayInstalling = true
|
|
84
|
+
secureLogger.debug("installOverlay: dispatching to main thread")
|
|
85
|
+
|
|
86
|
+
DispatchQueue.main.async { [weak self] in
|
|
87
|
+
guard let self else { return }
|
|
88
|
+
defer { self.overlayInstalling = false }
|
|
89
|
+
|
|
90
|
+
// Accept foregroundInactive too — that's the state during the
|
|
91
|
+
// willDeactivate transition on both simulator and device.
|
|
92
|
+
guard let scene = UIApplication.shared.connectedScenes
|
|
93
|
+
.first(where: {
|
|
94
|
+
$0.activationState == .foregroundActive ||
|
|
95
|
+
$0.activationState == .foregroundInactive ||
|
|
96
|
+
$0.activationState == .background
|
|
97
|
+
})
|
|
98
|
+
as? UIWindowScene else {
|
|
99
|
+
secureLogger.error("installOverlay: no suitable UIWindowScene found — overlay NOT installed")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
secureLogger.debug("installOverlay: scene found (state=\(String(describing: scene.activationState.rawValue))), creating overlay window")
|
|
104
|
+
|
|
105
|
+
let window = UIWindow(windowScene: scene)
|
|
106
|
+
window.windowLevel = .alert + 1
|
|
107
|
+
window.backgroundColor = .black
|
|
108
|
+
window.rootViewController = UIViewController()
|
|
109
|
+
window.rootViewController?.view.backgroundColor = .black
|
|
110
|
+
window.isHidden = false
|
|
111
|
+
|
|
112
|
+
self.overlayWindow = window
|
|
113
|
+
secureLogger.debug("installOverlay: overlay window installed successfully")
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func removeOverlay() {
|
|
118
|
+
DispatchQueue.main.async { [weak self] in
|
|
119
|
+
guard let self else { return }
|
|
120
|
+
if self.overlayWindow != nil {
|
|
121
|
+
secureLogger.debug("removeOverlay: hiding and releasing overlay window")
|
|
122
|
+
}
|
|
123
|
+
self.overlayWindow?.isHidden = true
|
|
124
|
+
self.overlayWindow = nil
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deinit {
|
|
129
|
+
NotificationCenter.default.removeObserver(self)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "URLWhitelistManager")
|
|
5
|
+
|
|
6
|
+
/// Data structure to hold categorized URL patterns for efficient matching
|
|
7
|
+
private struct CategorizedPatterns {
|
|
8
|
+
let contains: [String]
|
|
9
|
+
let prefix: [String]
|
|
10
|
+
let suffix: [String]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Manager for URL whitelisting and access control
|
|
14
|
+
/// Provides centralized URL validation with pattern matching support
|
|
15
|
+
///
|
|
16
|
+
/// Pattern matching rules:
|
|
17
|
+
/// - *text* or text (no wildcard) -> checks if URL contains "text"
|
|
18
|
+
/// - text* -> checks if URL starts with "text"
|
|
19
|
+
/// - *text -> checks if URL ends with "text"
|
|
20
|
+
///
|
|
21
|
+
/// URL processing:
|
|
22
|
+
/// - Query parameters and hash fragments are stripped before matching
|
|
23
|
+
/// - Matching is case-insensitive
|
|
24
|
+
/// - Ports are kept as-is (user must include port in pattern if URL has explicit port)
|
|
25
|
+
class URLWhitelistManager {
|
|
26
|
+
static let shared = URLWhitelistManager()
|
|
27
|
+
|
|
28
|
+
private var accessControlEnabled: Bool = false
|
|
29
|
+
private var patterns: CategorizedPatterns?
|
|
30
|
+
private let syncQueue = DispatchQueue(label: "com.app.urlwhitelist", attributes: .concurrent)
|
|
31
|
+
|
|
32
|
+
private init() {
|
|
33
|
+
loadConfiguration()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#if DEBUG
|
|
37
|
+
/// Test-only initialization method to override configuration
|
|
38
|
+
/// This allows unit tests to set custom whitelist configurations
|
|
39
|
+
/// - Parameters:
|
|
40
|
+
/// - enabled: Whether access control is enabled
|
|
41
|
+
/// - allowedUrls: Array of URL patterns to allow
|
|
42
|
+
func testInitialize(enabled: Bool, allowedUrls: [String]) {
|
|
43
|
+
syncQueue.sync(flags: .barrier) {
|
|
44
|
+
self.accessControlEnabled = enabled
|
|
45
|
+
self.patterns = categorizePatterns(allowedUrls)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
#endif
|
|
49
|
+
|
|
50
|
+
/// Load URL whitelist configuration from ConfigConstants
|
|
51
|
+
private func loadConfiguration() {
|
|
52
|
+
syncQueue.sync(flags: .barrier) {
|
|
53
|
+
self.accessControlEnabled = ConfigConstants.accessControlEnabled
|
|
54
|
+
let allowedUrls = ConfigConstants.allowedUrls
|
|
55
|
+
self.patterns = categorizePatterns(allowedUrls)
|
|
56
|
+
|
|
57
|
+
logger.info("URLWhitelistManager initialized")
|
|
58
|
+
logger.info("Access Control Enabled: \(self.accessControlEnabled)")
|
|
59
|
+
logger.info("Allowed URLs: \(allowedUrls)")
|
|
60
|
+
if let patterns = self.patterns {
|
|
61
|
+
logger.info("Categorized - Contains: \(patterns.contains)")
|
|
62
|
+
logger.info("Categorized - Prefix: \(patterns.prefix)")
|
|
63
|
+
logger.info("Categorized - Suffix: \(patterns.suffix)")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Check if URL whitelisting is enabled
|
|
69
|
+
var isAccessControlEnabled: Bool {
|
|
70
|
+
return syncQueue.sync { self.accessControlEnabled }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Check if a URL is allowed according to the whitelist patterns
|
|
74
|
+
/// If access control is disabled, all URLs are allowed
|
|
75
|
+
/// - Parameter url: The URL to check
|
|
76
|
+
/// - Returns: true if URL is allowed, false otherwise
|
|
77
|
+
func isUrlAllowed(_ url: URL) -> Bool {
|
|
78
|
+
return isUrlAllowed(url.absoluteString)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Check if a URL string is allowed according to the whitelist patterns
|
|
82
|
+
/// If access control is disabled, all URLs are allowed
|
|
83
|
+
/// - Parameter urlString: The URL string to check
|
|
84
|
+
/// - Returns: true if URL is allowed, false otherwise
|
|
85
|
+
func isUrlAllowed(_ urlString: String) -> Bool {
|
|
86
|
+
return syncQueue.sync {
|
|
87
|
+
// If access control is disabled, allow everything
|
|
88
|
+
if !self.accessControlEnabled {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Always allow framework server URLs (localhost with /framework- pattern)
|
|
93
|
+
// These are internal infrastructure URLs used for large file handling
|
|
94
|
+
let frameworkPattern = "^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?/framework-[a-zA-Z0-9_-]+"
|
|
95
|
+
if let regex = try? NSRegularExpression(pattern: frameworkPattern, options: .caseInsensitive) {
|
|
96
|
+
let range = NSRange(location: 0, length: urlString.utf16.count)
|
|
97
|
+
if regex.firstMatch(in: urlString, options: [], range: range) != nil {
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
guard let patterns = self.patterns else {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If all pattern lists are empty, block everything
|
|
107
|
+
if patterns.contains.isEmpty && patterns.prefix.isEmpty && patterns.suffix.isEmpty {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let cleanUrl = cleanUrl(urlString)
|
|
112
|
+
|
|
113
|
+
// Check contains patterns
|
|
114
|
+
if patterns.contains.contains(where: { cleanUrl.contains($0) }) {
|
|
115
|
+
logger.info("✅ URL allowed (contains match): \(urlString)")
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check prefix patterns
|
|
120
|
+
if patterns.prefix.contains(where: { cleanUrl.hasPrefix($0) }) {
|
|
121
|
+
logger.info("✅ URL allowed (prefix match): \(urlString)")
|
|
122
|
+
return true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check suffix patterns
|
|
126
|
+
if patterns.suffix.contains(where: { cleanUrl.hasSuffix($0) }) {
|
|
127
|
+
logger.info("✅ URL allowed (suffix match): \(urlString)")
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.warning("🚫 URL blocked by access control: \(urlString)")
|
|
132
|
+
logger.warning("🚫 Clean URL: \(cleanUrl)")
|
|
133
|
+
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Check if a URL is from an external domain (not in whitelist)
|
|
139
|
+
/// If access control is disabled, nothing is considered external
|
|
140
|
+
/// - Parameter url: The URL to check
|
|
141
|
+
/// - Returns: true if URL is external, false if it's whitelisted
|
|
142
|
+
func isExternalDomain(_ url: URL) -> Bool {
|
|
143
|
+
return isExternalDomain(url.absoluteString)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Check if a URL string is from an external domain (not in whitelist)
|
|
147
|
+
/// If access control is disabled, nothing is considered external
|
|
148
|
+
/// - Parameter urlString: The URL string to check
|
|
149
|
+
/// - Returns: true if URL is external, false if it's whitelisted
|
|
150
|
+
func isExternalDomain(_ urlString: String) -> Bool {
|
|
151
|
+
// If access control is disabled, nothing is external
|
|
152
|
+
if !self.accessControlEnabled {
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Simply the inverse of isUrlAllowed
|
|
157
|
+
return !isUrlAllowed(urlString)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Clean URL by removing query parameters and hash, then lowercase for case-insensitive matching
|
|
161
|
+
/// Also decodes URL-encoded characters to prevent bypass attempts
|
|
162
|
+
/// - Parameter url: The URL string to clean
|
|
163
|
+
/// - Returns: Cleaned and lowercased URL string
|
|
164
|
+
private func cleanUrl(_ url: String) -> String {
|
|
165
|
+
// Decode URL-encoded characters first to prevent bypass (e.g., %3F for ?, %23 for #)
|
|
166
|
+
let decoded = url.removingPercentEncoding ?? url
|
|
167
|
+
|
|
168
|
+
var cleanUrl = decoded
|
|
169
|
+
|
|
170
|
+
// Remove hash fragment (# and everything after)
|
|
171
|
+
if let hashIndex = cleanUrl.firstIndex(of: "#") {
|
|
172
|
+
cleanUrl = String(cleanUrl[..<hashIndex])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove query parameters (? and everything after)
|
|
176
|
+
if let queryIndex = cleanUrl.firstIndex(of: "?") {
|
|
177
|
+
cleanUrl = String(cleanUrl[..<queryIndex])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return cleanUrl.lowercased()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Categorize patterns into contains, prefix, and suffix for efficient matching
|
|
184
|
+
/// - Parameter allowedUrls: Array of URL pattern strings
|
|
185
|
+
/// - Returns: Categorized patterns structure
|
|
186
|
+
private func categorizePatterns(_ allowedUrls: [String]) -> CategorizedPatterns {
|
|
187
|
+
var contains: [String] = []
|
|
188
|
+
var prefix: [String] = []
|
|
189
|
+
var suffix: [String] = []
|
|
190
|
+
|
|
191
|
+
for pattern in allowedUrls {
|
|
192
|
+
let startsWithWildcard = pattern.hasPrefix("*")
|
|
193
|
+
let endsWithWildcard = pattern.hasSuffix("*")
|
|
194
|
+
|
|
195
|
+
if startsWithWildcard && endsWithWildcard {
|
|
196
|
+
// *text* -> contains check
|
|
197
|
+
let extracted = String(pattern.dropFirst().dropLast()).lowercased()
|
|
198
|
+
if !extracted.isEmpty {
|
|
199
|
+
contains.append(extracted)
|
|
200
|
+
}
|
|
201
|
+
} else if endsWithWildcard {
|
|
202
|
+
// text* -> prefix check
|
|
203
|
+
let extracted = String(pattern.dropLast()).lowercased()
|
|
204
|
+
if !extracted.isEmpty {
|
|
205
|
+
prefix.append(extracted)
|
|
206
|
+
}
|
|
207
|
+
} else if startsWithWildcard {
|
|
208
|
+
// *text -> suffix check
|
|
209
|
+
let extracted = String(pattern.dropFirst()).lowercased()
|
|
210
|
+
if !extracted.isEmpty {
|
|
211
|
+
suffix.append(extracted)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// No wildcard -> treat as contains check
|
|
215
|
+
let extracted = pattern.lowercased()
|
|
216
|
+
if !extracted.isEmpty {
|
|
217
|
+
contains.append(extracted)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return CategorizedPatterns(contains: contains, prefix: prefix, suffix: suffix)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// WebKitConfig.swift
|
|
3
|
+
// CatalystCore
|
|
4
|
+
//
|
|
5
|
+
// Lightweight shim that keeps legacy APIs compiling without triggering
|
|
6
|
+
// WKProcessPool deprecation warnings on iOS 15+.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import WebKit
|
|
10
|
+
|
|
11
|
+
public enum WebKitConfig {
|
|
12
|
+
@available(iOS, introduced: 11.0, deprecated: 15.0, message: "Custom WKProcessPool instances have no effect on iOS 15+.")
|
|
13
|
+
public static let sharedProcessPool = WKProcessPool()
|
|
14
|
+
|
|
15
|
+
private static var legacyPrewarmedWebView: WKWebView?
|
|
16
|
+
|
|
17
|
+
/// Recreate the legacy shared process pool behavior for iOS versions where it still matters.
|
|
18
|
+
@discardableResult
|
|
19
|
+
public static func prewarmProcessPoolIfNeeded() -> WKWebView? {
|
|
20
|
+
if #available(iOS 15, *) {
|
|
21
|
+
logWithTimestamp("🔥 WebKit process pool prewarm skipped (ignored by iOS 15+)")
|
|
22
|
+
legacyPrewarmedWebView = nil
|
|
23
|
+
return nil
|
|
24
|
+
} else {
|
|
25
|
+
if legacyPrewarmedWebView == nil {
|
|
26
|
+
let configuration = WKWebViewConfiguration()
|
|
27
|
+
configuration.processPool = sharedProcessPool
|
|
28
|
+
legacyPrewarmedWebView = WKWebView(frame: .zero, configuration: configuration)
|
|
29
|
+
logWithTimestamp("🔥 WebKit process pool prewarmed for legacy iOS versions")
|
|
30
|
+
}
|
|
31
|
+
return legacyPrewarmedWebView
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Apply the shared process pool only on OS versions where Apple still honors it.
|
|
36
|
+
public static func applySharedProcessPoolIfNeeded(to configuration: WKWebViewConfiguration) {
|
|
37
|
+
if #available(iOS 15, *) {
|
|
38
|
+
return
|
|
39
|
+
} else {
|
|
40
|
+
configuration.processPool = sharedProcessPool
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|