@thelacanians/vue-native-cli 0.4.2 → 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 +43 -23
- 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,68 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Native module providing async key-value storage backed by UserDefaults.
|
|
5
|
+
///
|
|
6
|
+
/// Methods:
|
|
7
|
+
/// - getItem(key: String) -> String?
|
|
8
|
+
/// - setItem(key: String, value: String)
|
|
9
|
+
/// - removeItem(key: String)
|
|
10
|
+
/// - getAllKeys() -> [String]
|
|
11
|
+
/// - clear()
|
|
12
|
+
final class AsyncStorageModule: NativeModule {
|
|
13
|
+
let moduleName = "AsyncStorage"
|
|
14
|
+
|
|
15
|
+
private let defaults = UserDefaults.standard
|
|
16
|
+
private let keyPrefix = "VueNative.AsyncStorage."
|
|
17
|
+
|
|
18
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
19
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
20
|
+
guard let self = self else { return }
|
|
21
|
+
switch method {
|
|
22
|
+
case "getItem":
|
|
23
|
+
guard let key = args.first as? String else {
|
|
24
|
+
callback(nil, "getItem: missing key")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
let result = self.defaults.string(forKey: self.keyPrefix + key)
|
|
28
|
+
callback(result, nil)
|
|
29
|
+
|
|
30
|
+
case "setItem":
|
|
31
|
+
guard args.count >= 2,
|
|
32
|
+
let key = args[0] as? String,
|
|
33
|
+
let val = args[1] as? String else {
|
|
34
|
+
callback(nil, "setItem: missing key or value")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
self.defaults.set(val, forKey: self.keyPrefix + key)
|
|
38
|
+
callback(nil, nil)
|
|
39
|
+
|
|
40
|
+
case "removeItem":
|
|
41
|
+
guard let key = args.first as? String else {
|
|
42
|
+
callback(nil, "removeItem: missing key")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
self.defaults.removeObject(forKey: self.keyPrefix + key)
|
|
46
|
+
callback(nil, nil)
|
|
47
|
+
|
|
48
|
+
case "getAllKeys":
|
|
49
|
+
let allKeys = self.defaults.dictionaryRepresentation().keys
|
|
50
|
+
.filter { $0.hasPrefix(self.keyPrefix) }
|
|
51
|
+
.map { String($0.dropFirst(self.keyPrefix.count)) }
|
|
52
|
+
callback(allKeys, nil)
|
|
53
|
+
|
|
54
|
+
case "clear":
|
|
55
|
+
let allKeys = self.defaults.dictionaryRepresentation().keys
|
|
56
|
+
.filter { $0.hasPrefix(self.keyPrefix) }
|
|
57
|
+
for key in allKeys {
|
|
58
|
+
self.defaults.removeObject(forKey: key)
|
|
59
|
+
}
|
|
60
|
+
callback(nil, nil)
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
callback(nil, "AsyncStorageModule: Unknown method '\(method)'")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
#endif
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
/// Native module for audio playback and recording.
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - play(uri: String, options?: Object) -- play audio from URI
|
|
9
|
+
/// - pause() -- pause playback
|
|
10
|
+
/// - resume() -- resume playback
|
|
11
|
+
/// - stop() -- stop playback and release player
|
|
12
|
+
/// - seek(position: Number) -- seek to position in seconds
|
|
13
|
+
/// - setVolume(volume: Number) -- set volume 0.0–1.0
|
|
14
|
+
/// - startRecording(options?: Object) -- start audio recording
|
|
15
|
+
/// - stopRecording() -- stop recording, returns { uri, duration }
|
|
16
|
+
/// - pauseRecording() -- pause recording
|
|
17
|
+
/// - resumeRecording() -- resume recording
|
|
18
|
+
/// - getStatus() -- returns current playback status
|
|
19
|
+
///
|
|
20
|
+
/// Events (via bridge.sendGlobalEvent):
|
|
21
|
+
/// - audio:progress { currentTime, duration }
|
|
22
|
+
/// - audio:complete {}
|
|
23
|
+
/// - audio:error { message }
|
|
24
|
+
final class AudioModule: NativeModule {
|
|
25
|
+
let moduleName = "Audio"
|
|
26
|
+
|
|
27
|
+
private var player: AVAudioPlayer?
|
|
28
|
+
private var recorder: AVAudioRecorder?
|
|
29
|
+
private var displayLink: CADisplayLink?
|
|
30
|
+
private var isPlaying = false
|
|
31
|
+
private var bridge: NativeBridge? { NativeBridge.shared }
|
|
32
|
+
|
|
33
|
+
// MARK: - Delegate to forward completion events
|
|
34
|
+
private var playerDelegate: AudioPlayerDelegate?
|
|
35
|
+
|
|
36
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
37
|
+
switch method {
|
|
38
|
+
case "play":
|
|
39
|
+
let uri = args.first as? String ?? ""
|
|
40
|
+
let options = (args.count > 1 ? args[1] as? [String: Any] : nil) ?? [:]
|
|
41
|
+
play(uri: uri, options: options, callback: callback)
|
|
42
|
+
|
|
43
|
+
case "pause":
|
|
44
|
+
pause(callback: callback)
|
|
45
|
+
|
|
46
|
+
case "resume":
|
|
47
|
+
resume(callback: callback)
|
|
48
|
+
|
|
49
|
+
case "stop":
|
|
50
|
+
stop(callback: callback)
|
|
51
|
+
|
|
52
|
+
case "seek":
|
|
53
|
+
let position = (args.first as? Double) ?? (args.first as? Int).map(Double.init) ?? 0
|
|
54
|
+
seek(position: position, callback: callback)
|
|
55
|
+
|
|
56
|
+
case "setVolume":
|
|
57
|
+
let volume = (args.first as? Double) ?? (args.first as? Int).map(Double.init) ?? 1.0
|
|
58
|
+
setVolume(Float(volume), callback: callback)
|
|
59
|
+
|
|
60
|
+
case "startRecording":
|
|
61
|
+
let options = args.first as? [String: Any] ?? [:]
|
|
62
|
+
startRecording(options: options, callback: callback)
|
|
63
|
+
|
|
64
|
+
case "stopRecording":
|
|
65
|
+
stopRecording(callback: callback)
|
|
66
|
+
|
|
67
|
+
case "pauseRecording":
|
|
68
|
+
pauseRecording(callback: callback)
|
|
69
|
+
|
|
70
|
+
case "resumeRecording":
|
|
71
|
+
resumeRecording(callback: callback)
|
|
72
|
+
|
|
73
|
+
case "getStatus":
|
|
74
|
+
getStatus(callback: callback)
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
callback(nil, "AudioModule: Unknown method '\(method)'")
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// MARK: - Playback
|
|
82
|
+
|
|
83
|
+
private func play(uri: String, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
84
|
+
DispatchQueue.main.async { [weak self] in
|
|
85
|
+
guard let self = self else { return }
|
|
86
|
+
|
|
87
|
+
// Stop any existing playback
|
|
88
|
+
self.stopProgressReporting()
|
|
89
|
+
self.player?.stop()
|
|
90
|
+
|
|
91
|
+
do {
|
|
92
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
|
|
93
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
94
|
+
} catch {
|
|
95
|
+
callback(nil, "Failed to set audio session: \(error.localizedDescription)")
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
guard let url = URL(string: uri) else {
|
|
100
|
+
callback(nil, "Invalid audio URI: \(uri)")
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if it's a remote URL — download first
|
|
105
|
+
if url.scheme == "http" || url.scheme == "https" {
|
|
106
|
+
self.downloadAndPlay(url: url, options: options, callback: callback)
|
|
107
|
+
} else {
|
|
108
|
+
self.playLocal(url: url, options: options, callback: callback)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func downloadAndPlay(url: URL, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
114
|
+
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
|
|
115
|
+
DispatchQueue.main.async {
|
|
116
|
+
guard let self = self else { return }
|
|
117
|
+
if let error = error {
|
|
118
|
+
callback(nil, "Failed to download audio: \(error.localizedDescription)")
|
|
119
|
+
self.bridge?.sendGlobalEvent("audio:error", payload: ["message": error.localizedDescription])
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
guard let data = data else {
|
|
123
|
+
callback(nil, "No audio data received")
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
do {
|
|
127
|
+
let player = try AVAudioPlayer(data: data)
|
|
128
|
+
self.setupPlayer(player, options: options, callback: callback)
|
|
129
|
+
} catch {
|
|
130
|
+
callback(nil, "Failed to initialize player: \(error.localizedDescription)")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}.resume()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func playLocal(url: URL, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
137
|
+
do {
|
|
138
|
+
let player = try AVAudioPlayer(contentsOf: url)
|
|
139
|
+
setupPlayer(player, options: options, callback: callback)
|
|
140
|
+
} catch {
|
|
141
|
+
callback(nil, "Failed to play audio: \(error.localizedDescription)")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func setupPlayer(_ player: AVAudioPlayer, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
146
|
+
let volume = Float(options["volume"] as? Double ?? 1.0)
|
|
147
|
+
let loop = options["loop"] as? Bool ?? false
|
|
148
|
+
|
|
149
|
+
player.volume = volume
|
|
150
|
+
player.numberOfLoops = loop ? -1 : 0
|
|
151
|
+
|
|
152
|
+
let delegate = AudioPlayerDelegate { [weak self] successfully in
|
|
153
|
+
guard let self = self else { return }
|
|
154
|
+
self.isPlaying = false
|
|
155
|
+
self.stopProgressReporting()
|
|
156
|
+
self.bridge?.sendGlobalEvent("audio:complete", payload: [:])
|
|
157
|
+
}
|
|
158
|
+
player.delegate = delegate
|
|
159
|
+
self.playerDelegate = delegate
|
|
160
|
+
self.player = player
|
|
161
|
+
|
|
162
|
+
player.prepareToPlay()
|
|
163
|
+
player.play()
|
|
164
|
+
self.isPlaying = true
|
|
165
|
+
self.startProgressReporting()
|
|
166
|
+
|
|
167
|
+
callback([
|
|
168
|
+
"duration": player.duration,
|
|
169
|
+
"currentTime": 0.0,
|
|
170
|
+
], nil)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func pause(callback: @escaping (Any?, String?) -> Void) {
|
|
174
|
+
DispatchQueue.main.async { [weak self] in
|
|
175
|
+
self?.player?.pause()
|
|
176
|
+
self?.isPlaying = false
|
|
177
|
+
self?.stopProgressReporting()
|
|
178
|
+
callback(nil, nil)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private func resume(callback: @escaping (Any?, String?) -> Void) {
|
|
183
|
+
DispatchQueue.main.async { [weak self] in
|
|
184
|
+
guard let self = self else { return }
|
|
185
|
+
self.player?.play()
|
|
186
|
+
self.isPlaying = true
|
|
187
|
+
self.startProgressReporting()
|
|
188
|
+
callback(nil, nil)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func stop(callback: @escaping (Any?, String?) -> Void) {
|
|
193
|
+
DispatchQueue.main.async { [weak self] in
|
|
194
|
+
guard let self = self else { return }
|
|
195
|
+
self.player?.stop()
|
|
196
|
+
self.player = nil
|
|
197
|
+
self.playerDelegate = nil
|
|
198
|
+
self.isPlaying = false
|
|
199
|
+
self.stopProgressReporting()
|
|
200
|
+
callback(nil, nil)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func seek(position: Double, callback: @escaping (Any?, String?) -> Void) {
|
|
205
|
+
DispatchQueue.main.async { [weak self] in
|
|
206
|
+
self?.player?.currentTime = position
|
|
207
|
+
callback(nil, nil)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private func setVolume(_ volume: Float, callback: @escaping (Any?, String?) -> Void) {
|
|
212
|
+
DispatchQueue.main.async { [weak self] in
|
|
213
|
+
self?.player?.volume = max(0, min(1, volume))
|
|
214
|
+
callback(nil, nil)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - Progress Reporting
|
|
219
|
+
|
|
220
|
+
private func startProgressReporting() {
|
|
221
|
+
stopProgressReporting()
|
|
222
|
+
let link = CADisplayLink(target: self, selector: #selector(reportProgress))
|
|
223
|
+
// Report at ~4 Hz (every 15 frames at 60fps)
|
|
224
|
+
link.preferredFrameRateRange = CAFrameRateRange(minimum: 4, maximum: 4, preferred: 4)
|
|
225
|
+
link.add(to: .main, forMode: .common)
|
|
226
|
+
displayLink = link
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func stopProgressReporting() {
|
|
230
|
+
displayLink?.invalidate()
|
|
231
|
+
displayLink = nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@objc private func reportProgress() {
|
|
235
|
+
guard let player = player, isPlaying else { return }
|
|
236
|
+
bridge?.sendGlobalEvent("audio:progress", payload: [
|
|
237
|
+
"currentTime": player.currentTime,
|
|
238
|
+
"duration": player.duration,
|
|
239
|
+
])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// MARK: - Recording
|
|
243
|
+
|
|
244
|
+
private func startRecording(options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
245
|
+
DispatchQueue.main.async { [weak self] in
|
|
246
|
+
guard let self = self else { return }
|
|
247
|
+
|
|
248
|
+
do {
|
|
249
|
+
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
|
|
250
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
251
|
+
} catch {
|
|
252
|
+
callback(nil, "Failed to set audio session for recording: \(error.localizedDescription)")
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let quality = options["quality"] as? String ?? "medium"
|
|
257
|
+
let format = options["format"] as? String ?? "m4a"
|
|
258
|
+
|
|
259
|
+
let ext = format == "wav" ? "wav" : "m4a"
|
|
260
|
+
let url = FileManager.default.temporaryDirectory
|
|
261
|
+
.appendingPathComponent(UUID().uuidString + ".\(ext)")
|
|
262
|
+
|
|
263
|
+
var settings: [String: Any] = [:]
|
|
264
|
+
if format == "wav" {
|
|
265
|
+
settings = [
|
|
266
|
+
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
|
267
|
+
AVSampleRateKey: quality == "high" ? 44100.0 : 22050.0,
|
|
268
|
+
AVNumberOfChannelsKey: 1,
|
|
269
|
+
AVLinearPCMBitDepthKey: 16,
|
|
270
|
+
AVLinearPCMIsFloatKey: false,
|
|
271
|
+
]
|
|
272
|
+
} else {
|
|
273
|
+
let sampleRate: Double
|
|
274
|
+
let bitRate: Int
|
|
275
|
+
switch quality {
|
|
276
|
+
case "low": sampleRate = 22050; bitRate = 32000
|
|
277
|
+
case "high": sampleRate = 44100; bitRate = 128000
|
|
278
|
+
default: sampleRate = 44100; bitRate = 64000
|
|
279
|
+
}
|
|
280
|
+
settings = [
|
|
281
|
+
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
|
282
|
+
AVSampleRateKey: sampleRate,
|
|
283
|
+
AVNumberOfChannelsKey: 1,
|
|
284
|
+
AVEncoderAudioQualityKey: AVAudioQuality.medium.rawValue,
|
|
285
|
+
AVEncoderBitRateKey: bitRate,
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
do {
|
|
290
|
+
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
|
291
|
+
recorder.prepareToRecord()
|
|
292
|
+
recorder.record()
|
|
293
|
+
self.recorder = recorder
|
|
294
|
+
callback(["uri": url.absoluteString], nil)
|
|
295
|
+
} catch {
|
|
296
|
+
callback(nil, "Failed to start recording: \(error.localizedDescription)")
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private func stopRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
302
|
+
DispatchQueue.main.async { [weak self] in
|
|
303
|
+
guard let self = self, let recorder = self.recorder else {
|
|
304
|
+
callback(nil, "No active recording")
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
let duration = recorder.currentTime
|
|
308
|
+
let uri = recorder.url.absoluteString
|
|
309
|
+
recorder.stop()
|
|
310
|
+
self.recorder = nil
|
|
311
|
+
callback(["uri": uri, "duration": duration], nil)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func pauseRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
316
|
+
DispatchQueue.main.async { [weak self] in
|
|
317
|
+
self?.recorder?.pause()
|
|
318
|
+
callback(nil, nil)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private func resumeRecording(callback: @escaping (Any?, String?) -> Void) {
|
|
323
|
+
DispatchQueue.main.async { [weak self] in
|
|
324
|
+
self?.recorder?.record()
|
|
325
|
+
callback(nil, nil)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// MARK: - Status
|
|
330
|
+
|
|
331
|
+
private func getStatus(callback: @escaping (Any?, String?) -> Void) {
|
|
332
|
+
DispatchQueue.main.async { [weak self] in
|
|
333
|
+
guard let self = self else { callback(nil, nil); return }
|
|
334
|
+
var status: [String: Any] = [
|
|
335
|
+
"isPlaying": self.isPlaying,
|
|
336
|
+
"isRecording": self.recorder?.isRecording ?? false,
|
|
337
|
+
]
|
|
338
|
+
if let player = self.player {
|
|
339
|
+
status["currentTime"] = player.currentTime
|
|
340
|
+
status["duration"] = player.duration
|
|
341
|
+
status["volume"] = player.volume
|
|
342
|
+
}
|
|
343
|
+
callback(status, nil)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// MARK: - AVAudioPlayerDelegate wrapper
|
|
349
|
+
|
|
350
|
+
private final class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate {
|
|
351
|
+
private let onComplete: (Bool) -> Void
|
|
352
|
+
|
|
353
|
+
init(onComplete: @escaping (Bool) -> Void) {
|
|
354
|
+
self.onComplete = onComplete
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
358
|
+
onComplete(flag)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
|
|
362
|
+
// Treat decode error as completion failure
|
|
363
|
+
onComplete(false)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
#endif
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import BackgroundTasks
|
|
4
|
+
|
|
5
|
+
/// Native module for scheduling background tasks using BGTaskScheduler.
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - scheduleTask(taskId, type, options) — schedule a background task
|
|
9
|
+
/// - cancelTask(taskId) — cancel a specific task
|
|
10
|
+
/// - cancelAllTasks() — cancel all scheduled tasks
|
|
11
|
+
/// - completeTask(taskId) — signal task completion from JS
|
|
12
|
+
///
|
|
13
|
+
/// Events:
|
|
14
|
+
/// - background:taskExecute — fired when a background task runs, payload: { taskId }
|
|
15
|
+
@available(iOS 13.0, *)
|
|
16
|
+
final class BackgroundTaskModule: NativeModule {
|
|
17
|
+
var moduleName: String { "BackgroundTask" }
|
|
18
|
+
private weak var bridge: NativeBridge?
|
|
19
|
+
|
|
20
|
+
/// Track active tasks so we can call setTaskCompleted from JS
|
|
21
|
+
private var activeTasks: [String: BGTask] = [:]
|
|
22
|
+
|
|
23
|
+
/// Registered task identifiers
|
|
24
|
+
private var registeredIdentifiers: Set<String> = []
|
|
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 "scheduleTask":
|
|
33
|
+
guard args.count >= 2,
|
|
34
|
+
let taskId = args[0] as? String,
|
|
35
|
+
let type = args[1] as? String else {
|
|
36
|
+
callback(nil, "scheduleTask: missing taskId or type")
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
let options = args.count >= 3 ? args[2] as? [String: Any] : nil
|
|
40
|
+
scheduleTask(taskId: taskId, type: type, options: options ?? [:], callback: callback)
|
|
41
|
+
|
|
42
|
+
case "cancelTask":
|
|
43
|
+
guard let taskId = args.first as? String else {
|
|
44
|
+
callback(nil, "cancelTask: missing taskId")
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
BGTaskScheduler.shared.cancel(taskIdentifier: taskId)
|
|
48
|
+
callback(nil, nil)
|
|
49
|
+
|
|
50
|
+
case "cancelAllTasks":
|
|
51
|
+
BGTaskScheduler.shared.cancelAllTaskRequests()
|
|
52
|
+
callback(nil, nil)
|
|
53
|
+
|
|
54
|
+
case "completeTask":
|
|
55
|
+
guard let taskId = args.first as? String else {
|
|
56
|
+
callback(nil, "completeTask: missing taskId")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
let success = (args.count >= 2 ? args[1] as? Bool : nil) ?? true
|
|
60
|
+
if let task = activeTasks[taskId] {
|
|
61
|
+
task.setTaskCompleted(success: success)
|
|
62
|
+
activeTasks.removeValue(forKey: taskId)
|
|
63
|
+
}
|
|
64
|
+
callback(nil, nil)
|
|
65
|
+
|
|
66
|
+
case "registerTask":
|
|
67
|
+
guard let taskId = args.first as? String else {
|
|
68
|
+
callback(nil, "registerTask: missing taskId")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
registerTaskHandler(taskId: taskId)
|
|
72
|
+
callback(nil, nil)
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
callback(nil, "BackgroundTaskModule: Unknown method '\(method)'")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Private
|
|
80
|
+
|
|
81
|
+
private func registerTaskHandler(taskId: String) {
|
|
82
|
+
guard !registeredIdentifiers.contains(taskId) else { return }
|
|
83
|
+
registeredIdentifiers.insert(taskId)
|
|
84
|
+
|
|
85
|
+
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: nil) { [weak self] task in
|
|
86
|
+
self?.handleTaskExecution(task: task)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func handleTaskExecution(task: BGTask) {
|
|
91
|
+
let taskId = task.identifier
|
|
92
|
+
activeTasks[taskId] = task
|
|
93
|
+
|
|
94
|
+
task.expirationHandler = { [weak self] in
|
|
95
|
+
self?.activeTasks[taskId]?.setTaskCompleted(success: false)
|
|
96
|
+
self?.activeTasks.removeValue(forKey: taskId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Notify JS that the task is executing
|
|
100
|
+
let bridge = bridge
|
|
101
|
+
DispatchQueue.main.async {
|
|
102
|
+
bridge?.dispatchGlobalEvent("background:taskExecute", payload: ["taskId": taskId])
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private func scheduleTask(taskId: String, type: String, options: [String: Any], callback: @escaping (Any?, String?) -> Void) {
|
|
107
|
+
// Ensure task handler is registered
|
|
108
|
+
registerTaskHandler(taskId: taskId)
|
|
109
|
+
|
|
110
|
+
let request: BGTaskRequest
|
|
111
|
+
if type == "processing" {
|
|
112
|
+
let processingRequest = BGProcessingTaskRequest(identifier: taskId)
|
|
113
|
+
processingRequest.requiresNetworkConnectivity = options["requiresNetworkConnectivity"] as? Bool ?? false
|
|
114
|
+
processingRequest.requiresExternalPower = options["requiresExternalPower"] as? Bool ?? false
|
|
115
|
+
request = processingRequest
|
|
116
|
+
} else {
|
|
117
|
+
// Default: app refresh
|
|
118
|
+
request = BGAppRefreshTaskRequest(identifier: taskId)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if let earliestBeginDate = options["earliestBeginDate"] as? Double {
|
|
122
|
+
request.earliestBeginDate = Date(timeIntervalSince1970: earliestBeginDate / 1000.0)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
do {
|
|
126
|
+
try BGTaskScheduler.shared.submit(request)
|
|
127
|
+
callback(nil, nil)
|
|
128
|
+
} catch {
|
|
129
|
+
callback(nil, "Failed to schedule task: \(error.localizedDescription)")
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func invokeSync(method: String, args: [Any]) -> Any? { nil }
|
|
134
|
+
}
|
|
135
|
+
#endif
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#if canImport(UIKit)
|
|
2
|
+
import UIKit
|
|
3
|
+
import LocalAuthentication
|
|
4
|
+
|
|
5
|
+
/// Native module for biometric authentication (Face ID, Touch ID, Optic ID).
|
|
6
|
+
///
|
|
7
|
+
/// Methods:
|
|
8
|
+
/// - getSupportedBiometry() -> "faceID" | "touchID" | "opticID" | "none"
|
|
9
|
+
/// - isAvailable() -> Bool
|
|
10
|
+
/// - authenticate(reason: String) -> { success: Bool, error?: String }
|
|
11
|
+
final class BiometryModule: NativeModule {
|
|
12
|
+
var moduleName: String { "Biometry" }
|
|
13
|
+
|
|
14
|
+
func invoke(method: String, args: [Any], callback: @escaping (Any?, String?) -> Void) {
|
|
15
|
+
switch method {
|
|
16
|
+
case "getSupportedBiometry":
|
|
17
|
+
callback(supportedBiometry(), nil)
|
|
18
|
+
case "isAvailable":
|
|
19
|
+
let context = LAContext()
|
|
20
|
+
var error: NSError?
|
|
21
|
+
let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
|
22
|
+
callback(available, nil)
|
|
23
|
+
case "authenticate":
|
|
24
|
+
let reason = args.first as? String ?? "Authenticate"
|
|
25
|
+
authenticate(reason: reason, callback: callback)
|
|
26
|
+
default:
|
|
27
|
+
callback(nil, "BiometryModule: Unknown method '\(method)'")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Private helpers
|
|
32
|
+
|
|
33
|
+
private func authenticate(reason: String, callback: @escaping (Any?, String?) -> Void) {
|
|
34
|
+
let context = LAContext()
|
|
35
|
+
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
|
|
36
|
+
if success {
|
|
37
|
+
callback(["success": true], nil)
|
|
38
|
+
} else {
|
|
39
|
+
let msg = error?.localizedDescription ?? "Authentication failed"
|
|
40
|
+
callback(["success": false, "error": msg], nil)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func supportedBiometry() -> String {
|
|
46
|
+
let context = LAContext()
|
|
47
|
+
var error: NSError?
|
|
48
|
+
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
|
49
|
+
return "none"
|
|
50
|
+
}
|
|
51
|
+
switch context.biometryType {
|
|
52
|
+
case .faceID: return "faceID"
|
|
53
|
+
case .touchID: return "touchID"
|
|
54
|
+
case .opticID: return "opticID"
|
|
55
|
+
default: return "none"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func invokeSync(method: String, args: [Any]) -> Any? { nil }
|
|
60
|
+
}
|
|
61
|
+
#endif
|