@thelacanians/vue-native-cli 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +34 -17
- package/native/android/README.md +205 -0
- package/native/android/VueNativeCore/build.gradle.kts +100 -0
- package/native/android/VueNativeCore/consumer-rules.pro +12 -0
- package/native/android/VueNativeCore/proguard-rules.pro +33 -0
- package/native/android/VueNativeCore/src/main/AndroidManifest.xml +17 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/ErrorOverlayView.kt +94 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/HotReloadManager.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSPolyfills.kt +652 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSRuntime.kt +207 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +417 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/ComponentRegistry.kt +76 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActionSheetFactory.kt +78 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActivityIndicatorFactory.kt +46 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VAlertDialogFactory.kt +84 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VButtonFactory.kt +73 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VCheckboxFactory.kt +93 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VDropdownFactory.kt +125 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VImageFactory.kt +75 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VInputFactory.kt +210 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VKeyboardAvoidingFactory.kt +31 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +183 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPickerFactory.kt +57 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPressableFactory.kt +109 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VProgressBarFactory.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRadioFactory.kt +103 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRefreshControlFactory.kt +73 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRootFactory.kt +39 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSafeAreaFactory.kt +48 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSectionListFactory.kt +144 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSegmentedControlFactory.kt +77 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +74 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VStatusBarFactory.kt +52 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSwitchFactory.kt +62 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VTextFactory.kt +53 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VVideoFactory.kt +191 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +48 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VWebViewFactory.kt +90 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/NativeComponentFactory.kt +40 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/VTextNodeView.kt +23 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/GestureHelper.kt +16 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/TouchableView.kt +105 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AnimationModule.kt +292 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AppStateModule.kt +41 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AsyncStorageModule.kt +59 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AudioModule.kt +331 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BackgroundTaskModule.kt +166 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BiometryModule.kt +56 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +302 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CalendarModule.kt +198 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CameraModule.kt +64 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ClipboardModule.kt +36 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ContactsModule.kt +288 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DatabaseModule.kt +229 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DeviceInfoModule.kt +39 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/FileSystemModule.kt +193 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeolocationModule.kt +68 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HapticsModule.kt +61 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HttpModule.kt +111 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +302 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/KeyboardModule.kt +26 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/LinkingModule.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModule.kt +27 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +92 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NetworkModule.kt +75 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NotificationsModule.kt +181 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/OTAModule.kt +255 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PerformanceModule.kt +147 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PermissionsModule.kt +126 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SecureStorageModule.kt +51 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SensorsModule.kt +134 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ShareModule.kt +36 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +160 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/WebSocketModule.kt +155 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +802 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Tags.kt +43 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/VueNativeActivity.kt +169 -0
- package/native/android/VueNativeCore/src/main/res/values/ids.xml +8 -0
- package/native/android/app/build.gradle.kts +45 -0
- package/native/android/app/proguard-rules.pro +5 -0
- package/native/android/app/src/main/AndroidManifest.xml +25 -0
- package/native/android/app/src/main/assets/.gitkeep +0 -0
- package/native/android/app/src/main/kotlin/com/vuenative/example/counter/MainActivity.kt +14 -0
- package/native/android/app/src/main/res/layout/activity_main.xml +6 -0
- package/native/android/app/src/main/res/values/strings.xml +3 -0
- package/native/android/app/src/main/res/values/themes.xml +9 -0
- package/native/android/app/src/main/res/xml/network_security_config.xml +8 -0
- package/native/android/build.gradle.kts +6 -0
- package/native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/native/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/native/android/gradle.properties +4 -0
- package/native/android/gradlew +87 -0
- package/native/android/gradlew.bat +48 -0
- package/native/android/settings.gradle.kts +20 -0
- package/native/ios/VueNativeCore/Package.resolved +23 -0
- package/native/ios/VueNativeCore/Package.swift +32 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/CertificatePinning.swift +132 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/ErrorOverlayView.swift +92 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/HotReloadManager.swift +147 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSPolyfills.swift +711 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSRuntime.swift +421 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +891 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/VueNativeViewController.swift +88 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/ComponentRegistry.swift +193 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActionSheetFactory.swift +91 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VActivityIndicatorFactory.swift +74 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VAlertDialogFactory.swift +150 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VButtonFactory.swift +93 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VCheckboxFactory.swift +114 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VDropdownFactory.swift +112 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VImageFactory.swift +172 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VInputFactory.swift +357 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VKeyboardAvoidingFactory.swift +99 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +250 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VModalFactory.swift +112 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPickerFactory.swift +96 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VPressableFactory.swift +168 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VProgressBarFactory.swift +39 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRadioFactory.swift +167 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VRefreshControlFactory.swift +153 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSafeAreaFactory.swift +56 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +240 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSectionListFactory.swift +248 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSegmentedControlFactory.swift +73 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +63 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VStatusBarFactory.swift +50 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSwitchFactory.swift +108 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +290 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VVideoFactory.swift +246 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +157 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VWebViewFactory.swift +172 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/NativeComponentFactory.swift +53 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +107 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/TouchableView.swift +136 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/UIColor+Hex.swift +80 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AnimationModule.swift +291 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AppStateModule.swift +65 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AsyncStorageModule.swift +68 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/AudioModule.swift +366 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BackgroundTaskModule.swift +135 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BiometryModule.swift +61 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/BluetoothModule.swift +387 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CalendarModule.swift +161 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/CameraModule.swift +318 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ClipboardModule.swift +33 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ContactsModule.swift +173 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DatabaseModule.swift +259 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/DeviceInfoModule.swift +34 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/FileSystemModule.swift +233 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeolocationModule.swift +147 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/HapticsModule.swift +50 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/IAPModule.swift +194 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/KeyboardModule.swift +31 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/LinkingModule.swift +42 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModule.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +78 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NetworkModule.swift +62 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NotificationsModule.swift +215 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/OTAModule.swift +281 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PerformanceModule.swift +138 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/PermissionsModule.swift +190 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SecureStorageModule.swift +118 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SensorsModule.swift +103 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/ShareModule.swift +49 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/SocialAuthModule.swift +240 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/WebSocketModule.swift +213 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Resources/vue-native-placeholder.js +8 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Styling/StyleEngine.swift +885 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSRuntimeTests.swift +362 -0
- package/package.json +3 -2
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import UserNotifications
|
|
4
|
+
|
|
5
|
+
/// Native module for local and remote (push) notifications.
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - requestPermission() -> Bool
|
|
9
|
+
/// - getPermissionStatus() -> "granted"|"denied"|"notDetermined"
|
|
10
|
+
/// - scheduleLocal(notification: Object) -> notificationId: String
|
|
11
|
+
/// - cancel(id: String)
|
|
12
|
+
/// - cancelAll()
|
|
13
|
+
/// - registerForPush() -> Void (registers for APNS remote notifications)
|
|
14
|
+
/// - getToken() -> String? (returns cached APNS device token)
|
|
15
|
+
///
|
|
16
|
+
/// Global events dispatched on bridge:
|
|
17
|
+
/// "notification:received" -- when a notification arrives in foreground or is tapped
|
|
18
|
+
/// "push:token" -- when APNS device token is received { token }
|
|
19
|
+
/// "push:received" -- when a remote push notification arrives { title, body, data }
|
|
20
|
+
final class NotificationsModule: NativeModule {
|
|
21
|
+
var moduleName: String { "Notifications" }
|
|
22
|
+
private weak var bridge: NativeBridge?
|
|
23
|
+
|
|
24
|
+
/// Serial queue for thread-safe access to the device token.
|
|
25
|
+
private let tokenQueue = DispatchQueue(label: "com.vuenative.notifications.token")
|
|
26
|
+
/// Backing storage for deviceToken. Access only via the computed property.
|
|
27
|
+
private var _deviceToken: String?
|
|
28
|
+
/// Cached APNS device token (hex string). Thread-safe.
|
|
29
|
+
private var deviceToken: String? {
|
|
30
|
+
get { tokenQueue.sync { _deviceToken } }
|
|
31
|
+
set { tokenQueue.sync { _deviceToken = newValue } }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
init(bridge: NativeBridge) {
|
|
35
|
+
self.bridge = bridge
|
|
36
|
+
setupForegroundHandler()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func setupForegroundHandler() {
|
|
40
|
+
UNUserNotificationCenter.current().delegate = NotificationCenterDelegate.shared
|
|
41
|
+
// Capture bridge weakly; dispatch to main actor because dispatchGlobalEvent is @MainActor.
|
|
42
|
+
let weakBridge = bridge
|
|
43
|
+
NotificationCenterDelegate.shared.onNotification = { payload in
|
|
44
|
+
DispatchQueue.main.async {
|
|
45
|
+
weakBridge?.dispatchGlobalEvent("notification:received", payload: payload)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
NotificationCenterDelegate.shared.onPushReceived = { payload in
|
|
49
|
+
DispatchQueue.main.async {
|
|
50
|
+
weakBridge?.dispatchGlobalEvent("push:received", payload: payload)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
56
|
+
switch method {
|
|
57
|
+
case "requestPermission":
|
|
58
|
+
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
|
|
59
|
+
callback(granted, error?.localizedDescription)
|
|
60
|
+
}
|
|
61
|
+
case "getPermissionStatus":
|
|
62
|
+
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
|
63
|
+
callback(self.statusString(settings.authorizationStatus), nil)
|
|
64
|
+
}
|
|
65
|
+
case "scheduleLocal":
|
|
66
|
+
guard let notification = args.first as? [String: Any] else {
|
|
67
|
+
callback(nil, "NotificationsModule: invalid notification object"); return
|
|
68
|
+
}
|
|
69
|
+
scheduleLocal(notification, callback: callback)
|
|
70
|
+
case "cancel":
|
|
71
|
+
if let id = args.first as? String {
|
|
72
|
+
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
|
|
73
|
+
}
|
|
74
|
+
callback(nil, nil)
|
|
75
|
+
case "cancelAll":
|
|
76
|
+
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
|
77
|
+
callback(nil, nil)
|
|
78
|
+
case "registerForPush":
|
|
79
|
+
DispatchQueue.main.async {
|
|
80
|
+
UIApplication.shared.registerForRemoteNotifications()
|
|
81
|
+
}
|
|
82
|
+
callback(true, nil)
|
|
83
|
+
case "getToken":
|
|
84
|
+
callback(deviceToken, nil)
|
|
85
|
+
default:
|
|
86
|
+
callback(nil, "NotificationsModule: Unknown method '\(method)'")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Push token handling (called from AppDelegate)
|
|
91
|
+
|
|
92
|
+
/// Call this from your AppDelegate's `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`
|
|
93
|
+
func didRegisterForRemoteNotifications(deviceToken data: Data) {
|
|
94
|
+
let token = data.map { String(format: "%02x", $0) }.joined()
|
|
95
|
+
self.deviceToken = token
|
|
96
|
+
DispatchQueue.main.async { [weak self] in
|
|
97
|
+
self?.bridge?.dispatchGlobalEvent("push:token", payload: ["token": token])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Call this from your AppDelegate's `application(_:didFailToRegisterForRemoteNotificationsWithError:)`
|
|
102
|
+
func didFailToRegisterForRemoteNotifications(error: Error) {
|
|
103
|
+
DispatchQueue.main.async { [weak self] in
|
|
104
|
+
self?.bridge?.dispatchGlobalEvent("push:error", payload: [
|
|
105
|
+
"message": error.localizedDescription
|
|
106
|
+
])
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Schedule local notification
|
|
111
|
+
|
|
112
|
+
private func scheduleLocal(_ notification: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
113
|
+
let content = UNMutableNotificationContent()
|
|
114
|
+
content.title = notification["title"] as? String ?? ""
|
|
115
|
+
content.body = notification["body"] as? String ?? ""
|
|
116
|
+
|
|
117
|
+
if let sound = notification["sound"] as? String, sound == "default" {
|
|
118
|
+
content.sound = .default
|
|
119
|
+
}
|
|
120
|
+
if let badge = notification["badge"] as? Int {
|
|
121
|
+
content.badge = NSNumber(value: badge)
|
|
122
|
+
}
|
|
123
|
+
if let data = notification["data"] as? [String: Any] {
|
|
124
|
+
content.userInfo = data
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let delay = max((notification["delay"] as? Double) ?? 0.1, 0.1)
|
|
128
|
+
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)
|
|
129
|
+
let id = (notification["id"] as? String) ?? UUID().uuidString
|
|
130
|
+
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
|
|
131
|
+
|
|
132
|
+
UNUserNotificationCenter.current().add(request) { error in
|
|
133
|
+
if let error = error {
|
|
134
|
+
callback(nil, error.localizedDescription)
|
|
135
|
+
} else {
|
|
136
|
+
callback(id, nil)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MARK: - Status helper
|
|
142
|
+
|
|
143
|
+
private func statusString(_ status: UNAuthorizationStatus) -> String {
|
|
144
|
+
switch status {
|
|
145
|
+
case .authorized, .provisional, .ephemeral: return "granted"
|
|
146
|
+
case .denied: return "denied"
|
|
147
|
+
case .notDetermined: return "notDetermined"
|
|
148
|
+
@unknown default: return "notDetermined"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func invokeSync(method: String, args: [Any]) -> Any? { nil }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - UNUserNotificationCenterDelegate
|
|
156
|
+
|
|
157
|
+
/// Singleton delegate that forwards foreground and tapped notifications
|
|
158
|
+
/// to whoever has set `onNotification`.
|
|
159
|
+
private final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
160
|
+
static let shared = NotificationCenterDelegate()
|
|
161
|
+
/// Called for local notifications and tapped notifications.
|
|
162
|
+
var onNotification: (([String: Any]) -> Void)?
|
|
163
|
+
/// Called specifically for remote push notifications arriving in foreground.
|
|
164
|
+
var onPushReceived: (([String: Any]) -> Void)?
|
|
165
|
+
|
|
166
|
+
/// Show banners/sounds/badges even when the app is in foreground.
|
|
167
|
+
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
168
|
+
willPresent notification: UNNotification,
|
|
169
|
+
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
170
|
+
let request = notification.request
|
|
171
|
+
if request.trigger is UNPushNotificationTrigger {
|
|
172
|
+
onPushReceived?(makePushPayload(from: request))
|
|
173
|
+
} else {
|
|
174
|
+
onNotification?(makePayload(from: request))
|
|
175
|
+
}
|
|
176
|
+
completionHandler([.banner, .sound, .badge])
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Called when the user taps a notification.
|
|
180
|
+
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
|
181
|
+
didReceive response: UNNotificationResponse,
|
|
182
|
+
withCompletionHandler completionHandler: @escaping () -> Void) {
|
|
183
|
+
let request = response.notification.request
|
|
184
|
+
if request.trigger is UNPushNotificationTrigger {
|
|
185
|
+
var payload = makePushPayload(from: request)
|
|
186
|
+
payload["action"] = response.actionIdentifier
|
|
187
|
+
onPushReceived?(payload)
|
|
188
|
+
} else {
|
|
189
|
+
var payload = makePayload(from: request)
|
|
190
|
+
payload["action"] = response.actionIdentifier
|
|
191
|
+
onNotification?(payload)
|
|
192
|
+
}
|
|
193
|
+
completionHandler()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private func makePayload(from request: UNNotificationRequest) -> [String: Any] {
|
|
197
|
+
[
|
|
198
|
+
"id": request.identifier,
|
|
199
|
+
"title": request.content.title,
|
|
200
|
+
"body": request.content.body,
|
|
201
|
+
"data": request.content.userInfo
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func makePushPayload(from request: UNNotificationRequest) -> [String: Any] {
|
|
206
|
+
[
|
|
207
|
+
"id": request.identifier,
|
|
208
|
+
"title": request.content.title,
|
|
209
|
+
"body": request.content.body,
|
|
210
|
+
"data": request.content.userInfo,
|
|
211
|
+
"remote": true
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
#endif
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import CommonCrypto
|
|
4
|
+
|
|
5
|
+
/// Native module for Over-The-Air (OTA) JS bundle updates.
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - checkForUpdate(serverUrl) — check for available updates
|
|
9
|
+
/// - downloadUpdate(url, hash) — download a new bundle and verify integrity
|
|
10
|
+
/// - applyUpdate() — swap to downloaded bundle on next launch
|
|
11
|
+
/// - rollback() — revert to the embedded bundle
|
|
12
|
+
/// - getCurrentVersion() — get current bundle version info
|
|
13
|
+
///
|
|
14
|
+
/// Events:
|
|
15
|
+
/// - ota:downloadProgress — payload: { progress: 0.0-1.0, bytesDownloaded, totalBytes }
|
|
16
|
+
final class OTAModule: NativeModule {
|
|
17
|
+
var moduleName: String { "OTA" }
|
|
18
|
+
|
|
19
|
+
private let defaults = UserDefaults.standard
|
|
20
|
+
private let keyPrefix = "VueNative.OTA."
|
|
21
|
+
|
|
22
|
+
// UserDefaults keys
|
|
23
|
+
private var currentVersionKey: String { keyPrefix + "currentVersion" }
|
|
24
|
+
private var bundlePathKey: String { keyPrefix + "bundlePath" }
|
|
25
|
+
private var previousBundlePathKey: String { keyPrefix + "previousBundlePath" }
|
|
26
|
+
private var previousVersionKey: String { keyPrefix + "previousVersion" }
|
|
27
|
+
|
|
28
|
+
private weak var bridge: NativeBridge?
|
|
29
|
+
|
|
30
|
+
init(bridge: NativeBridge) {
|
|
31
|
+
self.bridge = bridge
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
35
|
+
switch method {
|
|
36
|
+
case "checkForUpdate":
|
|
37
|
+
guard let serverUrl = args.first as? String else {
|
|
38
|
+
callback(nil, "checkForUpdate: missing serverUrl")
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
checkForUpdate(serverUrl: serverUrl, callback: callback)
|
|
42
|
+
|
|
43
|
+
case "downloadUpdate":
|
|
44
|
+
guard args.count >= 1,
|
|
45
|
+
let url = args[0] as? String else {
|
|
46
|
+
callback(nil, "downloadUpdate: missing url")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
let expectedHash = args.count >= 2 ? args[1] as? String : nil
|
|
50
|
+
downloadUpdate(url: url, expectedHash: expectedHash, callback: callback)
|
|
51
|
+
|
|
52
|
+
case "applyUpdate":
|
|
53
|
+
applyUpdate(callback: callback)
|
|
54
|
+
|
|
55
|
+
case "rollback":
|
|
56
|
+
rollback(callback: callback)
|
|
57
|
+
|
|
58
|
+
case "getCurrentVersion":
|
|
59
|
+
getCurrentVersion(callback: callback)
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
callback(nil, "OTAModule: Unknown method '\(method)'")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Check for update
|
|
67
|
+
|
|
68
|
+
private func checkForUpdate(serverUrl: String, callback: @escaping (Any?, String?) -> Void) {
|
|
69
|
+
guard let url = URL(string: serverUrl) else {
|
|
70
|
+
callback(nil, "Invalid server URL")
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let currentVersion = defaults.string(forKey: currentVersionKey) ?? "0"
|
|
75
|
+
|
|
76
|
+
var request = URLRequest(url: url)
|
|
77
|
+
request.httpMethod = "GET"
|
|
78
|
+
request.setValue(currentVersion, forHTTPHeaderField: "X-Current-Version")
|
|
79
|
+
request.setValue("ios", forHTTPHeaderField: "X-Platform")
|
|
80
|
+
request.setValue(Bundle.main.bundleIdentifier ?? "unknown", forHTTPHeaderField: "X-App-Id")
|
|
81
|
+
|
|
82
|
+
URLSession.shared.dataTask(with: request) { data, response, error in
|
|
83
|
+
if let error = error {
|
|
84
|
+
callback(nil, "Network error: \(error.localizedDescription)")
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
guard let data = data,
|
|
89
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
90
|
+
callback(nil, "Invalid response from update server")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let updateAvailable = json["updateAvailable"] as? Bool ?? false
|
|
95
|
+
let result: [String: Any] = [
|
|
96
|
+
"updateAvailable": updateAvailable,
|
|
97
|
+
"version": json["version"] as? String ?? "",
|
|
98
|
+
"downloadUrl": json["downloadUrl"] as? String ?? "",
|
|
99
|
+
"hash": json["hash"] as? String ?? "",
|
|
100
|
+
"size": json["size"] as? Int ?? 0,
|
|
101
|
+
"releaseNotes": json["releaseNotes"] as? String ?? "",
|
|
102
|
+
]
|
|
103
|
+
callback(result, nil)
|
|
104
|
+
}.resume()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// MARK: - Download update
|
|
108
|
+
|
|
109
|
+
private func downloadUpdate(url: String, expectedHash: String?, callback: @escaping (Any?, String?) -> Void) {
|
|
110
|
+
guard let downloadUrl = URL(string: url) else {
|
|
111
|
+
callback(nil, "Invalid download URL")
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let session = URLSession(configuration: .default, delegate: DownloadDelegate(bridge: bridge), delegateQueue: nil)
|
|
116
|
+
let task = session.downloadTask(with: downloadUrl) { [weak self] tempUrl, response, error in
|
|
117
|
+
guard let self = self else { return }
|
|
118
|
+
|
|
119
|
+
if let error = error {
|
|
120
|
+
callback(nil, "Download failed: \(error.localizedDescription)")
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
guard let tempUrl = tempUrl else {
|
|
125
|
+
callback(nil, "Download failed: no file received")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
do {
|
|
130
|
+
// Read downloaded data
|
|
131
|
+
let data = try Data(contentsOf: tempUrl)
|
|
132
|
+
|
|
133
|
+
// Verify hash if provided
|
|
134
|
+
if let expectedHash = expectedHash, !expectedHash.isEmpty {
|
|
135
|
+
let actualHash = self.sha256(data: data)
|
|
136
|
+
if actualHash.lowercased() != expectedHash.lowercased() {
|
|
137
|
+
callback(nil, "Bundle integrity check failed. Expected: \(expectedHash), got: \(actualHash)")
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Save to Documents directory
|
|
143
|
+
let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
144
|
+
let bundleDir = docsDir.appendingPathComponent("VueNativeOTA", isDirectory: true)
|
|
145
|
+
try FileManager.default.createDirectory(at: bundleDir, withIntermediateDirectories: true)
|
|
146
|
+
|
|
147
|
+
let bundlePath = bundleDir.appendingPathComponent("bundle.js")
|
|
148
|
+
|
|
149
|
+
// Remove old pending download if exists
|
|
150
|
+
if FileManager.default.fileExists(atPath: bundlePath.path) {
|
|
151
|
+
try FileManager.default.removeItem(at: bundlePath)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try data.write(to: bundlePath)
|
|
155
|
+
|
|
156
|
+
// Store the path for applyUpdate
|
|
157
|
+
self.defaults.set(bundlePath.path, forKey: self.keyPrefix + "pendingBundlePath")
|
|
158
|
+
|
|
159
|
+
callback(["path": bundlePath.path, "size": data.count], nil)
|
|
160
|
+
} catch {
|
|
161
|
+
callback(nil, "Failed to save bundle: \(error.localizedDescription)")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
task.resume()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MARK: - Apply update
|
|
168
|
+
|
|
169
|
+
private func applyUpdate(callback: @escaping (Any?, String?) -> Void) {
|
|
170
|
+
guard let pendingPath = defaults.string(forKey: keyPrefix + "pendingBundlePath"),
|
|
171
|
+
FileManager.default.fileExists(atPath: pendingPath) else {
|
|
172
|
+
callback(nil, "No pending update to apply")
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Save current bundle path for rollback
|
|
177
|
+
if let currentPath = defaults.string(forKey: bundlePathKey) {
|
|
178
|
+
defaults.set(currentPath, forKey: previousBundlePathKey)
|
|
179
|
+
}
|
|
180
|
+
if let currentVersion = defaults.string(forKey: currentVersionKey) {
|
|
181
|
+
defaults.set(currentVersion, forKey: previousVersionKey)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Set the new bundle as current
|
|
185
|
+
defaults.set(pendingPath, forKey: bundlePathKey)
|
|
186
|
+
defaults.removeObject(forKey: keyPrefix + "pendingBundlePath")
|
|
187
|
+
|
|
188
|
+
// Increment version tracker
|
|
189
|
+
let currentVersion = defaults.integer(forKey: currentVersionKey)
|
|
190
|
+
defaults.set(currentVersion + 1, forKey: currentVersionKey)
|
|
191
|
+
|
|
192
|
+
callback(["applied": true], nil)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// MARK: - Rollback
|
|
196
|
+
|
|
197
|
+
private func rollback(callback: @escaping (Any?, String?) -> Void) {
|
|
198
|
+
guard let previousPath = defaults.string(forKey: previousBundlePathKey) else {
|
|
199
|
+
// Rollback to embedded bundle
|
|
200
|
+
defaults.removeObject(forKey: bundlePathKey)
|
|
201
|
+
if let prevVersion = defaults.string(forKey: previousVersionKey) {
|
|
202
|
+
defaults.set(prevVersion, forKey: currentVersionKey)
|
|
203
|
+
} else {
|
|
204
|
+
defaults.removeObject(forKey: currentVersionKey)
|
|
205
|
+
}
|
|
206
|
+
callback(["rolledBack": true, "toEmbedded": true], nil)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
defaults.set(previousPath, forKey: bundlePathKey)
|
|
211
|
+
if let prevVersion = defaults.string(forKey: previousVersionKey) {
|
|
212
|
+
defaults.set(prevVersion, forKey: currentVersionKey)
|
|
213
|
+
}
|
|
214
|
+
defaults.removeObject(forKey: previousBundlePathKey)
|
|
215
|
+
defaults.removeObject(forKey: previousVersionKey)
|
|
216
|
+
|
|
217
|
+
callback(["rolledBack": true, "toEmbedded": false], nil)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// MARK: - Get current version
|
|
221
|
+
|
|
222
|
+
private func getCurrentVersion(callback: @escaping (Any?, String?) -> Void) {
|
|
223
|
+
let version = defaults.string(forKey: currentVersionKey) ?? "embedded"
|
|
224
|
+
let bundlePath = defaults.string(forKey: bundlePathKey)
|
|
225
|
+
let isUsingOTA = bundlePath != nil && FileManager.default.fileExists(atPath: bundlePath!)
|
|
226
|
+
|
|
227
|
+
callback([
|
|
228
|
+
"version": version,
|
|
229
|
+
"isUsingOTA": isUsingOTA,
|
|
230
|
+
"bundlePath": bundlePath ?? "",
|
|
231
|
+
], nil)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// MARK: - SHA-256
|
|
235
|
+
|
|
236
|
+
private func sha256(data: Data) -> String {
|
|
237
|
+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
238
|
+
data.withUnsafeBytes { buffer in
|
|
239
|
+
_ = CC_SHA256(buffer.baseAddress, CC_LONG(data.count), &hash)
|
|
240
|
+
}
|
|
241
|
+
return hash.map { String(format: "%02x", $0) }.joined()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
func invokeSync(method: String, args: [Any]) -> Any? { nil }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// MARK: - Download delegate for progress reporting
|
|
248
|
+
|
|
249
|
+
private class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
250
|
+
private weak var bridge: NativeBridge?
|
|
251
|
+
|
|
252
|
+
init(bridge: NativeBridge?) {
|
|
253
|
+
self.bridge = bridge
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
257
|
+
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
|
|
258
|
+
totalBytesExpectedToWrite: Int64) {
|
|
259
|
+
let progress: Double
|
|
260
|
+
if totalBytesExpectedToWrite > 0 {
|
|
261
|
+
progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
|
|
262
|
+
} else {
|
|
263
|
+
progress = 0
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let bridge = bridge
|
|
267
|
+
DispatchQueue.main.async {
|
|
268
|
+
bridge?.dispatchGlobalEvent("ota:downloadProgress", payload: [
|
|
269
|
+
"progress": progress,
|
|
270
|
+
"bytesDownloaded": totalBytesWritten,
|
|
271
|
+
"totalBytes": totalBytesExpectedToWrite,
|
|
272
|
+
])
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
277
|
+
didFinishDownloadingTo location: URL) {
|
|
278
|
+
// Handled in the completion handler of the download task
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
#endif
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Native module for performance profiling.
|
|
5
|
+
/// Tracks FPS via CADisplayLink, memory usage via task_info, and bridge operation counts.
|
|
6
|
+
/// Dispatches `perf:metrics` global events every 1 second while profiling is active.
|
|
7
|
+
final class PerformanceModule: NativeModule {
|
|
8
|
+
|
|
9
|
+
let moduleName = "Performance"
|
|
10
|
+
|
|
11
|
+
private weak var bridge: NativeBridge?
|
|
12
|
+
private var displayLink: CADisplayLink?
|
|
13
|
+
private var isProfiling = false
|
|
14
|
+
|
|
15
|
+
// FPS tracking
|
|
16
|
+
private var frameCount = 0
|
|
17
|
+
private var lastTimestamp: CFTimeInterval = 0
|
|
18
|
+
private var currentFPS: Double = 0
|
|
19
|
+
|
|
20
|
+
// Metrics timer
|
|
21
|
+
private var metricsTimer: Timer?
|
|
22
|
+
|
|
23
|
+
// Bridge operation count
|
|
24
|
+
private var bridgeOpsCount: Int = 0
|
|
25
|
+
|
|
26
|
+
init(bridge: NativeBridge) {
|
|
27
|
+
self.bridge = bridge
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
31
|
+
switch method {
|
|
32
|
+
case "startProfiling":
|
|
33
|
+
startProfiling(callback: callback)
|
|
34
|
+
case "stopProfiling":
|
|
35
|
+
stopProfiling(callback: callback)
|
|
36
|
+
case "getMetrics":
|
|
37
|
+
let metrics = collectMetrics()
|
|
38
|
+
callback(metrics, nil)
|
|
39
|
+
default:
|
|
40
|
+
callback(nil, "PerformanceModule: unknown method '\(method)'")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Start / Stop
|
|
45
|
+
|
|
46
|
+
private func startProfiling(callback: @escaping (Any?, String?) -> Void) {
|
|
47
|
+
guard !isProfiling else { callback(true, nil); return }
|
|
48
|
+
isProfiling = true
|
|
49
|
+
frameCount = 0
|
|
50
|
+
lastTimestamp = 0
|
|
51
|
+
currentFPS = 0
|
|
52
|
+
bridgeOpsCount = 0
|
|
53
|
+
|
|
54
|
+
DispatchQueue.main.async { [weak self] in
|
|
55
|
+
guard let self = self else { return }
|
|
56
|
+
|
|
57
|
+
// CADisplayLink for FPS measurement
|
|
58
|
+
let link = CADisplayLink(target: self, selector: #selector(self.handleDisplayLink(_:)))
|
|
59
|
+
link.add(to: .main, forMode: .common)
|
|
60
|
+
self.displayLink = link
|
|
61
|
+
|
|
62
|
+
// Timer for periodic metrics dispatch (every 1 second)
|
|
63
|
+
self.metricsTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
64
|
+
self?.dispatchMetrics()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
callback(true, nil)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func stopProfiling(callback: @escaping (Any?, String?) -> Void) {
|
|
72
|
+
guard isProfiling else { callback(true, nil); return }
|
|
73
|
+
isProfiling = false
|
|
74
|
+
|
|
75
|
+
DispatchQueue.main.async { [weak self] in
|
|
76
|
+
self?.displayLink?.invalidate()
|
|
77
|
+
self?.displayLink = nil
|
|
78
|
+
self?.metricsTimer?.invalidate()
|
|
79
|
+
self?.metricsTimer = nil
|
|
80
|
+
callback(true, nil)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - CADisplayLink handler
|
|
85
|
+
|
|
86
|
+
@objc private func handleDisplayLink(_ link: CADisplayLink) {
|
|
87
|
+
if lastTimestamp == 0 {
|
|
88
|
+
lastTimestamp = link.timestamp
|
|
89
|
+
frameCount = 0
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
frameCount += 1
|
|
94
|
+
let elapsed = link.timestamp - lastTimestamp
|
|
95
|
+
|
|
96
|
+
// Calculate FPS every 0.5 seconds for smoother readings
|
|
97
|
+
if elapsed >= 0.5 {
|
|
98
|
+
currentFPS = Double(frameCount) / elapsed
|
|
99
|
+
frameCount = 0
|
|
100
|
+
lastTimestamp = link.timestamp
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: - Metrics collection
|
|
105
|
+
|
|
106
|
+
private func collectMetrics() -> [String: Any] {
|
|
107
|
+
return [
|
|
108
|
+
"fps": round(currentFPS * 10) / 10,
|
|
109
|
+
"memoryMB": getMemoryUsageMB(),
|
|
110
|
+
"bridgeOps": bridgeOpsCount,
|
|
111
|
+
"timestamp": Date().timeIntervalSince1970 * 1000,
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private func dispatchMetrics() {
|
|
116
|
+
guard isProfiling else { return }
|
|
117
|
+
bridgeOpsCount += 1 // Count the metrics dispatch itself
|
|
118
|
+
let metrics = collectMetrics()
|
|
119
|
+
bridge?.dispatchGlobalEvent("perf:metrics", payload: metrics)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Memory measurement
|
|
123
|
+
|
|
124
|
+
private func getMemoryUsageMB() -> Double {
|
|
125
|
+
var info = mach_task_basic_info()
|
|
126
|
+
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
|
127
|
+
let result = withUnsafeMutablePointer(to: &info) {
|
|
128
|
+
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
129
|
+
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if result == KERN_SUCCESS {
|
|
133
|
+
return Double(info.resident_size) / (1024 * 1024)
|
|
134
|
+
}
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
#endif
|