craft-native 0.0.37 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/android/src/index.js +268 -0
  2. package/dist/android/src/index.js.map +10 -0
  3. package/dist/android/templates/AndroidManifest.xml.template +69 -0
  4. package/dist/android/templates/CraftBridge.kt.template +3748 -0
  5. package/dist/android/templates/CraftBridgeExtensions.kt.template +383 -0
  6. package/dist/android/templates/CraftWidgetProvider.kt.template +246 -0
  7. package/dist/android/templates/MainActivity.kt.template +109 -0
  8. package/dist/android/templates/build.gradle.kts.app.template +80 -0
  9. package/dist/android/templates/build.gradle.kts.project.template +5 -0
  10. package/dist/android/templates/fastlane/Appfile +25 -0
  11. package/dist/android/templates/fastlane/Fastfile +192 -0
  12. package/dist/android/templates/fastlane/Gemfile +7 -0
  13. package/dist/android/templates/github-workflow-android.yml +248 -0
  14. package/dist/android/templates/github-workflow-test.yml +329 -0
  15. package/dist/android/templates/settings.gradle.kts.template +18 -0
  16. package/dist/android/templates/test-bridges.html +1463 -0
  17. package/dist/api/window.d.ts +18 -0
  18. package/dist/api/window.d.ts.map +1 -1
  19. package/dist/assets/index.d.ts.map +1 -1
  20. package/dist/bridge/core.d.ts +2 -0
  21. package/dist/bridge/core.d.ts.map +1 -1
  22. package/dist/cli.js +4168 -0
  23. package/dist/components/sidebar.d.ts +5 -1
  24. package/dist/components/sidebar.d.ts.map +1 -1
  25. package/dist/dev/devtools.d.ts +1 -1
  26. package/dist/dev/devtools.d.ts.map +1 -1
  27. package/dist/dev/hot-reload.d.ts +12 -3
  28. package/dist/dev/hot-reload.d.ts.map +1 -1
  29. package/dist/index.cjs +573 -208
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +585 -216
  32. package/dist/index.js.map +45 -0
  33. package/dist/ios/src/index.js +186 -0
  34. package/dist/ios/src/index.js.map +10 -0
  35. package/dist/ios/templates/CraftApp.swift +4241 -0
  36. package/dist/ios/templates/CraftAppExtensions.swift +361 -0
  37. package/dist/ios/templates/CraftWidget.swift +247 -0
  38. package/dist/ios/templates/Info.plist.template +65 -0
  39. package/dist/ios/templates/fastlane/Appfile +45 -0
  40. package/dist/ios/templates/fastlane/Fastfile +189 -0
  41. package/dist/ios/templates/fastlane/Gemfile +7 -0
  42. package/dist/ios/templates/fastlane/Matchfile +58 -0
  43. package/dist/ios/templates/github-workflow-ios.yml +192 -0
  44. package/dist/ios/templates/github-workflow-release.yml +175 -0
  45. package/dist/ios/templates/github-workflow-test.yml +272 -0
  46. package/dist/ios/templates/project.yml.template +25 -0
  47. package/dist/ios/templates/test-bridges.html +1463 -0
  48. package/dist/package.d.ts +106 -2
  49. package/dist/package.d.ts.map +1 -1
  50. package/dist/package.test.d.ts +2 -0
  51. package/dist/package.test.d.ts.map +1 -0
  52. package/dist/styles/sidebar-templates.d.ts.map +1 -1
  53. package/dist/styles/sidebars.d.ts +12 -8
  54. package/dist/styles/sidebars.d.ts.map +1 -1
  55. package/dist/templates/projects/blank/craft.config.ts +19 -0
  56. package/dist/templates/projects/blank/index.html +27 -0
  57. package/dist/templates/projects/blank/package.json +19 -0
  58. package/dist/templates/projects/blank/src/main.ts +26 -0
  59. package/dist/templates/projects/dashboard/craft.config.ts +42 -0
  60. package/dist/templates/projects/dashboard/index.html +13 -0
  61. package/dist/templates/projects/dashboard/package.json +20 -0
  62. package/dist/templates/projects/dashboard/src/main.ts +163 -0
  63. package/dist/templates/projects/dashboard/src/styles.css +280 -0
  64. package/dist/templates/projects/drawer/craft.config.ts +47 -0
  65. package/dist/templates/projects/drawer/index.html +13 -0
  66. package/dist/templates/projects/drawer/package.json +21 -0
  67. package/dist/templates/projects/drawer/src/main.ts +406 -0
  68. package/dist/templates/projects/drawer/src/styles.css +468 -0
  69. package/dist/templates/projects/ecommerce/craft.config.ts +32 -0
  70. package/dist/templates/projects/ecommerce/index.html +13 -0
  71. package/dist/templates/projects/ecommerce/package.json +19 -0
  72. package/dist/templates/projects/ecommerce/src/main.ts +194 -0
  73. package/dist/templates/projects/ecommerce/src/styles.css +169 -0
  74. package/dist/templates/projects/social/craft.config.ts +32 -0
  75. package/dist/templates/projects/social/index.html +13 -0
  76. package/dist/templates/projects/social/package.json +19 -0
  77. package/dist/templates/projects/social/src/main.ts +270 -0
  78. package/dist/templates/projects/social/src/styles.css +216 -0
  79. package/dist/templates/projects/tabs/craft.config.ts +47 -0
  80. package/dist/templates/projects/tabs/index.html +13 -0
  81. package/dist/templates/projects/tabs/package.json +21 -0
  82. package/dist/templates/projects/tabs/src/main.ts +177 -0
  83. package/dist/templates/projects/tabs/src/styles.css +163 -0
  84. package/dist/types.d.ts +29 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/updater/index.d.ts +1 -0
  87. package/dist/updater/index.d.ts.map +1 -1
  88. package/package.json +5 -12
  89. package/bin/cli.ts +0 -937
@@ -0,0 +1,4241 @@
1
+ import SwiftUI
2
+ import WebKit
3
+ import Speech
4
+ import AVFoundation
5
+ import LocalAuthentication
6
+ import Security
7
+ import UserNotifications
8
+ import Photos
9
+ import CoreLocation
10
+ import Contacts
11
+ import ContactsUI
12
+ import EventKit
13
+ import StoreKit
14
+ import Network
15
+ import CoreMotion
16
+ import CoreBluetooth
17
+ import CoreNFC
18
+ import HealthKit
19
+ import VisionKit
20
+ import UniformTypeIdentifiers
21
+ import PDFKit
22
+ import SQLite3
23
+ import AuthenticationServices
24
+ import BackgroundTasks
25
+ import ARKit
26
+ import RealityKit
27
+ import SceneKit
28
+ import Vision
29
+ import WidgetKit
30
+ import Intents
31
+ import WatchConnectivity
32
+
33
+ // MARK: - App Entry Point
34
+ @main
35
+ struct CraftApp: App {
36
+ @StateObject private var appState = AppState()
37
+
38
+ var body: some Scene {
39
+ WindowGroup {
40
+ CraftWebView(config: appState.config)
41
+ .ignoresSafeArea()
42
+ .preferredColorScheme(appState.config.darkMode ? .dark : .light)
43
+ .environmentObject(appState)
44
+ .onOpenURL { url in
45
+ // Handle deep links and universal links
46
+ DeepLinkManager.shared.handleURL(url)
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ // MARK: - Deep Link Manager
53
+ class DeepLinkManager {
54
+ static let shared = DeepLinkManager()
55
+
56
+ private var initialURL: URL?
57
+ private var pendingURL: URL?
58
+ private weak var webView: WKWebView?
59
+ private var isReady = false
60
+
61
+ private init() {}
62
+
63
+ func setWebView(_ webView: WKWebView) {
64
+ self.webView = webView
65
+ }
66
+
67
+ func setReady() {
68
+ isReady = true
69
+ // If there's a pending URL, dispatch it now
70
+ if let url = pendingURL {
71
+ dispatchDeepLink(url)
72
+ pendingURL = nil
73
+ }
74
+ }
75
+
76
+ func handleURL(_ url: URL) {
77
+ // Store as initial URL if this is the first one
78
+ if initialURL == nil {
79
+ initialURL = url
80
+ }
81
+
82
+ if isReady, let webView = webView {
83
+ dispatchDeepLink(url)
84
+ } else {
85
+ // Store for later when web view is ready
86
+ pendingURL = url
87
+ }
88
+ }
89
+
90
+ func getInitialURL() -> URL? {
91
+ return initialURL
92
+ }
93
+
94
+ private func dispatchDeepLink(_ url: URL) {
95
+ guard let webView = webView else { return }
96
+
97
+ // Parse URL components
98
+ var params: [String: Any] = [
99
+ "url": url.absoluteString,
100
+ "scheme": url.scheme ?? "",
101
+ "host": url.host ?? "",
102
+ "path": url.path,
103
+ "query": url.query ?? ""
104
+ ]
105
+
106
+ // Parse query parameters
107
+ if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
108
+ let queryItems = components.queryItems {
109
+ var queryParams: [String: String] = [:]
110
+ for item in queryItems {
111
+ queryParams[item.name] = item.value ?? ""
112
+ }
113
+ params["queryParams"] = queryParams
114
+ }
115
+
116
+ do {
117
+ let jsonData = try JSONSerialization.data(withJSONObject: params)
118
+ let jsonStr = String(data: jsonData, encoding: .utf8) ?? "{}"
119
+ let script = "window.dispatchEvent(new CustomEvent('craftDeepLink', {detail: \(jsonStr)}));"
120
+ DispatchQueue.main.async {
121
+ webView.evaluateJavaScript(script, completionHandler: nil)
122
+ }
123
+ } catch {
124
+ print("Deep link JSON error: \(error)")
125
+ }
126
+ }
127
+ }
128
+
129
+ // MARK: - App State
130
+ class AppState: ObservableObject {
131
+ @Published var config: CraftConfig
132
+
133
+ init() {
134
+ // Load config from craft.config.json if available
135
+ if let configURL = Bundle.main.url(forResource: "craft.config", withExtension: "json"),
136
+ let data = try? Data(contentsOf: configURL),
137
+ let config = try? JSONDecoder().decode(CraftConfig.self, from: data) {
138
+ self.config = config
139
+ } else {
140
+ self.config = CraftConfig()
141
+ }
142
+ }
143
+ }
144
+
145
+ // MARK: - Configuration
146
+ struct CraftConfig: Codable {
147
+ var appName: String = "Craft App"
148
+ var bundleId: String = "com.craft.app"
149
+ var darkMode: Bool = true
150
+ var backgroundColor: String = "#1a1a2e"
151
+ var enableSpeechRecognition: Bool = true
152
+ var enableHaptics: Bool = true
153
+ var enableShare: Bool = true
154
+ var enableCamera: Bool = true
155
+ var enableBiometric: Bool = true
156
+ var enablePushNotifications: Bool = false
157
+ var enableSecureStorage: Bool = true
158
+ var enableGeolocation: Bool = true
159
+ var enableClipboard: Bool = true
160
+ var enableContacts: Bool = true
161
+ var enableCalendar: Bool = true
162
+ var enableLocalNotifications: Bool = true
163
+ var enableInAppPurchase: Bool = true
164
+ var enableKeepAwake: Bool = true
165
+ var enableOrientationLock: Bool = true
166
+ var enableDeepLinks: Bool = true
167
+ var enableQRScanner: Bool = true
168
+ var enableFilePicker: Bool = true
169
+ var enableFileDownload: Bool = true
170
+ var enableSocialAuth: Bool = true
171
+ var enableAudioRecording: Bool = true
172
+ var enableVideoRecording: Bool = true
173
+ var enableMotionSensors: Bool = true
174
+ var enableLocalDatabase: Bool = true
175
+ var enableBluetooth: Bool = true
176
+ var enableNFC: Bool = true
177
+ var enableHealthKit: Bool = false
178
+ var enableBackgroundTasks: Bool = true
179
+ var enableScreenCapture: Bool = true
180
+ var enablePDFViewer: Bool = true
181
+ var enableAR: Bool = true
182
+ var enableMLKit: Bool = true
183
+ var devServerURL: String? = nil
184
+ }
185
+
186
+ // MARK: - WebView
187
+ struct CraftWebView: UIViewRepresentable {
188
+ let config: CraftConfig
189
+
190
+ func makeUIView(context: Context) -> WKWebView {
191
+ let webConfig = WKWebViewConfiguration()
192
+ webConfig.defaultWebpagePreferences.allowsContentJavaScript = true
193
+ webConfig.allowsInlineMediaPlayback = true
194
+ webConfig.mediaTypesRequiringUserActionForPlayback = []
195
+
196
+ // Add native bridge
197
+ let contentController = WKUserContentController()
198
+ contentController.add(context.coordinator, name: "craft")
199
+ webConfig.userContentController = contentController
200
+
201
+ let webView = WKWebView(frame: .zero, configuration: webConfig)
202
+ webView.navigationDelegate = context.coordinator
203
+ webView.isOpaque = false
204
+
205
+ // Register with DeepLinkManager
206
+ DeepLinkManager.shared.setWebView(webView)
207
+
208
+ // Parse background color
209
+ let bgColor = UIColor(hex: config.backgroundColor) ?? .black
210
+ webView.backgroundColor = bgColor
211
+ webView.scrollView.backgroundColor = bgColor
212
+
213
+ // Load content
214
+ if let devURL = config.devServerURL, !devURL.isEmpty {
215
+ // Development mode - connect to server
216
+ if let url = URL(string: devURL) {
217
+ webView.load(URLRequest(url: url))
218
+ }
219
+ } else if let htmlPath = Bundle.main.path(forResource: "index", ofType: "html") {
220
+ // Production mode - load bundled HTML
221
+ let htmlURL = URL(fileURLWithPath: htmlPath)
222
+ webView.loadFileURL(htmlURL, allowingReadAccessTo: htmlURL.deletingLastPathComponent())
223
+ }
224
+
225
+ return webView
226
+ }
227
+
228
+ func updateUIView(_ webView: WKWebView, context: Context) {}
229
+
230
+ func makeCoordinator() -> Coordinator {
231
+ Coordinator(config: config)
232
+ }
233
+
234
+ // MARK: - Coordinator (Native Bridge)
235
+ class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, UIImagePickerControllerDelegate, UINavigationControllerDelegate, CLLocationManagerDelegate, ARSCNViewDelegate, WCSessionDelegate {
236
+ let config: CraftConfig
237
+ private var speechRecognizer: SFSpeechRecognizer?
238
+ private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
239
+ private var recognitionTask: SFSpeechRecognitionTask?
240
+ private var audioEngine = AVAudioEngine()
241
+ private weak var webView: WKWebView?
242
+ private var pendingCallbackId: String?
243
+
244
+ // Location
245
+ private var locationManager: CLLocationManager?
246
+ private var locationCallbackId: String?
247
+
248
+ // Network monitoring
249
+ private var networkMonitor: NWPathMonitor?
250
+ private var isConnected = true
251
+ private var connectionType = "unknown"
252
+
253
+ // Contacts
254
+ private var contactStore: CNContactStore?
255
+
256
+ // Calendar
257
+ private var eventStore: EKEventStore?
258
+
259
+ // Keep awake
260
+ private var isKeepingAwake = false
261
+
262
+ // Orientation lock
263
+ private var lockedOrientation: UIInterfaceOrientationMask?
264
+
265
+ // Deep links pending
266
+ private var pendingDeepLink: URL?
267
+
268
+ // Motion sensors
269
+ private var motionManager: CMMotionManager?
270
+ private var isMotionUpdating = false
271
+
272
+ // Bluetooth
273
+ private var centralManager: CBCentralManager?
274
+ private var peripheralManager: CBPeripheralManager?
275
+ private var discoveredPeripherals: [CBPeripheral] = []
276
+
277
+ // Audio recording
278
+ private var audioRecorder: AVAudioRecorder?
279
+ private var recordingURL: URL?
280
+
281
+ // Health
282
+ private var healthStore: HKHealthStore?
283
+
284
+ // SQLite database
285
+ private var db: OpaquePointer?
286
+
287
+ // Pending callback for async operations
288
+ private var pendingCallbackId: String?
289
+
290
+ init(config: CraftConfig) {
291
+ self.config = config
292
+ super.init()
293
+ if config.enableSpeechRecognition {
294
+ speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
295
+ }
296
+ if config.enableGeolocation {
297
+ locationManager = CLLocationManager()
298
+ }
299
+ if config.enableContacts {
300
+ contactStore = CNContactStore()
301
+ }
302
+ if config.enableCalendar {
303
+ eventStore = EKEventStore()
304
+ }
305
+ if config.enableMotionSensors {
306
+ motionManager = CMMotionManager()
307
+ }
308
+ if config.enableHealthKit && HKHealthStore.isHealthDataAvailable() {
309
+ healthStore = HKHealthStore()
310
+ }
311
+ if config.enableLocalDatabase {
312
+ setupDatabase()
313
+ }
314
+ setupNetworkMonitoring()
315
+ }
316
+
317
+ private func setupDatabase() {
318
+ let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
319
+ let dbPath = documentsPath.appendingPathComponent("craft.db").path
320
+
321
+ if sqlite3_open(dbPath, &db) == SQLITE_OK {
322
+ print("Database opened at \(dbPath)")
323
+ } else {
324
+ print("Failed to open database")
325
+ }
326
+ }
327
+
328
+ private func setupNetworkMonitoring() {
329
+ networkMonitor = NWPathMonitor()
330
+ networkMonitor?.pathUpdateHandler = { [weak self] path in
331
+ self?.isConnected = path.status == .satisfied
332
+ if path.usesInterfaceType(.wifi) {
333
+ self?.connectionType = "wifi"
334
+ } else if path.usesInterfaceType(.cellular) {
335
+ self?.connectionType = "cellular"
336
+ } else if path.usesInterfaceType(.wiredEthernet) {
337
+ self?.connectionType = "ethernet"
338
+ } else {
339
+ self?.connectionType = "unknown"
340
+ }
341
+ self?.sendToWeb("craftNetworkChange", data: [
342
+ "isConnected": self?.isConnected ?? false,
343
+ "type": self?.connectionType ?? "unknown"
344
+ ])
345
+ }
346
+ networkMonitor?.start(queue: DispatchQueue.global())
347
+ }
348
+
349
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
350
+ guard let body = message.body as? [String: Any],
351
+ let action = body["action"] as? String else { return }
352
+
353
+ let callbackId = body["callbackId"] as? String
354
+
355
+ switch action {
356
+ case "startListening":
357
+ if config.enableSpeechRecognition { startSpeechRecognition() }
358
+ case "stopListening":
359
+ stopSpeechRecognition()
360
+ case "haptic":
361
+ if config.enableHaptics {
362
+ let style = body["style"] as? String ?? "medium"
363
+ triggerHaptic(style: style)
364
+ }
365
+ case "share":
366
+ if config.enableShare, let text = body["text"] as? String {
367
+ shareText(text)
368
+ }
369
+ case "openCamera":
370
+ if config.enableCamera {
371
+ pendingCallbackId = callbackId
372
+ openCamera()
373
+ }
374
+ case "pickImage":
375
+ if config.enableCamera {
376
+ pendingCallbackId = callbackId
377
+ pickImage()
378
+ }
379
+ case "authenticate":
380
+ if config.enableBiometric {
381
+ let reason = body["reason"] as? String ?? "Authenticate to continue"
382
+ authenticate(reason: reason, callbackId: callbackId)
383
+ }
384
+ case "registerPush":
385
+ if config.enablePushNotifications {
386
+ registerPushNotifications(callbackId: callbackId)
387
+ }
388
+ case "secureSet":
389
+ if config.enableSecureStorage,
390
+ let key = body["key"] as? String,
391
+ let value = body["value"] as? String {
392
+ let success = secureStore(key: key, value: value)
393
+ resolveCallback(callbackId, result: success)
394
+ }
395
+ case "secureGet":
396
+ if config.enableSecureStorage,
397
+ let key = body["key"] as? String {
398
+ let value = secureRetrieve(key: key)
399
+ resolveCallback(callbackId, result: value as Any)
400
+ }
401
+ case "secureRemove":
402
+ if config.enableSecureStorage,
403
+ let key = body["key"] as? String {
404
+ let success = secureRemove(key: key)
405
+ resolveCallback(callbackId, result: success)
406
+ }
407
+ case "log":
408
+ if let msg = body["message"] as? String {
409
+ print("[Craft Web] \(msg)")
410
+ }
411
+
412
+ // Memory Usage (for profiling)
413
+ case "getMemoryUsage":
414
+ getMemoryUsage(callbackId: callbackId)
415
+
416
+ // Geolocation
417
+ case "getCurrentPosition":
418
+ if config.enableGeolocation {
419
+ getCurrentPosition(callbackId: callbackId)
420
+ }
421
+ case "watchPosition":
422
+ if config.enableGeolocation {
423
+ watchPosition(callbackId: callbackId)
424
+ }
425
+ case "clearWatch":
426
+ stopWatchingPosition()
427
+ // Clipboard
428
+ case "clipboardWrite":
429
+ if config.enableClipboard, let text = body["text"] as? String {
430
+ UIPasteboard.general.string = text
431
+ resolveCallback(callbackId, result: true)
432
+ }
433
+ case "clipboardRead":
434
+ if config.enableClipboard {
435
+ let text = UIPasteboard.general.string ?? ""
436
+ resolveCallback(callbackId, result: text)
437
+ }
438
+ // Device Info
439
+ case "getDeviceInfo":
440
+ getDeviceInfo(callbackId: callbackId)
441
+ // App Badge
442
+ case "setBadge":
443
+ if let count = body["count"] as? Int {
444
+ setBadgeCount(count, callbackId: callbackId)
445
+ }
446
+ case "clearBadge":
447
+ setBadgeCount(0, callbackId: callbackId)
448
+ // Network Status
449
+ case "getNetworkStatus":
450
+ resolveCallback(callbackId, result: ["isConnected": isConnected, "type": connectionType])
451
+ // App Review
452
+ case "requestReview":
453
+ requestAppReview()
454
+ resolveCallback(callbackId, result: true)
455
+ // Flashlight/Torch
456
+ case "setFlashlight":
457
+ if let enabled = body["enabled"] as? Bool {
458
+ setFlashlight(enabled: enabled, callbackId: callbackId)
459
+ }
460
+ // Vibrate pattern
461
+ case "vibrate":
462
+ if let pattern = body["pattern"] as? [Int] {
463
+ vibratePattern(pattern)
464
+ } else {
465
+ triggerHaptic(style: "medium")
466
+ }
467
+ resolveCallback(callbackId, result: true)
468
+ // Open URL
469
+ case "openURL":
470
+ if let urlString = body["url"] as? String, let url = URL(string: urlString) {
471
+ UIApplication.shared.open(url)
472
+ resolveCallback(callbackId, result: true)
473
+ }
474
+ // App state
475
+ case "getAppState":
476
+ let state = UIApplication.shared.applicationState
477
+ let stateStr = state == .active ? "active" : (state == .background ? "background" : "inactive")
478
+ resolveCallback(callbackId, result: stateStr)
479
+
480
+ // MARK: - Contacts
481
+ case "getContacts":
482
+ if config.enableContacts {
483
+ getContacts(callbackId: callbackId)
484
+ }
485
+ case "addContact":
486
+ if config.enableContacts,
487
+ let contactData = body["contact"] as? [String: Any] {
488
+ addContact(contactData, callbackId: callbackId)
489
+ }
490
+
491
+ // MARK: - Calendar
492
+ case "getCalendarEvents":
493
+ if config.enableCalendar {
494
+ let startDate = body["startDate"] as? Double
495
+ let endDate = body["endDate"] as? Double
496
+ getCalendarEvents(startDate: startDate, endDate: endDate, callbackId: callbackId)
497
+ }
498
+ case "createCalendarEvent":
499
+ if config.enableCalendar,
500
+ let eventData = body["event"] as? [String: Any] {
501
+ createCalendarEvent(eventData, callbackId: callbackId)
502
+ }
503
+ case "deleteCalendarEvent":
504
+ if config.enableCalendar,
505
+ let eventId = body["eventId"] as? String {
506
+ deleteCalendarEvent(eventId, callbackId: callbackId)
507
+ }
508
+
509
+ // MARK: - Local Notifications
510
+ case "scheduleNotification":
511
+ if config.enableLocalNotifications,
512
+ let notifData = body["notification"] as? [String: Any] {
513
+ scheduleLocalNotification(notifData, callbackId: callbackId)
514
+ }
515
+ case "cancelNotification":
516
+ if config.enableLocalNotifications,
517
+ let notifId = body["id"] as? String {
518
+ cancelLocalNotification(notifId, callbackId: callbackId)
519
+ }
520
+ case "cancelAllNotifications":
521
+ if config.enableLocalNotifications {
522
+ cancelAllLocalNotifications(callbackId: callbackId)
523
+ }
524
+ case "getPendingNotifications":
525
+ if config.enableLocalNotifications {
526
+ getPendingNotifications(callbackId: callbackId)
527
+ }
528
+
529
+ // MARK: - Deep Links
530
+ case "registerDeepLinkHandler":
531
+ if config.enableDeepLinks {
532
+ // Handler is registered in JS
533
+ resolveCallback(callbackId, result: true)
534
+ }
535
+ case "getInitialURL":
536
+ if config.enableDeepLinks {
537
+ if let url = DeepLinkManager.shared.getInitialURL() {
538
+ resolveCallbackJSON(callbackId, json: [
539
+ "url": url.absoluteString,
540
+ "scheme": url.scheme ?? "",
541
+ "host": url.host ?? "",
542
+ "path": url.path,
543
+ "query": url.query ?? ""
544
+ ])
545
+ } else {
546
+ resolveCallback(callbackId, result: NSNull())
547
+ }
548
+ }
549
+
550
+ // MARK: - In-App Purchase
551
+ case "getProducts":
552
+ if config.enableInAppPurchase,
553
+ let productIds = body["productIds"] as? [String] {
554
+ getProducts(productIds, callbackId: callbackId)
555
+ }
556
+ case "purchase":
557
+ if config.enableInAppPurchase,
558
+ let productId = body["productId"] as? String {
559
+ purchaseProduct(productId, callbackId: callbackId)
560
+ }
561
+ case "restorePurchases":
562
+ if config.enableInAppPurchase {
563
+ restorePurchases(callbackId: callbackId)
564
+ }
565
+
566
+ // MARK: - Keep Awake
567
+ case "setKeepAwake":
568
+ if config.enableKeepAwake,
569
+ let enabled = body["enabled"] as? Bool {
570
+ setKeepAwake(enabled, callbackId: callbackId)
571
+ }
572
+
573
+ // MARK: - Orientation Lock
574
+ case "lockOrientation":
575
+ if config.enableOrientationLock,
576
+ let orientation = body["orientation"] as? String {
577
+ lockOrientation(orientation, callbackId: callbackId)
578
+ }
579
+ case "unlockOrientation":
580
+ if config.enableOrientationLock {
581
+ unlockOrientation(callbackId: callbackId)
582
+ }
583
+
584
+ // MARK: - QR/Barcode Scanner
585
+ case "scanQRCode":
586
+ if config.enableQRScanner {
587
+ scanQRCode(callbackId: callbackId)
588
+ }
589
+
590
+ // MARK: - File Picker
591
+ case "pickFile":
592
+ if config.enableFilePicker {
593
+ let types = body["types"] as? [String]
594
+ pickFile(types: types, callbackId: callbackId)
595
+ }
596
+
597
+ // MARK: - File Download
598
+ case "downloadFile":
599
+ if config.enableFileDownload,
600
+ let url = body["url"] as? String,
601
+ let filename = body["filename"] as? String {
602
+ downloadFile(url: url, filename: filename, callbackId: callbackId)
603
+ }
604
+ case "saveFile":
605
+ if config.enableFileDownload,
606
+ let data = body["data"] as? String,
607
+ let filename = body["filename"] as? String {
608
+ saveFile(data: data, filename: filename, callbackId: callbackId)
609
+ }
610
+
611
+ // MARK: - Social Auth
612
+ case "signInWithApple":
613
+ if config.enableSocialAuth {
614
+ signInWithApple(callbackId: callbackId)
615
+ }
616
+
617
+ // MARK: - Audio Recording
618
+ case "startAudioRecording":
619
+ if config.enableAudioRecording {
620
+ startAudioRecording(callbackId: callbackId)
621
+ }
622
+ case "stopAudioRecording":
623
+ if config.enableAudioRecording {
624
+ stopAudioRecording(callbackId: callbackId)
625
+ }
626
+
627
+ // MARK: - Video Recording
628
+ case "startVideoRecording":
629
+ if config.enableVideoRecording {
630
+ startVideoRecording(callbackId: callbackId)
631
+ }
632
+
633
+ // MARK: - Motion Sensors
634
+ case "startMotionUpdates":
635
+ if config.enableMotionSensors {
636
+ let interval = body["interval"] as? Double ?? 100
637
+ startMotionUpdates(interval: interval, callbackId: callbackId)
638
+ }
639
+ case "stopMotionUpdates":
640
+ stopMotionUpdates()
641
+ resolveCallback(callbackId, result: true)
642
+
643
+ // MARK: - Local Database
644
+ case "dbExecute":
645
+ if config.enableLocalDatabase,
646
+ let sql = body["sql"] as? String {
647
+ let params = body["params"] as? [Any]
648
+ dbExecute(sql: sql, params: params, callbackId: callbackId)
649
+ }
650
+ case "dbQuery":
651
+ if config.enableLocalDatabase,
652
+ let sql = body["sql"] as? String {
653
+ let params = body["params"] as? [Any]
654
+ dbQuery(sql: sql, params: params, callbackId: callbackId)
655
+ }
656
+
657
+ // MARK: - Bluetooth
658
+ case "startBluetoothScan":
659
+ if config.enableBluetooth {
660
+ startBluetoothScan(callbackId: callbackId)
661
+ }
662
+ case "stopBluetoothScan":
663
+ stopBluetoothScan()
664
+ resolveCallback(callbackId, result: true)
665
+
666
+ // MARK: - NFC
667
+ case "scanNFC":
668
+ if config.enableNFC {
669
+ scanNFC(callbackId: callbackId)
670
+ }
671
+
672
+ // MARK: - Health
673
+ case "requestHealthAuthorization":
674
+ if config.enableHealthKit {
675
+ let types = body["types"] as? [String] ?? []
676
+ requestHealthAuthorization(types: types, callbackId: callbackId)
677
+ }
678
+ case "getHealthData":
679
+ if config.enableHealthKit,
680
+ let dataType = body["type"] as? String {
681
+ let startDate = body["startDate"] as? Double
682
+ let endDate = body["endDate"] as? Double
683
+ getHealthData(type: dataType, startDate: startDate, endDate: endDate, callbackId: callbackId)
684
+ }
685
+
686
+ // MARK: - Screen Capture
687
+ case "takeScreenshot":
688
+ if config.enableScreenCapture {
689
+ takeScreenshot(callbackId: callbackId)
690
+ }
691
+
692
+ // MARK: - Background Tasks
693
+ case "registerBackgroundTask":
694
+ if config.enableBackgroundTasks,
695
+ let taskId = body["taskId"] as? String {
696
+ registerBackgroundTask(taskId: taskId, callbackId: callbackId)
697
+ }
698
+ case "scheduleBackgroundTask":
699
+ if config.enableBackgroundTasks,
700
+ let taskId = body["taskId"] as? String {
701
+ let delay = body["delay"] as? Double ?? 900 // 15 minutes default
702
+ let requiresNetwork = body["requiresNetwork"] as? Bool ?? false
703
+ let requiresCharging = body["requiresCharging"] as? Bool ?? false
704
+ scheduleBackgroundTask(taskId: taskId, delay: delay, requiresNetwork: requiresNetwork, requiresCharging: requiresCharging, callbackId: callbackId)
705
+ }
706
+ case "cancelBackgroundTask":
707
+ if config.enableBackgroundTasks,
708
+ let taskId = body["taskId"] as? String {
709
+ cancelBackgroundTask(taskId: taskId, callbackId: callbackId)
710
+ }
711
+ case "cancelAllBackgroundTasks":
712
+ if config.enableBackgroundTasks {
713
+ cancelAllBackgroundTasks(callbackId: callbackId)
714
+ }
715
+
716
+ // MARK: - PDF Viewer
717
+ case "openPDF":
718
+ if config.enablePDFViewer,
719
+ let source = body["source"] as? String {
720
+ let page = body["page"] as? Int ?? 0
721
+ openPDF(source: source, page: page, callbackId: callbackId)
722
+ }
723
+ case "closePDF":
724
+ closePDF(callbackId: callbackId)
725
+
726
+ // MARK: - Contacts Picker
727
+ case "pickContact":
728
+ if config.enableContacts {
729
+ let multiple = body["multiple"] as? Bool ?? false
730
+ pickContact(multiple: multiple, callbackId: callbackId)
731
+ }
732
+
733
+ // MARK: - App Shortcuts
734
+ case "setShortcuts":
735
+ if let shortcuts = body["shortcuts"] as? [[String: Any]] {
736
+ setAppShortcuts(shortcuts: shortcuts, callbackId: callbackId)
737
+ }
738
+ case "clearShortcuts":
739
+ clearAppShortcuts(callbackId: callbackId)
740
+
741
+ // MARK: - Keychain Sharing
742
+ case "setSharedItem":
743
+ if let key = body["key"] as? String,
744
+ let value = body["value"] as? String {
745
+ let group = body["group"] as? String
746
+ setSharedKeychainItem(key: key, value: value, group: group, callbackId: callbackId)
747
+ }
748
+ case "getSharedItem":
749
+ if let key = body["key"] as? String {
750
+ let group = body["group"] as? String
751
+ getSharedKeychainItem(key: key, group: group, callbackId: callbackId)
752
+ }
753
+ case "removeSharedItem":
754
+ if let key = body["key"] as? String {
755
+ let group = body["group"] as? String
756
+ removeSharedKeychainItem(key: key, group: group, callbackId: callbackId)
757
+ }
758
+
759
+ // MARK: - Local Auth Persistence
760
+ case "setBiometricPersistence":
761
+ if let enabled = body["enabled"] as? Bool {
762
+ let duration = body["duration"] as? Double ?? 300 // 5 min default
763
+ setBiometricPersistence(enabled: enabled, duration: duration, callbackId: callbackId)
764
+ }
765
+ case "checkBiometricPersistence":
766
+ checkBiometricPersistence(callbackId: callbackId)
767
+ case "clearBiometricPersistence":
768
+ clearBiometricPersistence(callbackId: callbackId)
769
+
770
+ // MARK: - AR (ARKit)
771
+ case "startAR":
772
+ if config.enableAR {
773
+ let options = body["options"] as? [String: Any] ?? [:]
774
+ startAR(options: options, callbackId: callbackId)
775
+ }
776
+ case "stopAR":
777
+ if config.enableAR {
778
+ stopAR(callbackId: callbackId)
779
+ }
780
+ case "placeARObject":
781
+ if config.enableAR,
782
+ let model = body["model"] as? String {
783
+ let position = body["position"] as? [String: Double]
784
+ placeARObject(model: model, position: position, callbackId: callbackId)
785
+ }
786
+ case "removeARObject":
787
+ if config.enableAR,
788
+ let objectId = body["objectId"] as? String {
789
+ removeARObject(objectId: objectId, callbackId: callbackId)
790
+ }
791
+ case "getARPlanes":
792
+ if config.enableAR {
793
+ getARPlanes(callbackId: callbackId)
794
+ }
795
+
796
+ // MARK: - ML (Core ML / Vision)
797
+ case "classifyImage":
798
+ if config.enableMLKit,
799
+ let imageBase64 = body["image"] as? String {
800
+ classifyImage(imageBase64: imageBase64, callbackId: callbackId)
801
+ }
802
+ case "detectObjects":
803
+ if config.enableMLKit,
804
+ let imageBase64 = body["image"] as? String {
805
+ detectObjects(imageBase64: imageBase64, callbackId: callbackId)
806
+ }
807
+ case "recognizeText":
808
+ if config.enableMLKit,
809
+ let imageBase64 = body["image"] as? String {
810
+ recognizeText(imageBase64: imageBase64, callbackId: callbackId)
811
+ }
812
+
813
+ // MARK: - Widget
814
+ case "updateWidget":
815
+ if let data = body["data"] as? [String: Any] {
816
+ updateWidget(data: data, callbackId: callbackId)
817
+ }
818
+ case "reloadWidgets":
819
+ reloadAllWidgets(callbackId: callbackId)
820
+
821
+ // MARK: - Siri Shortcuts
822
+ case "registerSiriShortcut":
823
+ if let phrase = body["phrase"] as? String,
824
+ let action = body["action"] as? String {
825
+ registerSiriShortcut(phrase: phrase, action: action, callbackId: callbackId)
826
+ }
827
+ case "removeSiriShortcut":
828
+ if let action = body["action"] as? String {
829
+ removeSiriShortcut(action: action, callbackId: callbackId)
830
+ }
831
+
832
+ // MARK: - Watch Connectivity
833
+ case "sendToWatch":
834
+ if let message = body["message"] as? [String: Any] {
835
+ sendMessageToWatch(message: message, callbackId: callbackId)
836
+ }
837
+ case "updateWatchContext":
838
+ if let context = body["context"] as? [String: Any] {
839
+ updateWatchContext(context: context, callbackId: callbackId)
840
+ }
841
+ case "isWatchReachable":
842
+ isWatchReachable(callbackId: callbackId)
843
+
844
+ default:
845
+ break
846
+ }
847
+ }
848
+
849
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
850
+ self.webView = webView
851
+ injectNativeBridge()
852
+ }
853
+
854
+ private func injectNativeBridge() {
855
+ let laContext = LAContext()
856
+ var biometricAvailable = false
857
+ if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
858
+ biometricAvailable = true
859
+ }
860
+
861
+ let script = """
862
+ window.craft = {
863
+ platform: 'ios',
864
+ capabilities: {
865
+ haptics: \(config.enableHaptics),
866
+ speechRecognition: \(config.enableSpeechRecognition),
867
+ share: \(config.enableShare),
868
+ camera: \(config.enableCamera),
869
+ biometric: \(biometricAvailable && config.enableBiometric),
870
+ pushNotifications: \(config.enablePushNotifications),
871
+ secureStorage: \(config.enableSecureStorage),
872
+ geolocation: \(config.enableGeolocation),
873
+ clipboard: \(config.enableClipboard),
874
+ contacts: \(config.enableContacts),
875
+ calendar: \(config.enableCalendar),
876
+ localNotifications: \(config.enableLocalNotifications),
877
+ inAppPurchase: \(config.enableInAppPurchase),
878
+ keepAwake: \(config.enableKeepAwake),
879
+ orientationLock: \(config.enableOrientationLock),
880
+ deepLinks: \(config.enableDeepLinks),
881
+ flashlight: true,
882
+ network: true,
883
+ deviceInfo: true,
884
+ badge: true,
885
+ appReview: true
886
+ },
887
+
888
+ _callbacks: {},
889
+ _callbackId: 0,
890
+
891
+ _createCallback: function() {
892
+ var id = 'cb_' + (++this._callbackId);
893
+ var self = this;
894
+ return new Promise(function(resolve, reject) {
895
+ self._callbacks[id] = {resolve: resolve, reject: reject};
896
+ setTimeout(function() {
897
+ if (self._callbacks[id]) {
898
+ reject(new Error('Timeout'));
899
+ delete self._callbacks[id];
900
+ }
901
+ }, 30000);
902
+ }).finally(function() {
903
+ return id;
904
+ });
905
+ },
906
+
907
+ _resolveCallback: function(id, result) {
908
+ if (this._callbacks[id]) {
909
+ this._callbacks[id].resolve(result);
910
+ delete this._callbacks[id];
911
+ }
912
+ },
913
+
914
+ _rejectCallback: function(id, error, code) {
915
+ if (this._callbacks[id]) {
916
+ var err = new Error(error);
917
+ err.code = code || 'CRAFT_ERROR';
918
+ err.bridge = true;
919
+ this._callbacks[id].reject(err);
920
+ delete this._callbacks[id];
921
+ // Store last error for debugging
922
+ this._lastError = {
923
+ message: error,
924
+ code: code || 'CRAFT_ERROR',
925
+ timestamp: Date.now(),
926
+ stack: null
927
+ };
928
+ // Dispatch global error event if debug mode
929
+ if (this._debug) {
930
+ window.dispatchEvent(new CustomEvent('craftError', {detail: this._lastError}));
931
+ }
932
+ // Store in error history
933
+ this._errorHistory.push(this._lastError);
934
+ if (this._errorHistory.length > 50) this._errorHistory.shift();
935
+ }
936
+ },
937
+
938
+ // Enhanced reject with native stack trace
939
+ _rejectCallbackWithStack: function(id, error, code, nativeStack) {
940
+ if (this._callbacks[id]) {
941
+ var err = new Error(error);
942
+ err.code = code || 'CRAFT_ERROR';
943
+ err.bridge = true;
944
+ err.nativeStack = nativeStack;
945
+ this._callbacks[id].reject(err);
946
+ delete this._callbacks[id];
947
+ // Store last error with native stack
948
+ this._lastError = {
949
+ message: error,
950
+ code: code || 'CRAFT_ERROR',
951
+ timestamp: Date.now(),
952
+ nativeStack: nativeStack,
953
+ jsStack: new Error().stack
954
+ };
955
+ if (this._debug) {
956
+ window.dispatchEvent(new CustomEvent('craftError', {detail: this._lastError}));
957
+ console.error('[Craft Error]', code, error, '\\nNative:', nativeStack);
958
+ }
959
+ this._errorHistory.push(this._lastError);
960
+ if (this._errorHistory.length > 50) this._errorHistory.shift();
961
+ }
962
+ },
963
+
964
+ // Debug mode
965
+ _debug: false,
966
+ _lastError: null,
967
+ _errorHistory: [],
968
+ _callLog: [],
969
+ _networkLog: [],
970
+ _consoleLog: [],
971
+ _originalConsole: null,
972
+
973
+ // Error code mappings for user-friendly messages
974
+ _errorMessages: {
975
+ 'PERMISSION_DENIED': 'Permission was denied. Please grant access in Settings.',
976
+ 'NOT_AVAILABLE': 'This feature is not available on this device.',
977
+ 'CANCELLED': 'The operation was cancelled by the user.',
978
+ 'NETWORK_ERROR': 'A network error occurred. Please check your connection.',
979
+ 'TIMEOUT': 'The operation timed out. Please try again.',
980
+ 'INVALID_PARAMS': 'Invalid parameters provided.',
981
+ 'NOT_FOUND': 'The requested resource was not found.',
982
+ 'AUTH_FAILED': 'Authentication failed.',
983
+ 'STORAGE_FULL': 'Storage is full. Please free up space.',
984
+ 'CRAFT_ERROR': 'An unexpected error occurred.'
985
+ },
986
+
987
+ // Get user-friendly error message
988
+ getErrorMessage: function(code) {
989
+ return this._errorMessages[code] || this._errorMessages['CRAFT_ERROR'];
990
+ },
991
+
992
+ // Get error history
993
+ getErrorHistory: function() {
994
+ return this._errorHistory.slice();
995
+ },
996
+
997
+ // Clear error history
998
+ clearErrorHistory: function() {
999
+ this._errorHistory = [];
1000
+ this._lastError = null;
1001
+ },
1002
+
1003
+ // Enable/disable debug mode
1004
+ setDebugMode: function(enabled) {
1005
+ this._debug = enabled;
1006
+ if (enabled) {
1007
+ this._callLog = [];
1008
+ this._networkLog = [];
1009
+ this._setupConsoleCapture();
1010
+ this._setupNetworkInspector();
1011
+ console.log('[Craft] Debug mode enabled');
1012
+ } else {
1013
+ this._restoreConsole();
1014
+ }
1015
+ },
1016
+
1017
+ // Setup console capture
1018
+ _setupConsoleCapture: function() {
1019
+ if (this._originalConsole) return; // Already setup
1020
+ var self = this;
1021
+ this._originalConsole = {
1022
+ log: console.log,
1023
+ warn: console.warn,
1024
+ error: console.error,
1025
+ info: console.info,
1026
+ debug: console.debug
1027
+ };
1028
+ ['log', 'warn', 'error', 'info', 'debug'].forEach(function(level) {
1029
+ console[level] = function() {
1030
+ var args = Array.prototype.slice.call(arguments);
1031
+ self._consoleLog.push({
1032
+ level: level,
1033
+ args: args.map(function(a) {
1034
+ try { return typeof a === 'object' ? JSON.stringify(a) : String(a); }
1035
+ catch(e) { return String(a); }
1036
+ }),
1037
+ timestamp: Date.now()
1038
+ });
1039
+ if (self._consoleLog.length > 200) self._consoleLog.shift();
1040
+ self._originalConsole[level].apply(console, arguments);
1041
+ };
1042
+ });
1043
+ },
1044
+
1045
+ // Restore original console
1046
+ _restoreConsole: function() {
1047
+ if (!this._originalConsole) return;
1048
+ console.log = this._originalConsole.log;
1049
+ console.warn = this._originalConsole.warn;
1050
+ console.error = this._originalConsole.error;
1051
+ console.info = this._originalConsole.info;
1052
+ console.debug = this._originalConsole.debug;
1053
+ this._originalConsole = null;
1054
+ },
1055
+
1056
+ // Setup network inspector
1057
+ _setupNetworkInspector: function() {
1058
+ var self = this;
1059
+ if (window._craftNetworkSetup) return;
1060
+ window._craftNetworkSetup = true;
1061
+
1062
+ // Intercept fetch
1063
+ var originalFetch = window.fetch;
1064
+ window.fetch = function(url, options) {
1065
+ var startTime = Date.now();
1066
+ var entry = {
1067
+ type: 'fetch',
1068
+ url: typeof url === 'string' ? url : url.url,
1069
+ method: (options && options.method) || 'GET',
1070
+ startTime: startTime,
1071
+ status: null,
1072
+ duration: null
1073
+ };
1074
+ self._networkLog.push(entry);
1075
+ if (self._networkLog.length > 100) self._networkLog.shift();
1076
+
1077
+ return originalFetch.apply(window, arguments).then(function(response) {
1078
+ entry.status = response.status;
1079
+ entry.duration = Date.now() - startTime;
1080
+ return response;
1081
+ }).catch(function(err) {
1082
+ entry.status = 'error';
1083
+ entry.error = err.message;
1084
+ entry.duration = Date.now() - startTime;
1085
+ throw err;
1086
+ });
1087
+ };
1088
+
1089
+ // Intercept XMLHttpRequest
1090
+ var OriginalXHR = window.XMLHttpRequest;
1091
+ window.XMLHttpRequest = function() {
1092
+ var xhr = new OriginalXHR();
1093
+ var entry = { type: 'xhr', url: '', method: '', startTime: null, status: null, duration: null };
1094
+
1095
+ var originalOpen = xhr.open;
1096
+ xhr.open = function(method, url) {
1097
+ entry.method = method;
1098
+ entry.url = url;
1099
+ return originalOpen.apply(xhr, arguments);
1100
+ };
1101
+
1102
+ var originalSend = xhr.send;
1103
+ xhr.send = function() {
1104
+ entry.startTime = Date.now();
1105
+ self._networkLog.push(entry);
1106
+ if (self._networkLog.length > 100) self._networkLog.shift();
1107
+
1108
+ xhr.addEventListener('loadend', function() {
1109
+ entry.status = xhr.status;
1110
+ entry.duration = Date.now() - entry.startTime;
1111
+ });
1112
+ return originalSend.apply(xhr, arguments);
1113
+ };
1114
+ return xhr;
1115
+ };
1116
+ },
1117
+
1118
+ // Get console log
1119
+ getConsoleLog: function() {
1120
+ return this._consoleLog.slice();
1121
+ },
1122
+
1123
+ // Clear console log
1124
+ clearConsoleLog: function() {
1125
+ this._consoleLog = [];
1126
+ },
1127
+
1128
+ // Get network log
1129
+ getNetworkLog: function() {
1130
+ return this._networkLog.slice();
1131
+ },
1132
+
1133
+ // Clear network log
1134
+ clearNetworkLog: function() {
1135
+ this._networkLog = [];
1136
+ },
1137
+
1138
+ // Get last error details
1139
+ getLastError: function() {
1140
+ return this._lastError;
1141
+ },
1142
+
1143
+ // Get call log (when debug mode is on)
1144
+ getCallLog: function() {
1145
+ return this._callLog;
1146
+ },
1147
+
1148
+ // Clear call log
1149
+ clearCallLog: function() {
1150
+ this._callLog = [];
1151
+ },
1152
+
1153
+ // Get full debug report
1154
+ getDebugReport: function() {
1155
+ return {
1156
+ enabled: this._debug,
1157
+ lastError: this._lastError,
1158
+ errorHistory: this._errorHistory.slice(-10),
1159
+ callLog: this._callLog.slice(-20),
1160
+ networkLog: this._networkLog.slice(-20),
1161
+ consoleLog: this._consoleLog.slice(-50),
1162
+ timestamp: Date.now()
1163
+ };
1164
+ },
1165
+
1166
+ // Internal: log bridge call
1167
+ _logCall: function(action, params) {
1168
+ if (this._debug) {
1169
+ var entry = {
1170
+ action: action,
1171
+ params: params,
1172
+ timestamp: Date.now()
1173
+ };
1174
+ this._callLog.push(entry);
1175
+ if (this._callLog.length > 100) this._callLog.shift();
1176
+ console.log('[Craft] ' + action, params);
1177
+ }
1178
+ },
1179
+
1180
+ // Performance Profiling
1181
+ _profiling: false,
1182
+ _profilingData: null,
1183
+ _profilingCallTimings: [],
1184
+
1185
+ startProfiling: function() {
1186
+ this._profiling = true;
1187
+ this._profilingData = {
1188
+ startTime: Date.now(),
1189
+ startMemory: null,
1190
+ callTimings: [],
1191
+ bridgeCalls: 0
1192
+ };
1193
+ this._profilingCallTimings = [];
1194
+ // Request memory info from native
1195
+ window.webkit.messageHandlers.craft.postMessage({action: 'getMemoryUsage', callbackId: '_profiling_start'});
1196
+ console.log('[Craft] Profiling started');
1197
+ return { started: true, timestamp: this._profilingData.startTime };
1198
+ },
1199
+
1200
+ stopProfiling: function() {
1201
+ var self = this;
1202
+ if (!this._profiling || !this._profilingData) {
1203
+ return Promise.resolve(null);
1204
+ }
1205
+ this._profiling = false;
1206
+ var id = 'cb_' + (++this._callbackId);
1207
+ window.webkit.messageHandlers.craft.postMessage({action: 'getMemoryUsage', callbackId: id});
1208
+ return new Promise(function(resolve) {
1209
+ self._callbacks[id] = {
1210
+ resolve: function(memory) {
1211
+ var report = {
1212
+ duration: Date.now() - self._profilingData.startTime,
1213
+ bridgeCalls: self._profilingCallTimings.length,
1214
+ callTimings: self._profilingCallTimings,
1215
+ memory: {
1216
+ start: self._profilingData.startMemory,
1217
+ end: memory
1218
+ },
1219
+ avgCallTime: self._profilingCallTimings.length > 0
1220
+ ? self._profilingCallTimings.reduce(function(a, b) { return a + b.duration; }, 0) / self._profilingCallTimings.length
1221
+ : 0
1222
+ };
1223
+ self._profilingData = null;
1224
+ console.log('[Craft] Profiling stopped', report);
1225
+ resolve(report);
1226
+ },
1227
+ reject: function() { resolve(null); }
1228
+ };
1229
+ });
1230
+ },
1231
+
1232
+ _recordCallTiming: function(action, startTime, endTime) {
1233
+ if (this._profiling) {
1234
+ this._profilingCallTimings.push({
1235
+ action: action,
1236
+ startTime: startTime,
1237
+ endTime: endTime,
1238
+ duration: endTime - startTime
1239
+ });
1240
+ }
1241
+ },
1242
+
1243
+ getProfilingData: function() {
1244
+ if (!this._profilingData) return null;
1245
+ return {
1246
+ running: this._profiling,
1247
+ duration: Date.now() - this._profilingData.startTime,
1248
+ bridgeCalls: this._profilingCallTimings.length,
1249
+ callTimings: this._profilingCallTimings
1250
+ };
1251
+ },
1252
+
1253
+ haptic: function(style) {
1254
+ window.webkit.messageHandlers.craft.postMessage({action: 'haptic', style: style || 'medium'});
1255
+ },
1256
+
1257
+ startListening: function() {
1258
+ window.webkit.messageHandlers.craft.postMessage({action: 'startListening'});
1259
+ },
1260
+
1261
+ stopListening: function() {
1262
+ window.webkit.messageHandlers.craft.postMessage({action: 'stopListening'});
1263
+ },
1264
+
1265
+ share: function(text) {
1266
+ window.webkit.messageHandlers.craft.postMessage({action: 'share', text: text});
1267
+ },
1268
+
1269
+ openCamera: function() {
1270
+ var self = this;
1271
+ var id = 'cb_' + (++this._callbackId);
1272
+ window.webkit.messageHandlers.craft.postMessage({action: 'openCamera', callbackId: id});
1273
+ return new Promise(function(resolve, reject) {
1274
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1275
+ });
1276
+ },
1277
+
1278
+ pickImage: function() {
1279
+ var self = this;
1280
+ var id = 'cb_' + (++this._callbackId);
1281
+ window.webkit.messageHandlers.craft.postMessage({action: 'pickImage', callbackId: id});
1282
+ return new Promise(function(resolve, reject) {
1283
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1284
+ });
1285
+ },
1286
+
1287
+ authenticate: function(reason) {
1288
+ var self = this;
1289
+ var id = 'cb_' + (++this._callbackId);
1290
+ window.webkit.messageHandlers.craft.postMessage({action: 'authenticate', reason: reason, callbackId: id});
1291
+ return new Promise(function(resolve, reject) {
1292
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1293
+ });
1294
+ },
1295
+
1296
+ registerPush: function() {
1297
+ var self = this;
1298
+ var id = 'cb_' + (++this._callbackId);
1299
+ window.webkit.messageHandlers.craft.postMessage({action: 'registerPush', callbackId: id});
1300
+ return new Promise(function(resolve, reject) {
1301
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1302
+ });
1303
+ },
1304
+
1305
+ secureStore: {
1306
+ set: function(key, value) {
1307
+ var self = window.craft;
1308
+ var id = 'cb_' + (++self._callbackId);
1309
+ window.webkit.messageHandlers.craft.postMessage({action: 'secureSet', key: key, value: value, callbackId: id});
1310
+ return new Promise(function(resolve, reject) {
1311
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1312
+ });
1313
+ },
1314
+ get: function(key) {
1315
+ var self = window.craft;
1316
+ var id = 'cb_' + (++self._callbackId);
1317
+ window.webkit.messageHandlers.craft.postMessage({action: 'secureGet', key: key, callbackId: id});
1318
+ return new Promise(function(resolve, reject) {
1319
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1320
+ });
1321
+ },
1322
+ remove: function(key) {
1323
+ var self = window.craft;
1324
+ var id = 'cb_' + (++self._callbackId);
1325
+ window.webkit.messageHandlers.craft.postMessage({action: 'secureRemove', key: key, callbackId: id});
1326
+ return new Promise(function(resolve, reject) {
1327
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1328
+ });
1329
+ }
1330
+ },
1331
+
1332
+ // Geolocation
1333
+ geolocation: {
1334
+ getCurrentPosition: function() {
1335
+ var self = window.craft;
1336
+ var id = 'cb_' + (++self._callbackId);
1337
+ window.webkit.messageHandlers.craft.postMessage({action: 'getCurrentPosition', callbackId: id});
1338
+ return new Promise(function(resolve, reject) {
1339
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1340
+ });
1341
+ },
1342
+ watchPosition: function(callback) {
1343
+ window.addEventListener('craftLocationUpdate', function(e) { callback(e.detail); });
1344
+ window.webkit.messageHandlers.craft.postMessage({action: 'watchPosition'});
1345
+ },
1346
+ clearWatch: function() {
1347
+ window.webkit.messageHandlers.craft.postMessage({action: 'clearWatch'});
1348
+ }
1349
+ },
1350
+
1351
+ // Clipboard
1352
+ clipboard: {
1353
+ write: function(text) {
1354
+ var self = window.craft;
1355
+ var id = 'cb_' + (++self._callbackId);
1356
+ window.webkit.messageHandlers.craft.postMessage({action: 'clipboardWrite', text: text, callbackId: id});
1357
+ return new Promise(function(resolve, reject) {
1358
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1359
+ });
1360
+ },
1361
+ read: function() {
1362
+ var self = window.craft;
1363
+ var id = 'cb_' + (++self._callbackId);
1364
+ window.webkit.messageHandlers.craft.postMessage({action: 'clipboardRead', callbackId: id});
1365
+ return new Promise(function(resolve, reject) {
1366
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1367
+ });
1368
+ }
1369
+ },
1370
+
1371
+ // Device info
1372
+ getDeviceInfo: function() {
1373
+ var self = this;
1374
+ var id = 'cb_' + (++this._callbackId);
1375
+ window.webkit.messageHandlers.craft.postMessage({action: 'getDeviceInfo', callbackId: id});
1376
+ return new Promise(function(resolve, reject) {
1377
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1378
+ });
1379
+ },
1380
+
1381
+ // App badge
1382
+ setBadge: function(count) {
1383
+ var self = this;
1384
+ var id = 'cb_' + (++this._callbackId);
1385
+ window.webkit.messageHandlers.craft.postMessage({action: 'setBadge', count: count, callbackId: id});
1386
+ return new Promise(function(resolve, reject) {
1387
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1388
+ });
1389
+ },
1390
+ clearBadge: function() {
1391
+ var self = this;
1392
+ var id = 'cb_' + (++this._callbackId);
1393
+ window.webkit.messageHandlers.craft.postMessage({action: 'clearBadge', callbackId: id});
1394
+ return new Promise(function(resolve, reject) {
1395
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1396
+ });
1397
+ },
1398
+
1399
+ // Network
1400
+ getNetworkStatus: function() {
1401
+ var self = this;
1402
+ var id = 'cb_' + (++this._callbackId);
1403
+ window.webkit.messageHandlers.craft.postMessage({action: 'getNetworkStatus', callbackId: id});
1404
+ return new Promise(function(resolve, reject) {
1405
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1406
+ });
1407
+ },
1408
+ onNetworkChange: function(callback) {
1409
+ window.addEventListener('craftNetworkChange', function(e) { callback(e.detail); });
1410
+ },
1411
+
1412
+ // App review
1413
+ requestReview: function() {
1414
+ var self = this;
1415
+ var id = 'cb_' + (++this._callbackId);
1416
+ window.webkit.messageHandlers.craft.postMessage({action: 'requestReview', callbackId: id});
1417
+ return new Promise(function(resolve, reject) {
1418
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1419
+ });
1420
+ },
1421
+
1422
+ // Flashlight
1423
+ setFlashlight: function(enabled) {
1424
+ var self = this;
1425
+ var id = 'cb_' + (++this._callbackId);
1426
+ window.webkit.messageHandlers.craft.postMessage({action: 'setFlashlight', enabled: enabled, callbackId: id});
1427
+ return new Promise(function(resolve, reject) {
1428
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1429
+ });
1430
+ },
1431
+
1432
+ // Vibrate
1433
+ vibrate: function(pattern) {
1434
+ window.webkit.messageHandlers.craft.postMessage({action: 'vibrate', pattern: pattern});
1435
+ },
1436
+
1437
+ // Open URL
1438
+ openURL: function(url) {
1439
+ var self = this;
1440
+ var id = 'cb_' + (++this._callbackId);
1441
+ window.webkit.messageHandlers.craft.postMessage({action: 'openURL', url: url, callbackId: id});
1442
+ return new Promise(function(resolve, reject) {
1443
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1444
+ });
1445
+ },
1446
+
1447
+ // App state
1448
+ getAppState: function() {
1449
+ var self = this;
1450
+ var id = 'cb_' + (++this._callbackId);
1451
+ window.webkit.messageHandlers.craft.postMessage({action: 'getAppState', callbackId: id});
1452
+ return new Promise(function(resolve, reject) {
1453
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1454
+ });
1455
+ },
1456
+
1457
+ // Contacts
1458
+ contacts: {
1459
+ getAll: function() {
1460
+ var self = window.craft;
1461
+ var id = 'cb_' + (++self._callbackId);
1462
+ window.webkit.messageHandlers.craft.postMessage({action: 'getContacts', callbackId: id});
1463
+ return new Promise(function(resolve, reject) {
1464
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1465
+ });
1466
+ },
1467
+ add: function(contact) {
1468
+ var self = window.craft;
1469
+ var id = 'cb_' + (++self._callbackId);
1470
+ window.webkit.messageHandlers.craft.postMessage({action: 'addContact', contact: contact, callbackId: id});
1471
+ return new Promise(function(resolve, reject) {
1472
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1473
+ });
1474
+ }
1475
+ },
1476
+
1477
+ // Calendar
1478
+ calendar: {
1479
+ getEvents: function(startDate, endDate) {
1480
+ var self = window.craft;
1481
+ var id = 'cb_' + (++self._callbackId);
1482
+ window.webkit.messageHandlers.craft.postMessage({action: 'getCalendarEvents', startDate: startDate, endDate: endDate, callbackId: id});
1483
+ return new Promise(function(resolve, reject) {
1484
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1485
+ });
1486
+ },
1487
+ createEvent: function(event) {
1488
+ var self = window.craft;
1489
+ var id = 'cb_' + (++self._callbackId);
1490
+ window.webkit.messageHandlers.craft.postMessage({action: 'createCalendarEvent', event: event, callbackId: id});
1491
+ return new Promise(function(resolve, reject) {
1492
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1493
+ });
1494
+ },
1495
+ deleteEvent: function(eventId) {
1496
+ var self = window.craft;
1497
+ var id = 'cb_' + (++self._callbackId);
1498
+ window.webkit.messageHandlers.craft.postMessage({action: 'deleteCalendarEvent', eventId: eventId, callbackId: id});
1499
+ return new Promise(function(resolve, reject) {
1500
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1501
+ });
1502
+ }
1503
+ },
1504
+
1505
+ // Local Notifications
1506
+ notifications: {
1507
+ schedule: function(notification) {
1508
+ var self = window.craft;
1509
+ var id = 'cb_' + (++self._callbackId);
1510
+ window.webkit.messageHandlers.craft.postMessage({action: 'scheduleNotification', notification: notification, callbackId: id});
1511
+ return new Promise(function(resolve, reject) {
1512
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1513
+ });
1514
+ },
1515
+ cancel: function(notificationId) {
1516
+ var self = window.craft;
1517
+ var id = 'cb_' + (++self._callbackId);
1518
+ window.webkit.messageHandlers.craft.postMessage({action: 'cancelNotification', id: notificationId, callbackId: id});
1519
+ return new Promise(function(resolve, reject) {
1520
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1521
+ });
1522
+ },
1523
+ cancelAll: function() {
1524
+ var self = window.craft;
1525
+ var id = 'cb_' + (++self._callbackId);
1526
+ window.webkit.messageHandlers.craft.postMessage({action: 'cancelAllNotifications', callbackId: id});
1527
+ return new Promise(function(resolve, reject) {
1528
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1529
+ });
1530
+ },
1531
+ getPending: function() {
1532
+ var self = window.craft;
1533
+ var id = 'cb_' + (++self._callbackId);
1534
+ window.webkit.messageHandlers.craft.postMessage({action: 'getPendingNotifications', callbackId: id});
1535
+ return new Promise(function(resolve, reject) {
1536
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1537
+ });
1538
+ }
1539
+ },
1540
+
1541
+ // In-App Purchase
1542
+ iap: {
1543
+ getProducts: function(productIds) {
1544
+ var self = window.craft;
1545
+ var id = 'cb_' + (++self._callbackId);
1546
+ window.webkit.messageHandlers.craft.postMessage({action: 'getProducts', productIds: productIds, callbackId: id});
1547
+ return new Promise(function(resolve, reject) {
1548
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1549
+ });
1550
+ },
1551
+ purchase: function(productId) {
1552
+ var self = window.craft;
1553
+ var id = 'cb_' + (++self._callbackId);
1554
+ window.webkit.messageHandlers.craft.postMessage({action: 'purchase', productId: productId, callbackId: id});
1555
+ return new Promise(function(resolve, reject) {
1556
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1557
+ });
1558
+ },
1559
+ restore: function() {
1560
+ var self = window.craft;
1561
+ var id = 'cb_' + (++self._callbackId);
1562
+ window.webkit.messageHandlers.craft.postMessage({action: 'restorePurchases', callbackId: id});
1563
+ return new Promise(function(resolve, reject) {
1564
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1565
+ });
1566
+ }
1567
+ },
1568
+
1569
+ // Keep Awake
1570
+ setKeepAwake: function(enabled) {
1571
+ var self = this;
1572
+ var id = 'cb_' + (++this._callbackId);
1573
+ window.webkit.messageHandlers.craft.postMessage({action: 'setKeepAwake', enabled: enabled, callbackId: id});
1574
+ return new Promise(function(resolve, reject) {
1575
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1576
+ });
1577
+ },
1578
+
1579
+ // Orientation Lock
1580
+ lockOrientation: function(orientation) {
1581
+ var self = this;
1582
+ var id = 'cb_' + (++this._callbackId);
1583
+ window.webkit.messageHandlers.craft.postMessage({action: 'lockOrientation', orientation: orientation, callbackId: id});
1584
+ return new Promise(function(resolve, reject) {
1585
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1586
+ });
1587
+ },
1588
+ unlockOrientation: function() {
1589
+ var self = this;
1590
+ var id = 'cb_' + (++this._callbackId);
1591
+ window.webkit.messageHandlers.craft.postMessage({action: 'unlockOrientation', callbackId: id});
1592
+ return new Promise(function(resolve, reject) {
1593
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1594
+ });
1595
+ },
1596
+
1597
+ // Deep Links
1598
+ onDeepLink: function(callback) {
1599
+ window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
1600
+ },
1601
+
1602
+ // Background Tasks
1603
+ backgroundTask: {
1604
+ register: function(taskId) {
1605
+ var self = window.craft;
1606
+ var id = 'cb_' + (++self._callbackId);
1607
+ window.webkit.messageHandlers.craft.postMessage({action: 'registerBackgroundTask', taskId: taskId, callbackId: id});
1608
+ return new Promise(function(resolve, reject) {
1609
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1610
+ });
1611
+ },
1612
+ schedule: function(taskId, options) {
1613
+ var self = window.craft;
1614
+ var id = 'cb_' + (++self._callbackId);
1615
+ options = options || {};
1616
+ window.webkit.messageHandlers.craft.postMessage({
1617
+ action: 'scheduleBackgroundTask',
1618
+ taskId: taskId,
1619
+ delay: options.delay || 900,
1620
+ requiresNetwork: options.requiresNetwork || false,
1621
+ requiresCharging: options.requiresCharging || false,
1622
+ callbackId: id
1623
+ });
1624
+ return new Promise(function(resolve, reject) {
1625
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1626
+ });
1627
+ },
1628
+ cancel: function(taskId) {
1629
+ var self = window.craft;
1630
+ var id = 'cb_' + (++self._callbackId);
1631
+ window.webkit.messageHandlers.craft.postMessage({action: 'cancelBackgroundTask', taskId: taskId, callbackId: id});
1632
+ return new Promise(function(resolve, reject) {
1633
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1634
+ });
1635
+ },
1636
+ cancelAll: function() {
1637
+ var self = window.craft;
1638
+ var id = 'cb_' + (++self._callbackId);
1639
+ window.webkit.messageHandlers.craft.postMessage({action: 'cancelAllBackgroundTasks', callbackId: id});
1640
+ return new Promise(function(resolve, reject) {
1641
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1642
+ });
1643
+ }
1644
+ },
1645
+
1646
+ // PDF Viewer
1647
+ openPDF: function(source, page) {
1648
+ var self = this;
1649
+ var id = 'cb_' + (++this._callbackId);
1650
+ window.webkit.messageHandlers.craft.postMessage({action: 'openPDF', source: source, page: page || 0, callbackId: id});
1651
+ return new Promise(function(resolve, reject) {
1652
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1653
+ });
1654
+ },
1655
+ closePDF: function() {
1656
+ var self = this;
1657
+ var id = 'cb_' + (++this._callbackId);
1658
+ window.webkit.messageHandlers.craft.postMessage({action: 'closePDF', callbackId: id});
1659
+ return new Promise(function(resolve, reject) {
1660
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1661
+ });
1662
+ },
1663
+
1664
+ // Contacts Picker
1665
+ pickContact: function(options) {
1666
+ var self = this;
1667
+ var id = 'cb_' + (++this._callbackId);
1668
+ options = options || {};
1669
+ window.webkit.messageHandlers.craft.postMessage({action: 'pickContact', multiple: options.multiple || false, callbackId: id});
1670
+ return new Promise(function(resolve, reject) {
1671
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1672
+ });
1673
+ },
1674
+
1675
+ // App Shortcuts
1676
+ shortcuts: {
1677
+ set: function(shortcuts) {
1678
+ var self = window.craft;
1679
+ var id = 'cb_' + (++self._callbackId);
1680
+ window.webkit.messageHandlers.craft.postMessage({action: 'setShortcuts', shortcuts: shortcuts, callbackId: id});
1681
+ return new Promise(function(resolve, reject) {
1682
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1683
+ });
1684
+ },
1685
+ clear: function() {
1686
+ var self = window.craft;
1687
+ var id = 'cb_' + (++self._callbackId);
1688
+ window.webkit.messageHandlers.craft.postMessage({action: 'clearShortcuts', callbackId: id});
1689
+ return new Promise(function(resolve, reject) {
1690
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1691
+ });
1692
+ },
1693
+ onShortcut: function(callback) {
1694
+ window.addEventListener('craftShortcut', function(e) { callback(e.detail); });
1695
+ }
1696
+ },
1697
+
1698
+ // Keychain Sharing
1699
+ sharedKeychain: {
1700
+ set: function(key, value, group) {
1701
+ var self = window.craft;
1702
+ var id = 'cb_' + (++self._callbackId);
1703
+ window.webkit.messageHandlers.craft.postMessage({action: 'setSharedItem', key: key, value: value, group: group || null, callbackId: id});
1704
+ return new Promise(function(resolve, reject) {
1705
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1706
+ });
1707
+ },
1708
+ get: function(key, group) {
1709
+ var self = window.craft;
1710
+ var id = 'cb_' + (++self._callbackId);
1711
+ window.webkit.messageHandlers.craft.postMessage({action: 'getSharedItem', key: key, group: group || null, callbackId: id});
1712
+ return new Promise(function(resolve, reject) {
1713
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1714
+ });
1715
+ },
1716
+ remove: function(key, group) {
1717
+ var self = window.craft;
1718
+ var id = 'cb_' + (++self._callbackId);
1719
+ window.webkit.messageHandlers.craft.postMessage({action: 'removeSharedItem', key: key, group: group || null, callbackId: id});
1720
+ return new Promise(function(resolve, reject) {
1721
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1722
+ });
1723
+ }
1724
+ },
1725
+
1726
+ // Local Auth Persistence
1727
+ authPersistence: {
1728
+ enable: function(duration) {
1729
+ var self = window.craft;
1730
+ var id = 'cb_' + (++self._callbackId);
1731
+ window.webkit.messageHandlers.craft.postMessage({action: 'setBiometricPersistence', enabled: true, duration: duration || 300, callbackId: id});
1732
+ return new Promise(function(resolve, reject) {
1733
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1734
+ });
1735
+ },
1736
+ disable: function() {
1737
+ var self = window.craft;
1738
+ var id = 'cb_' + (++self._callbackId);
1739
+ window.webkit.messageHandlers.craft.postMessage({action: 'setBiometricPersistence', enabled: false, callbackId: id});
1740
+ return new Promise(function(resolve, reject) {
1741
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1742
+ });
1743
+ },
1744
+ check: function() {
1745
+ var self = window.craft;
1746
+ var id = 'cb_' + (++self._callbackId);
1747
+ window.webkit.messageHandlers.craft.postMessage({action: 'checkBiometricPersistence', callbackId: id});
1748
+ return new Promise(function(resolve, reject) {
1749
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1750
+ });
1751
+ },
1752
+ clear: function() {
1753
+ var self = window.craft;
1754
+ var id = 'cb_' + (++self._callbackId);
1755
+ window.webkit.messageHandlers.craft.postMessage({action: 'clearBiometricPersistence', callbackId: id});
1756
+ return new Promise(function(resolve, reject) {
1757
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1758
+ });
1759
+ }
1760
+ },
1761
+
1762
+ // AR (ARKit)
1763
+ ar: {
1764
+ start: function(options) {
1765
+ var self = window.craft;
1766
+ var id = 'cb_' + (++self._callbackId);
1767
+ window.webkit.messageHandlers.craft.postMessage({action: 'startAR', options: options || {}, callbackId: id});
1768
+ return new Promise(function(resolve, reject) {
1769
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1770
+ });
1771
+ },
1772
+ stop: function() {
1773
+ var self = window.craft;
1774
+ var id = 'cb_' + (++self._callbackId);
1775
+ window.webkit.messageHandlers.craft.postMessage({action: 'stopAR', callbackId: id});
1776
+ return new Promise(function(resolve, reject) {
1777
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1778
+ });
1779
+ },
1780
+ placeObject: function(model, position) {
1781
+ var self = window.craft;
1782
+ var id = 'cb_' + (++self._callbackId);
1783
+ window.webkit.messageHandlers.craft.postMessage({action: 'placeARObject', model: model, position: position, callbackId: id});
1784
+ return new Promise(function(resolve, reject) {
1785
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1786
+ });
1787
+ },
1788
+ removeObject: function(objectId) {
1789
+ var self = window.craft;
1790
+ var id = 'cb_' + (++self._callbackId);
1791
+ window.webkit.messageHandlers.craft.postMessage({action: 'removeARObject', objectId: objectId, callbackId: id});
1792
+ return new Promise(function(resolve, reject) {
1793
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1794
+ });
1795
+ },
1796
+ getPlanes: function() {
1797
+ var self = window.craft;
1798
+ var id = 'cb_' + (++self._callbackId);
1799
+ window.webkit.messageHandlers.craft.postMessage({action: 'getARPlanes', callbackId: id});
1800
+ return new Promise(function(resolve, reject) {
1801
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1802
+ });
1803
+ },
1804
+ onPlaneDetected: function(callback) {
1805
+ window.addEventListener('craftARPlane', function(e) { callback(e.detail); });
1806
+ }
1807
+ },
1808
+
1809
+ // ML (Core ML / Vision)
1810
+ ml: {
1811
+ classifyImage: function(imageBase64) {
1812
+ var self = window.craft;
1813
+ var id = 'cb_' + (++self._callbackId);
1814
+ window.webkit.messageHandlers.craft.postMessage({action: 'classifyImage', image: imageBase64, callbackId: id});
1815
+ return new Promise(function(resolve, reject) {
1816
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1817
+ });
1818
+ },
1819
+ detectObjects: function(imageBase64) {
1820
+ var self = window.craft;
1821
+ var id = 'cb_' + (++self._callbackId);
1822
+ window.webkit.messageHandlers.craft.postMessage({action: 'detectObjects', image: imageBase64, callbackId: id});
1823
+ return new Promise(function(resolve, reject) {
1824
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1825
+ });
1826
+ },
1827
+ recognizeText: function(imageBase64) {
1828
+ var self = window.craft;
1829
+ var id = 'cb_' + (++self._callbackId);
1830
+ window.webkit.messageHandlers.craft.postMessage({action: 'recognizeText', image: imageBase64, callbackId: id});
1831
+ return new Promise(function(resolve, reject) {
1832
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1833
+ });
1834
+ }
1835
+ },
1836
+
1837
+ // Widgets (WidgetKit)
1838
+ widget: {
1839
+ update: function(data) {
1840
+ var self = window.craft;
1841
+ var id = 'cb_' + (++self._callbackId);
1842
+ window.webkit.messageHandlers.craft.postMessage({action: 'updateWidget', data: data, callbackId: id});
1843
+ return new Promise(function(resolve, reject) {
1844
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1845
+ });
1846
+ },
1847
+ reload: function() {
1848
+ var self = window.craft;
1849
+ var id = 'cb_' + (++self._callbackId);
1850
+ window.webkit.messageHandlers.craft.postMessage({action: 'reloadWidgets', callbackId: id});
1851
+ return new Promise(function(resolve, reject) {
1852
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1853
+ });
1854
+ }
1855
+ },
1856
+
1857
+ // Siri Shortcuts
1858
+ siri: {
1859
+ register: function(phrase, action) {
1860
+ var self = window.craft;
1861
+ var id = 'cb_' + (++self._callbackId);
1862
+ window.webkit.messageHandlers.craft.postMessage({action: 'registerSiriShortcut', phrase: phrase, action: action, callbackId: id});
1863
+ return new Promise(function(resolve, reject) {
1864
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1865
+ });
1866
+ },
1867
+ remove: function(action) {
1868
+ var self = window.craft;
1869
+ var id = 'cb_' + (++self._callbackId);
1870
+ window.webkit.messageHandlers.craft.postMessage({action: 'removeSiriShortcut', action: action, callbackId: id});
1871
+ return new Promise(function(resolve, reject) {
1872
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1873
+ });
1874
+ },
1875
+ onInvoke: function(callback) {
1876
+ window.addEventListener('craftSiriShortcut', function(e) { callback(e.detail); });
1877
+ }
1878
+ },
1879
+
1880
+ // Watch Connectivity
1881
+ watch: {
1882
+ send: function(message) {
1883
+ var self = window.craft;
1884
+ var id = 'cb_' + (++self._callbackId);
1885
+ window.webkit.messageHandlers.craft.postMessage({action: 'sendToWatch', message: message, callbackId: id});
1886
+ return new Promise(function(resolve, reject) {
1887
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1888
+ });
1889
+ },
1890
+ updateContext: function(context) {
1891
+ var self = window.craft;
1892
+ var id = 'cb_' + (++self._callbackId);
1893
+ window.webkit.messageHandlers.craft.postMessage({action: 'updateWatchContext', context: context, callbackId: id});
1894
+ return new Promise(function(resolve, reject) {
1895
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1896
+ });
1897
+ },
1898
+ onMessage: function(callback) {
1899
+ window.addEventListener('craftWatchMessage', function(e) { callback(e.detail); });
1900
+ },
1901
+ onReachabilityChange: function(callback) {
1902
+ window.addEventListener('craftWatchReachability', function(e) { callback(e.detail); });
1903
+ },
1904
+ isReachable: function() {
1905
+ var self = window.craft;
1906
+ var id = 'cb_' + (++self._callbackId);
1907
+ window.webkit.messageHandlers.craft.postMessage({action: 'isWatchReachable', callbackId: id});
1908
+ return new Promise(function(resolve, reject) {
1909
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1910
+ });
1911
+ }
1912
+ },
1913
+
1914
+ // Deep Links
1915
+ deepLinks: {
1916
+ getInitialURL: function() {
1917
+ var self = window.craft;
1918
+ var id = 'cb_' + (++self._callbackId);
1919
+ window.webkit.messageHandlers.craft.postMessage({action: 'getInitialURL', callbackId: id});
1920
+ return new Promise(function(resolve, reject) {
1921
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1922
+ });
1923
+ },
1924
+ onLink: function(callback) {
1925
+ window.addEventListener('craftDeepLink', function(e) { callback(e.detail); });
1926
+ }
1927
+ },
1928
+
1929
+ // OTA Updates
1930
+ ota: {
1931
+ _config: null,
1932
+ _status: 'idle',
1933
+ _progressCallbacks: [],
1934
+ _statusCallbacks: [],
1935
+
1936
+ configure: function(options) {
1937
+ this._config = options;
1938
+ window.webkit.messageHandlers.craft.postMessage({action: 'otaConfigure', config: options});
1939
+ },
1940
+ checkForUpdate: function() {
1941
+ var self = window.craft;
1942
+ var id = 'cb_' + (++self._callbackId);
1943
+ window.webkit.messageHandlers.craft.postMessage({action: 'otaCheckForUpdate', callbackId: id});
1944
+ return new Promise(function(resolve, reject) {
1945
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1946
+ });
1947
+ },
1948
+ downloadUpdate: function(options) {
1949
+ var self = window.craft;
1950
+ var id = 'cb_' + (++self._callbackId);
1951
+ window.webkit.messageHandlers.craft.postMessage({action: 'otaDownloadUpdate', options: options || {}, callbackId: id});
1952
+ return new Promise(function(resolve, reject) {
1953
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1954
+ });
1955
+ },
1956
+ applyUpdate: function() {
1957
+ var self = window.craft;
1958
+ var id = 'cb_' + (++self._callbackId);
1959
+ window.webkit.messageHandlers.craft.postMessage({action: 'otaApplyUpdate', callbackId: id});
1960
+ return new Promise(function(resolve, reject) {
1961
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1962
+ });
1963
+ },
1964
+ rollback: function() {
1965
+ var self = window.craft;
1966
+ var id = 'cb_' + (++self._callbackId);
1967
+ window.webkit.messageHandlers.craft.postMessage({action: 'otaRollback', callbackId: id});
1968
+ return new Promise(function(resolve, reject) {
1969
+ self._callbacks[id] = {resolve: resolve, reject: reject};
1970
+ });
1971
+ },
1972
+ getCurrentBundle: function() {
1973
+ // This returns synchronously from stored data
1974
+ return window.craft.ota._currentBundle || {
1975
+ version: '1.0.0',
1976
+ buildNumber: 1,
1977
+ hash: '',
1978
+ isOriginal: true,
1979
+ installedAt: ''
1980
+ };
1981
+ },
1982
+ onProgress: function(callback) {
1983
+ this._progressCallbacks.push(callback);
1984
+ window.addEventListener('craftOTAProgress', function(e) { callback(e.detail); });
1985
+ },
1986
+ onStatusChange: function(callback) {
1987
+ this._statusCallbacks.push(callback);
1988
+ window.addEventListener('craftOTAStatus', function(e) { callback(e.detail.status); });
1989
+ }
1990
+ },
1991
+
1992
+ log: function(msg) {
1993
+ window.webkit.messageHandlers.craft.postMessage({action: 'log', message: msg});
1994
+ }
1995
+ };
1996
+
1997
+ // Dispatch ready event
1998
+ window.dispatchEvent(new CustomEvent('craftReady', {detail: window.craft}));
1999
+ console.log('Craft iOS bridge initialized');
2000
+ """
2001
+ webView?.evaluateJavaScript(script)
2002
+
2003
+ // Mark DeepLinkManager as ready
2004
+ DeepLinkManager.shared.setReady()
2005
+ }
2006
+
2007
+ // MARK: - Callback Helpers
2008
+ private func resolveCallback(_ callbackId: String?, result: Any) {
2009
+ guard let id = callbackId else { return }
2010
+ var resultStr: String
2011
+ if let str = result as? String {
2012
+ resultStr = "'\(str.replacingOccurrences(of: "'", with: "\\'"))'"
2013
+ } else if let bool = result as? Bool {
2014
+ resultStr = bool ? "true" : "false"
2015
+ } else if result is NSNull {
2016
+ resultStr = "null"
2017
+ } else {
2018
+ resultStr = "null"
2019
+ }
2020
+ let script = "window.craft._resolveCallback('\(id)', \(resultStr));"
2021
+ DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2022
+ }
2023
+
2024
+ private func rejectCallback(_ callbackId: String?, error: String, code: String = "CRAFT_ERROR") {
2025
+ guard let id = callbackId else { return }
2026
+ let escapedError = error.replacingOccurrences(of: "'", with: "\\'").replacingOccurrences(of: "\n", with: "\\n")
2027
+ let script = "window.craft._rejectCallback('\(id)', '\(escapedError)', '\(code)');"
2028
+ DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2029
+ }
2030
+
2031
+ /// Enhanced reject with native stack trace (for debug mode)
2032
+ private func rejectCallbackWithStack(_ callbackId: String?, error: String, code: String = "CRAFT_ERROR", file: String = #file, function: String = #function, line: Int = #line) {
2033
+ guard let id = callbackId else { return }
2034
+ let escapedError = error.replacingOccurrences(of: "'", with: "\\'").replacingOccurrences(of: "\n", with: "\\n")
2035
+ let fileName = (file as NSString).lastPathComponent
2036
+ let stackTrace = "\(fileName):\(line) in \(function)"
2037
+ let escapedStack = stackTrace.replacingOccurrences(of: "'", with: "\\'")
2038
+ let script = "window.craft._rejectCallbackWithStack('\(id)', '\(escapedError)', '\(code)', '\(escapedStack)');"
2039
+ DispatchQueue.main.async { self.webView?.evaluateJavaScript(script, completionHandler: nil) }
2040
+ }
2041
+
2042
+ // MARK: - Speech Recognition
2043
+ private func startSpeechRecognition() {
2044
+ SFSpeechRecognizer.requestAuthorization { [weak self] status in
2045
+ guard status == .authorized else {
2046
+ self?.sendToWeb("craftSpeechError", data: ["error": "Not authorized"])
2047
+ return
2048
+ }
2049
+ DispatchQueue.main.async { self?.beginRecording() }
2050
+ }
2051
+ }
2052
+
2053
+ private func beginRecording() {
2054
+ if recognitionTask != nil {
2055
+ recognitionTask?.cancel()
2056
+ recognitionTask = nil
2057
+ }
2058
+
2059
+ let audioSession = AVAudioSession.sharedInstance()
2060
+ do {
2061
+ try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
2062
+ try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
2063
+ } catch {
2064
+ sendToWeb("craftSpeechError", data: ["error": "Audio session failed"])
2065
+ return
2066
+ }
2067
+
2068
+ recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
2069
+ guard let recognitionRequest = recognitionRequest,
2070
+ let speechRecognizer = speechRecognizer,
2071
+ speechRecognizer.isAvailable else {
2072
+ sendToWeb("craftSpeechError", data: ["error": "Speech recognizer unavailable"])
2073
+ return
2074
+ }
2075
+
2076
+ recognitionRequest.shouldReportPartialResults = true
2077
+ let inputNode = audioEngine.inputNode
2078
+
2079
+ recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
2080
+ if let result = result {
2081
+ let transcript = result.bestTranscription.formattedString
2082
+ self?.sendToWeb("craftSpeechResult", data: [
2083
+ "transcript": transcript,
2084
+ "isFinal": result.isFinal
2085
+ ])
2086
+ }
2087
+ if error != nil || result?.isFinal == true {
2088
+ self?.stopSpeechRecognition()
2089
+ }
2090
+ }
2091
+
2092
+ let recordingFormat = inputNode.outputFormat(forBus: 0)
2093
+ inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
2094
+ self?.recognitionRequest?.append(buffer)
2095
+ }
2096
+
2097
+ audioEngine.prepare()
2098
+ do {
2099
+ try audioEngine.start()
2100
+ sendToWeb("craftSpeechStart", data: [:])
2101
+ triggerHaptic(style: "light")
2102
+ } catch {
2103
+ sendToWeb("craftSpeechError", data: ["error": "Audio engine failed"])
2104
+ }
2105
+ }
2106
+
2107
+ private func stopSpeechRecognition() {
2108
+ audioEngine.stop()
2109
+ audioEngine.inputNode.removeTap(onBus: 0)
2110
+ recognitionRequest?.endAudio()
2111
+ recognitionRequest = nil
2112
+ recognitionTask?.cancel()
2113
+ recognitionTask = nil
2114
+ sendToWeb("craftSpeechEnd", data: [:])
2115
+ triggerHaptic(style: "light")
2116
+ }
2117
+
2118
+ // MARK: - Haptics
2119
+ private func triggerHaptic(style: String) {
2120
+ switch style {
2121
+ case "light":
2122
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
2123
+ case "heavy":
2124
+ UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
2125
+ case "success":
2126
+ UINotificationFeedbackGenerator().notificationOccurred(.success)
2127
+ case "warning":
2128
+ UINotificationFeedbackGenerator().notificationOccurred(.warning)
2129
+ case "error":
2130
+ UINotificationFeedbackGenerator().notificationOccurred(.error)
2131
+ case "selection":
2132
+ UISelectionFeedbackGenerator().selectionChanged()
2133
+ default:
2134
+ UIImpactFeedbackGenerator(style: .medium).impactOccurred()
2135
+ }
2136
+ }
2137
+
2138
+ // MARK: - Share
2139
+ private func shareText(_ text: String) {
2140
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2141
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2142
+ let activityVC = UIActivityViewController(activityItems: [text], applicationActivities: nil)
2143
+ rootVC.present(activityVC, animated: true)
2144
+ }
2145
+
2146
+ // MARK: - Camera & Photo Library
2147
+ private func openCamera() {
2148
+ DispatchQueue.main.async {
2149
+ guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
2150
+ self.rejectCallback(self.pendingCallbackId, error: "Camera not available")
2151
+ return
2152
+ }
2153
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2154
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2155
+
2156
+ let picker = UIImagePickerController()
2157
+ picker.sourceType = .camera
2158
+ picker.delegate = self
2159
+ rootVC.present(picker, animated: true)
2160
+ }
2161
+ }
2162
+
2163
+ private func pickImage() {
2164
+ DispatchQueue.main.async {
2165
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2166
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2167
+
2168
+ let picker = UIImagePickerController()
2169
+ picker.sourceType = .photoLibrary
2170
+ picker.delegate = self
2171
+ rootVC.present(picker, animated: true)
2172
+ }
2173
+ }
2174
+
2175
+ func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
2176
+ picker.dismiss(animated: true)
2177
+
2178
+ if let image = info[.originalImage] as? UIImage,
2179
+ let imageData = image.jpegData(compressionQuality: 0.8) {
2180
+ let base64 = "data:image/jpeg;base64," + imageData.base64EncodedString()
2181
+ resolveCallback(pendingCallbackId, result: base64)
2182
+ } else {
2183
+ rejectCallback(pendingCallbackId, error: "Failed to process image")
2184
+ }
2185
+ pendingCallbackId = nil
2186
+ }
2187
+
2188
+ func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
2189
+ picker.dismiss(animated: true)
2190
+ rejectCallback(pendingCallbackId, error: "Cancelled")
2191
+ pendingCallbackId = nil
2192
+ }
2193
+
2194
+ // MARK: - Biometric Authentication
2195
+ private func authenticate(reason: String, callbackId: String?) {
2196
+ let context = LAContext()
2197
+ var error: NSError?
2198
+
2199
+ if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
2200
+ context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self] success, authError in
2201
+ DispatchQueue.main.async {
2202
+ if success {
2203
+ self?.resolveCallback(callbackId, result: true)
2204
+ } else {
2205
+ self?.rejectCallback(callbackId, error: authError?.localizedDescription ?? "Authentication failed")
2206
+ }
2207
+ }
2208
+ }
2209
+ } else {
2210
+ rejectCallback(callbackId, error: error?.localizedDescription ?? "Biometric not available")
2211
+ }
2212
+ }
2213
+
2214
+ // MARK: - Push Notifications
2215
+ private func registerPushNotifications(callbackId: String?) {
2216
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { [weak self] granted, error in
2217
+ if granted {
2218
+ DispatchQueue.main.async {
2219
+ UIApplication.shared.registerForRemoteNotifications()
2220
+ // In a real app, you'd get the device token from application delegate
2221
+ self?.resolveCallback(callbackId, result: "push-registered")
2222
+ }
2223
+ } else {
2224
+ self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
2225
+ }
2226
+ }
2227
+ }
2228
+
2229
+ // MARK: - Secure Storage (Keychain)
2230
+ private func secureStore(key: String, value: String) -> Bool {
2231
+ let data = value.data(using: .utf8)!
2232
+ let query: [String: Any] = [
2233
+ kSecClass as String: kSecClassGenericPassword,
2234
+ kSecAttrAccount as String: key,
2235
+ kSecValueData as String: data,
2236
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
2237
+ ]
2238
+
2239
+ SecItemDelete(query as CFDictionary)
2240
+ let status = SecItemAdd(query as CFDictionary, nil)
2241
+ return status == errSecSuccess
2242
+ }
2243
+
2244
+ private func secureRetrieve(key: String) -> String? {
2245
+ let query: [String: Any] = [
2246
+ kSecClass as String: kSecClassGenericPassword,
2247
+ kSecAttrAccount as String: key,
2248
+ kSecReturnData as String: true,
2249
+ kSecMatchLimit as String: kSecMatchLimitOne
2250
+ ]
2251
+
2252
+ var result: AnyObject?
2253
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
2254
+
2255
+ if status == errSecSuccess, let data = result as? Data {
2256
+ return String(data: data, encoding: .utf8)
2257
+ }
2258
+ return nil
2259
+ }
2260
+
2261
+ private func secureRemove(key: String) -> Bool {
2262
+ let query: [String: Any] = [
2263
+ kSecClass as String: kSecClassGenericPassword,
2264
+ kSecAttrAccount as String: key
2265
+ ]
2266
+ let status = SecItemDelete(query as CFDictionary)
2267
+ return status == errSecSuccess || status == errSecItemNotFound
2268
+ }
2269
+
2270
+ // MARK: - Geolocation
2271
+ private func getCurrentPosition(callbackId: String?) {
2272
+ locationManager?.delegate = self
2273
+ locationCallbackId = callbackId
2274
+ locationManager?.requestWhenInUseAuthorization()
2275
+ locationManager?.requestLocation()
2276
+ }
2277
+
2278
+ private func watchPosition(callbackId: String?) {
2279
+ locationManager?.delegate = self
2280
+ locationCallbackId = callbackId
2281
+ locationManager?.requestWhenInUseAuthorization()
2282
+ locationManager?.startUpdatingLocation()
2283
+ }
2284
+
2285
+ private func stopWatchingPosition() {
2286
+ locationManager?.stopUpdatingLocation()
2287
+ }
2288
+
2289
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
2290
+ guard let location = locations.last else { return }
2291
+ let data: [String: Any] = [
2292
+ "latitude": location.coordinate.latitude,
2293
+ "longitude": location.coordinate.longitude,
2294
+ "altitude": location.altitude,
2295
+ "accuracy": location.horizontalAccuracy,
2296
+ "altitudeAccuracy": location.verticalAccuracy,
2297
+ "heading": location.course,
2298
+ "speed": location.speed,
2299
+ "timestamp": location.timestamp.timeIntervalSince1970 * 1000
2300
+ ]
2301
+
2302
+ if let callbackId = locationCallbackId {
2303
+ resolveCallback(callbackId, result: data)
2304
+ // For single request, clear callback
2305
+ if !manager.allowsBackgroundLocationUpdates {
2306
+ locationCallbackId = nil
2307
+ }
2308
+ }
2309
+
2310
+ sendToWeb("craftLocationUpdate", data: data)
2311
+ }
2312
+
2313
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
2314
+ rejectCallback(locationCallbackId, error: error.localizedDescription)
2315
+ locationCallbackId = nil
2316
+ }
2317
+
2318
+ // MARK: - Memory Usage (for Profiling)
2319
+ private func getMemoryUsage(callbackId: String?) {
2320
+ var taskInfo = mach_task_basic_info()
2321
+ var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
2322
+ let kerr: kern_return_t = withUnsafeMutablePointer(to: &taskInfo) {
2323
+ $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
2324
+ task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
2325
+ }
2326
+ }
2327
+
2328
+ if kerr == KERN_SUCCESS {
2329
+ let usedMB = Double(taskInfo.resident_size) / 1024.0 / 1024.0
2330
+ let info: [String: Any] = [
2331
+ "usedMB": round(usedMB * 100) / 100,
2332
+ "residentSize": taskInfo.resident_size,
2333
+ "virtualSize": taskInfo.virtual_size
2334
+ ]
2335
+ resolveCallbackJSON(callbackId, json: info)
2336
+ } else {
2337
+ resolveCallbackJSON(callbackId, json: ["usedMB": 0, "error": "Failed to get memory info"])
2338
+ }
2339
+ }
2340
+
2341
+ // MARK: - Device Info
2342
+ private func getDeviceInfo(callbackId: String?) {
2343
+ let device = UIDevice.current
2344
+ let screen = UIScreen.main
2345
+ let info: [String: Any] = [
2346
+ "platform": "ios",
2347
+ "model": device.model,
2348
+ "name": device.name,
2349
+ "systemName": device.systemName,
2350
+ "systemVersion": device.systemVersion,
2351
+ "identifierForVendor": device.identifierForVendor?.uuidString ?? "",
2352
+ "isSimulator": TARGET_OS_SIMULATOR != 0,
2353
+ "screenWidth": screen.bounds.width,
2354
+ "screenHeight": screen.bounds.height,
2355
+ "screenScale": screen.scale,
2356
+ "batteryLevel": device.batteryLevel,
2357
+ "batteryState": getBatteryState(device.batteryState),
2358
+ "locale": Locale.current.identifier,
2359
+ "timezone": TimeZone.current.identifier
2360
+ ]
2361
+ resolveCallback(callbackId, result: info)
2362
+ }
2363
+
2364
+ private func getBatteryState(_ state: UIDevice.BatteryState) -> String {
2365
+ switch state {
2366
+ case .charging: return "charging"
2367
+ case .full: return "full"
2368
+ case .unplugged: return "unplugged"
2369
+ default: return "unknown"
2370
+ }
2371
+ }
2372
+
2373
+ // MARK: - App Badge
2374
+ private func setBadgeCount(_ count: Int, callbackId: String?) {
2375
+ UNUserNotificationCenter.current().requestAuthorization(options: .badge) { granted, _ in
2376
+ if granted {
2377
+ DispatchQueue.main.async {
2378
+ UIApplication.shared.applicationIconBadgeNumber = count
2379
+ self.resolveCallback(callbackId, result: true)
2380
+ }
2381
+ } else {
2382
+ self.rejectCallback(callbackId, error: "Badge permission denied")
2383
+ }
2384
+ }
2385
+ }
2386
+
2387
+ // MARK: - App Review
2388
+ private func requestAppReview() {
2389
+ DispatchQueue.main.async {
2390
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
2391
+ SKStoreReviewController.requestReview(in: windowScene)
2392
+ }
2393
+ }
2394
+ }
2395
+
2396
+ // MARK: - Flashlight
2397
+ private func setFlashlight(enabled: Bool, callbackId: String?) {
2398
+ guard let device = AVCaptureDevice.default(for: .video), device.hasTorch else {
2399
+ rejectCallback(callbackId, error: "Flashlight not available")
2400
+ return
2401
+ }
2402
+
2403
+ do {
2404
+ try device.lockForConfiguration()
2405
+ device.torchMode = enabled ? .on : .off
2406
+ device.unlockForConfiguration()
2407
+ resolveCallback(callbackId, result: true)
2408
+ } catch {
2409
+ rejectCallback(callbackId, error: error.localizedDescription)
2410
+ }
2411
+ }
2412
+
2413
+ // MARK: - Vibration Pattern
2414
+ private func vibratePattern(_ pattern: [Int]) {
2415
+ // iOS doesn't support custom vibration patterns like Android
2416
+ // We'll use haptic feedback instead
2417
+ for (index, duration) in pattern.enumerated() {
2418
+ if index % 2 == 0 && duration > 0 {
2419
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(duration) / 1000.0) {
2420
+ UIImpactFeedbackGenerator(style: .medium).impactOccurred()
2421
+ }
2422
+ }
2423
+ }
2424
+ }
2425
+
2426
+ // MARK: - Contacts
2427
+ private func getContacts(callbackId: String?) {
2428
+ contactStore?.requestAccess(for: .contacts) { [weak self] granted, error in
2429
+ guard granted else {
2430
+ self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
2431
+ return
2432
+ }
2433
+
2434
+ let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactIdentifierKey] as [CNKeyDescriptor]
2435
+ let request = CNContactFetchRequest(keysToFetch: keys)
2436
+
2437
+ var contacts: [[String: Any]] = []
2438
+ do {
2439
+ try self?.contactStore?.enumerateContacts(with: request) { contact, _ in
2440
+ var phones: [String] = []
2441
+ for phone in contact.phoneNumbers {
2442
+ phones.append(phone.value.stringValue)
2443
+ }
2444
+ var emails: [String] = []
2445
+ for email in contact.emailAddresses {
2446
+ emails.append(email.value as String)
2447
+ }
2448
+ contacts.append([
2449
+ "id": contact.identifier,
2450
+ "givenName": contact.givenName,
2451
+ "familyName": contact.familyName,
2452
+ "displayName": "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespaces),
2453
+ "phoneNumbers": phones,
2454
+ "emailAddresses": emails
2455
+ ])
2456
+ }
2457
+ self?.resolveCallback(callbackId, result: contacts)
2458
+ } catch {
2459
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2460
+ }
2461
+ }
2462
+ }
2463
+
2464
+ private func addContact(_ data: [String: Any], callbackId: String?) {
2465
+ contactStore?.requestAccess(for: .contacts) { [weak self] granted, error in
2466
+ guard granted else {
2467
+ self?.rejectCallback(callbackId, error: "Permission denied")
2468
+ return
2469
+ }
2470
+
2471
+ let contact = CNMutableContact()
2472
+ if let givenName = data["givenName"] as? String { contact.givenName = givenName }
2473
+ if let familyName = data["familyName"] as? String { contact.familyName = familyName }
2474
+ if let phone = data["phone"] as? String {
2475
+ contact.phoneNumbers = [CNLabeledValue(label: CNLabelPhoneNumberMain, value: CNPhoneNumber(stringValue: phone))]
2476
+ }
2477
+ if let email = data["email"] as? String {
2478
+ contact.emailAddresses = [CNLabeledValue(label: CNLabelHome, value: email as NSString)]
2479
+ }
2480
+
2481
+ let saveRequest = CNSaveRequest()
2482
+ saveRequest.add(contact, toContainerWithIdentifier: nil)
2483
+
2484
+ do {
2485
+ try self?.contactStore?.execute(saveRequest)
2486
+ self?.resolveCallback(callbackId, result: contact.identifier)
2487
+ } catch {
2488
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2489
+ }
2490
+ }
2491
+ }
2492
+
2493
+ // MARK: - Calendar
2494
+ private func getCalendarEvents(startDate: Double?, endDate: Double?, callbackId: String?) {
2495
+ eventStore?.requestAccess(to: .event) { [weak self] granted, error in
2496
+ guard granted else {
2497
+ self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
2498
+ return
2499
+ }
2500
+
2501
+ let start = startDate != nil ? Date(timeIntervalSince1970: startDate! / 1000) : Date()
2502
+ let end = endDate != nil ? Date(timeIntervalSince1970: endDate! / 1000) : Calendar.current.date(byAdding: .month, value: 1, to: Date())!
2503
+
2504
+ let predicate = self?.eventStore?.predicateForEvents(withStart: start, end: end, calendars: nil)
2505
+ let events = self?.eventStore?.events(matching: predicate!) ?? []
2506
+
2507
+ let eventData: [[String: Any]] = events.map { event in
2508
+ return [
2509
+ "id": event.eventIdentifier ?? "",
2510
+ "title": event.title ?? "",
2511
+ "location": event.location ?? "",
2512
+ "notes": event.notes ?? "",
2513
+ "startDate": event.startDate.timeIntervalSince1970 * 1000,
2514
+ "endDate": event.endDate.timeIntervalSince1970 * 1000,
2515
+ "isAllDay": event.isAllDay
2516
+ ]
2517
+ }
2518
+
2519
+ self?.resolveCallback(callbackId, result: eventData)
2520
+ }
2521
+ }
2522
+
2523
+ private func createCalendarEvent(_ data: [String: Any], callbackId: String?) {
2524
+ eventStore?.requestAccess(to: .event) { [weak self] granted, error in
2525
+ guard granted else {
2526
+ self?.rejectCallback(callbackId, error: "Permission denied")
2527
+ return
2528
+ }
2529
+
2530
+ let event = EKEvent(eventStore: self!.eventStore!)
2531
+ event.title = data["title"] as? String ?? ""
2532
+ event.location = data["location"] as? String
2533
+ event.notes = data["notes"] as? String
2534
+
2535
+ if let start = data["startDate"] as? Double {
2536
+ event.startDate = Date(timeIntervalSince1970: start / 1000)
2537
+ }
2538
+ if let end = data["endDate"] as? Double {
2539
+ event.endDate = Date(timeIntervalSince1970: end / 1000)
2540
+ }
2541
+ event.isAllDay = data["isAllDay"] as? Bool ?? false
2542
+ event.calendar = self?.eventStore?.defaultCalendarForNewEvents
2543
+
2544
+ do {
2545
+ try self?.eventStore?.save(event, span: .thisEvent)
2546
+ self?.resolveCallback(callbackId, result: event.eventIdentifier)
2547
+ } catch {
2548
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2549
+ }
2550
+ }
2551
+ }
2552
+
2553
+ private func deleteCalendarEvent(_ eventId: String, callbackId: String?) {
2554
+ guard let event = eventStore?.event(withIdentifier: eventId) else {
2555
+ rejectCallback(callbackId, error: "Event not found")
2556
+ return
2557
+ }
2558
+
2559
+ do {
2560
+ try eventStore?.remove(event, span: .thisEvent)
2561
+ resolveCallback(callbackId, result: true)
2562
+ } catch {
2563
+ rejectCallback(callbackId, error: error.localizedDescription)
2564
+ }
2565
+ }
2566
+
2567
+ // MARK: - Local Notifications
2568
+ private func scheduleLocalNotification(_ data: [String: Any], callbackId: String?) {
2569
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
2570
+ guard granted else {
2571
+ self?.rejectCallback(callbackId, error: "Permission denied")
2572
+ return
2573
+ }
2574
+
2575
+ let content = UNMutableNotificationContent()
2576
+ content.title = data["title"] as? String ?? ""
2577
+ content.body = data["body"] as? String ?? ""
2578
+ if let subtitle = data["subtitle"] as? String { content.subtitle = subtitle }
2579
+ if let badge = data["badge"] as? Int { content.badge = NSNumber(value: badge) }
2580
+ content.sound = .default
2581
+
2582
+ let id = data["id"] as? String ?? UUID().uuidString
2583
+ var trigger: UNNotificationTrigger?
2584
+
2585
+ if let timestamp = data["timestamp"] as? Double {
2586
+ let date = Date(timeIntervalSince1970: timestamp / 1000)
2587
+ let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
2588
+ trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
2589
+ } else if let delay = data["delay"] as? Double {
2590
+ trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay / 1000, repeats: false)
2591
+ }
2592
+
2593
+ let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
2594
+ UNUserNotificationCenter.current().add(request) { error in
2595
+ if let error = error {
2596
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2597
+ } else {
2598
+ self?.resolveCallback(callbackId, result: id)
2599
+ }
2600
+ }
2601
+ }
2602
+ }
2603
+
2604
+ private func cancelLocalNotification(_ id: String, callbackId: String?) {
2605
+ UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
2606
+ resolveCallback(callbackId, result: true)
2607
+ }
2608
+
2609
+ private func cancelAllLocalNotifications(callbackId: String?) {
2610
+ UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
2611
+ resolveCallback(callbackId, result: true)
2612
+ }
2613
+
2614
+ private func getPendingNotifications(callbackId: String?) {
2615
+ UNUserNotificationCenter.current().getPendingNotificationRequests { [weak self] requests in
2616
+ let notifications: [[String: Any]] = requests.map { request in
2617
+ return [
2618
+ "id": request.identifier,
2619
+ "title": request.content.title,
2620
+ "body": request.content.body,
2621
+ "subtitle": request.content.subtitle
2622
+ ]
2623
+ }
2624
+ self?.resolveCallback(callbackId, result: notifications)
2625
+ }
2626
+ }
2627
+
2628
+ // MARK: - In-App Purchase
2629
+ private func getProducts(_ productIds: [String], callbackId: String?) {
2630
+ Task {
2631
+ do {
2632
+ let products = try await Product.products(for: Set(productIds))
2633
+ let productData: [[String: Any]] = products.map { product in
2634
+ return [
2635
+ "id": product.id,
2636
+ "displayName": product.displayName,
2637
+ "description": product.description,
2638
+ "price": product.price.description,
2639
+ "displayPrice": product.displayPrice
2640
+ ]
2641
+ }
2642
+ resolveCallback(callbackId, result: productData)
2643
+ } catch {
2644
+ rejectCallback(callbackId, error: error.localizedDescription)
2645
+ }
2646
+ }
2647
+ }
2648
+
2649
+ private func purchaseProduct(_ productId: String, callbackId: String?) {
2650
+ Task {
2651
+ do {
2652
+ let products = try await Product.products(for: [productId])
2653
+ guard let product = products.first else {
2654
+ rejectCallback(callbackId, error: "Product not found")
2655
+ return
2656
+ }
2657
+
2658
+ let result = try await product.purchase()
2659
+ switch result {
2660
+ case .success(let verification):
2661
+ switch verification {
2662
+ case .verified(let transaction):
2663
+ await transaction.finish()
2664
+ resolveCallback(callbackId, result: ["transactionId": String(transaction.id), "productId": transaction.productID])
2665
+ case .unverified(_, let error):
2666
+ rejectCallback(callbackId, error: error.localizedDescription)
2667
+ }
2668
+ case .userCancelled:
2669
+ rejectCallback(callbackId, error: "User cancelled")
2670
+ case .pending:
2671
+ rejectCallback(callbackId, error: "Purchase pending")
2672
+ @unknown default:
2673
+ rejectCallback(callbackId, error: "Unknown result")
2674
+ }
2675
+ } catch {
2676
+ rejectCallback(callbackId, error: error.localizedDescription)
2677
+ }
2678
+ }
2679
+ }
2680
+
2681
+ private func restorePurchases(callbackId: String?) {
2682
+ Task {
2683
+ do {
2684
+ try await AppStore.sync()
2685
+ var restored: [[String: Any]] = []
2686
+ for await result in Transaction.currentEntitlements {
2687
+ if case .verified(let transaction) = result {
2688
+ restored.append([
2689
+ "transactionId": String(transaction.id),
2690
+ "productId": transaction.productID
2691
+ ])
2692
+ }
2693
+ }
2694
+ resolveCallback(callbackId, result: restored)
2695
+ } catch {
2696
+ rejectCallback(callbackId, error: error.localizedDescription)
2697
+ }
2698
+ }
2699
+ }
2700
+
2701
+ // MARK: - Keep Awake
2702
+ private func setKeepAwake(_ enabled: Bool, callbackId: String?) {
2703
+ DispatchQueue.main.async {
2704
+ UIApplication.shared.isIdleTimerDisabled = enabled
2705
+ self.isKeepingAwake = enabled
2706
+ self.resolveCallback(callbackId, result: enabled)
2707
+ }
2708
+ }
2709
+
2710
+ // MARK: - Orientation Lock
2711
+ private func lockOrientation(_ orientation: String, callbackId: String?) {
2712
+ var mask: UIInterfaceOrientationMask = .all
2713
+ var uiOrientation: UIInterfaceOrientation = .unknown
2714
+
2715
+ switch orientation {
2716
+ case "portrait":
2717
+ mask = .portrait
2718
+ uiOrientation = .portrait
2719
+ case "portraitUpsideDown":
2720
+ mask = .portraitUpsideDown
2721
+ uiOrientation = .portraitUpsideDown
2722
+ case "landscapeLeft":
2723
+ mask = .landscapeLeft
2724
+ uiOrientation = .landscapeLeft
2725
+ case "landscapeRight":
2726
+ mask = .landscapeRight
2727
+ uiOrientation = .landscapeRight
2728
+ case "landscape":
2729
+ mask = .landscape
2730
+ uiOrientation = .landscapeLeft
2731
+ default:
2732
+ mask = .all
2733
+ }
2734
+
2735
+ lockedOrientation = mask
2736
+
2737
+ DispatchQueue.main.async {
2738
+ if #available(iOS 16.0, *) {
2739
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
2740
+ windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: mask))
2741
+ } else {
2742
+ UIDevice.current.setValue(uiOrientation.rawValue, forKey: "orientation")
2743
+ }
2744
+ self.resolveCallback(callbackId, result: true)
2745
+ }
2746
+ }
2747
+
2748
+ private func unlockOrientation(callbackId: String?) {
2749
+ lockedOrientation = nil
2750
+ DispatchQueue.main.async {
2751
+ if #available(iOS 16.0, *) {
2752
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
2753
+ windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .all))
2754
+ }
2755
+ self.resolveCallback(callbackId, result: true)
2756
+ }
2757
+ }
2758
+
2759
+ // MARK: - Deep Links
2760
+ func handleDeepLink(_ url: URL) {
2761
+ pendingDeepLink = url
2762
+ sendToWeb("craftDeepLink", data: [
2763
+ "url": url.absoluteString,
2764
+ "scheme": url.scheme ?? "",
2765
+ "host": url.host ?? "",
2766
+ "path": url.path,
2767
+ "query": url.query ?? ""
2768
+ ])
2769
+ }
2770
+
2771
+ // MARK: - QR/Barcode Scanner
2772
+ private func scanQRCode(callbackId: String?) {
2773
+ DispatchQueue.main.async {
2774
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2775
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2776
+
2777
+ if #available(iOS 16.0, *) {
2778
+ let scannerVC = DataScannerViewController(
2779
+ recognizedDataTypes: [.barcode()],
2780
+ qualityLevel: .balanced,
2781
+ isHighlightingEnabled: true
2782
+ )
2783
+ scannerVC.delegate = self
2784
+ self.pendingCallbackId = callbackId
2785
+ rootVC.present(scannerVC, animated: true) {
2786
+ try? scannerVC.startScanning()
2787
+ }
2788
+ } else {
2789
+ self.rejectCallback(callbackId, error: "QR scanning requires iOS 16+")
2790
+ }
2791
+ }
2792
+ }
2793
+
2794
+ // MARK: - File Picker
2795
+ private func pickFile(types: [String]?, callbackId: String?) {
2796
+ DispatchQueue.main.async {
2797
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2798
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2799
+
2800
+ var allowedTypes: [UTType] = [.item]
2801
+ if let types = types {
2802
+ allowedTypes = types.compactMap { UTType(mimeType: $0) ?? UTType(filenameExtension: $0) }
2803
+ }
2804
+
2805
+ let picker = UIDocumentPickerViewController(forOpeningContentTypes: allowedTypes)
2806
+ picker.delegate = self
2807
+ picker.allowsMultipleSelection = false
2808
+ self.pendingCallbackId = callbackId
2809
+ rootVC.present(picker, animated: true)
2810
+ }
2811
+ }
2812
+
2813
+ // MARK: - File Download
2814
+ private func downloadFile(url: String, filename: String, callbackId: String?) {
2815
+ guard let downloadURL = URL(string: url) else {
2816
+ rejectCallback(callbackId, error: "Invalid URL")
2817
+ return
2818
+ }
2819
+
2820
+ let task = URLSession.shared.downloadTask(with: downloadURL) { [weak self] localURL, response, error in
2821
+ if let error = error {
2822
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2823
+ return
2824
+ }
2825
+
2826
+ guard let localURL = localURL else {
2827
+ self?.rejectCallback(callbackId, error: "Download failed")
2828
+ return
2829
+ }
2830
+
2831
+ let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
2832
+ let destinationURL = documentsPath.appendingPathComponent(filename)
2833
+
2834
+ do {
2835
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
2836
+ try FileManager.default.removeItem(at: destinationURL)
2837
+ }
2838
+ try FileManager.default.moveItem(at: localURL, to: destinationURL)
2839
+ self?.resolveCallback(callbackId, result: destinationURL.path)
2840
+ } catch {
2841
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2842
+ }
2843
+ }
2844
+ task.resume()
2845
+ }
2846
+
2847
+ private func saveFile(data: String, filename: String, callbackId: String?) {
2848
+ let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
2849
+ let fileURL = documentsPath.appendingPathComponent(filename)
2850
+
2851
+ do {
2852
+ if data.hasPrefix("data:") {
2853
+ // Base64 data URL
2854
+ let parts = data.components(separatedBy: ",")
2855
+ if parts.count == 2, let fileData = Data(base64Encoded: parts[1]) {
2856
+ try fileData.write(to: fileURL)
2857
+ }
2858
+ } else {
2859
+ // Plain text
2860
+ try data.write(to: fileURL, atomically: true, encoding: .utf8)
2861
+ }
2862
+ resolveCallback(callbackId, result: fileURL.path)
2863
+ } catch {
2864
+ rejectCallback(callbackId, error: error.localizedDescription)
2865
+ }
2866
+ }
2867
+
2868
+ // MARK: - Social Auth (Apple Sign In)
2869
+ private func signInWithApple(callbackId: String?) {
2870
+ DispatchQueue.main.async {
2871
+ self.pendingCallbackId = callbackId
2872
+ let provider = ASAuthorizationAppleIDProvider()
2873
+ let request = provider.createRequest()
2874
+ request.requestedScopes = [.fullName, .email]
2875
+
2876
+ let controller = ASAuthorizationController(authorizationRequests: [request])
2877
+ controller.delegate = self
2878
+ controller.presentationContextProvider = self
2879
+ controller.performRequests()
2880
+ }
2881
+ }
2882
+
2883
+ // MARK: - Audio Recording
2884
+ private func startAudioRecording(callbackId: String?) {
2885
+ AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in
2886
+ guard granted else {
2887
+ self?.rejectCallback(callbackId, error: "Microphone permission denied")
2888
+ return
2889
+ }
2890
+
2891
+ DispatchQueue.main.async {
2892
+ let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
2893
+ let audioFilename = documentsPath.appendingPathComponent("recording_\(Date().timeIntervalSince1970).m4a")
2894
+ self?.recordingURL = audioFilename
2895
+
2896
+ let settings: [String: Any] = [
2897
+ AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
2898
+ AVSampleRateKey: 44100.0,
2899
+ AVNumberOfChannelsKey: 2,
2900
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
2901
+ ]
2902
+
2903
+ do {
2904
+ try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
2905
+ try AVAudioSession.sharedInstance().setActive(true)
2906
+
2907
+ self?.audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
2908
+ self?.audioRecorder?.record()
2909
+ self?.resolveCallback(callbackId, result: true)
2910
+ } catch {
2911
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
2912
+ }
2913
+ }
2914
+ }
2915
+ }
2916
+
2917
+ private func stopAudioRecording(callbackId: String?) {
2918
+ audioRecorder?.stop()
2919
+ audioRecorder = nil
2920
+
2921
+ if let url = recordingURL, FileManager.default.fileExists(atPath: url.path) {
2922
+ if let data = try? Data(contentsOf: url) {
2923
+ let base64 = "data:audio/m4a;base64," + data.base64EncodedString()
2924
+ resolveCallback(callbackId, result: base64)
2925
+ } else {
2926
+ resolveCallback(callbackId, result: url.path)
2927
+ }
2928
+ } else {
2929
+ rejectCallback(callbackId, error: "No recording found")
2930
+ }
2931
+ recordingURL = nil
2932
+ }
2933
+
2934
+ // MARK: - Video Recording
2935
+ private func startVideoRecording(callbackId: String?) {
2936
+ DispatchQueue.main.async {
2937
+ guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
2938
+ self.rejectCallback(callbackId, error: "Camera not available")
2939
+ return
2940
+ }
2941
+
2942
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
2943
+ let rootVC = windowScene.windows.first?.rootViewController else { return }
2944
+
2945
+ let picker = UIImagePickerController()
2946
+ picker.sourceType = .camera
2947
+ picker.mediaTypes = ["public.movie"]
2948
+ picker.videoQuality = .typeMedium
2949
+ picker.delegate = self
2950
+ self.pendingCallbackId = callbackId
2951
+ rootVC.present(picker, animated: true)
2952
+ }
2953
+ }
2954
+
2955
+ // MARK: - Motion Sensors
2956
+ private func startMotionUpdates(interval: Double, callbackId: String?) {
2957
+ guard let motionManager = motionManager, motionManager.isDeviceMotionAvailable else {
2958
+ rejectCallback(callbackId, error: "Motion sensors not available")
2959
+ return
2960
+ }
2961
+
2962
+ let updateInterval = interval / 1000.0 // Convert ms to seconds
2963
+ motionManager.deviceMotionUpdateInterval = updateInterval
2964
+
2965
+ motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
2966
+ guard let motion = motion else { return }
2967
+
2968
+ self?.sendToWeb("craftMotionUpdate", data: [
2969
+ "acceleration": [
2970
+ "x": motion.userAcceleration.x,
2971
+ "y": motion.userAcceleration.y,
2972
+ "z": motion.userAcceleration.z
2973
+ ],
2974
+ "rotation": [
2975
+ "alpha": motion.attitude.yaw,
2976
+ "beta": motion.attitude.pitch,
2977
+ "gamma": motion.attitude.roll
2978
+ ],
2979
+ "gravity": [
2980
+ "x": motion.gravity.x,
2981
+ "y": motion.gravity.y,
2982
+ "z": motion.gravity.z
2983
+ ]
2984
+ ])
2985
+ }
2986
+
2987
+ isMotionUpdating = true
2988
+ resolveCallback(callbackId, result: true)
2989
+ }
2990
+
2991
+ private func stopMotionUpdates() {
2992
+ motionManager?.stopDeviceMotionUpdates()
2993
+ isMotionUpdating = false
2994
+ }
2995
+
2996
+ // MARK: - Local Database (SQLite)
2997
+ private func dbExecute(sql: String, params: [Any]?, callbackId: String?) {
2998
+ guard let db = db else {
2999
+ rejectCallback(callbackId, error: "Database not initialized")
3000
+ return
3001
+ }
3002
+
3003
+ var statement: OpaquePointer?
3004
+ if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
3005
+ // Bind parameters
3006
+ if let params = params {
3007
+ for (index, param) in params.enumerated() {
3008
+ let idx = Int32(index + 1)
3009
+ if let str = param as? String {
3010
+ sqlite3_bind_text(statement, idx, str, -1, nil)
3011
+ } else if let int = param as? Int {
3012
+ sqlite3_bind_int(statement, idx, Int32(int))
3013
+ } else if let double = param as? Double {
3014
+ sqlite3_bind_double(statement, idx, double)
3015
+ } else {
3016
+ sqlite3_bind_null(statement, idx)
3017
+ }
3018
+ }
3019
+ }
3020
+
3021
+ if sqlite3_step(statement) == SQLITE_DONE {
3022
+ let rowsAffected = sqlite3_changes(db)
3023
+ let lastInsertId = sqlite3_last_insert_rowid(db)
3024
+ resolveCallback(callbackId, result: ["rowsAffected": rowsAffected, "lastInsertId": lastInsertId])
3025
+ } else {
3026
+ let error = String(cString: sqlite3_errmsg(db))
3027
+ rejectCallback(callbackId, error: error)
3028
+ }
3029
+ } else {
3030
+ let error = String(cString: sqlite3_errmsg(db))
3031
+ rejectCallback(callbackId, error: error)
3032
+ }
3033
+ sqlite3_finalize(statement)
3034
+ }
3035
+
3036
+ private func dbQuery(sql: String, params: [Any]?, callbackId: String?) {
3037
+ guard let db = db else {
3038
+ rejectCallback(callbackId, error: "Database not initialized")
3039
+ return
3040
+ }
3041
+
3042
+ var statement: OpaquePointer?
3043
+ var results: [[String: Any]] = []
3044
+
3045
+ if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
3046
+ // Bind parameters
3047
+ if let params = params {
3048
+ for (index, param) in params.enumerated() {
3049
+ let idx = Int32(index + 1)
3050
+ if let str = param as? String {
3051
+ sqlite3_bind_text(statement, idx, str, -1, nil)
3052
+ } else if let int = param as? Int {
3053
+ sqlite3_bind_int(statement, idx, Int32(int))
3054
+ } else if let double = param as? Double {
3055
+ sqlite3_bind_double(statement, idx, double)
3056
+ } else {
3057
+ sqlite3_bind_null(statement, idx)
3058
+ }
3059
+ }
3060
+ }
3061
+
3062
+ let columnCount = sqlite3_column_count(statement)
3063
+
3064
+ while sqlite3_step(statement) == SQLITE_ROW {
3065
+ var row: [String: Any] = [:]
3066
+ for i in 0..<columnCount {
3067
+ let columnName = String(cString: sqlite3_column_name(statement, i))
3068
+ let type = sqlite3_column_type(statement, i)
3069
+
3070
+ switch type {
3071
+ case SQLITE_INTEGER:
3072
+ row[columnName] = sqlite3_column_int(statement, i)
3073
+ case SQLITE_FLOAT:
3074
+ row[columnName] = sqlite3_column_double(statement, i)
3075
+ case SQLITE_TEXT:
3076
+ if let text = sqlite3_column_text(statement, i) {
3077
+ row[columnName] = String(cString: text)
3078
+ }
3079
+ case SQLITE_NULL:
3080
+ row[columnName] = NSNull()
3081
+ default:
3082
+ break
3083
+ }
3084
+ }
3085
+ results.append(row)
3086
+ }
3087
+
3088
+ resolveCallback(callbackId, result: results)
3089
+ } else {
3090
+ let error = String(cString: sqlite3_errmsg(db))
3091
+ rejectCallback(callbackId, error: error)
3092
+ }
3093
+ sqlite3_finalize(statement)
3094
+ }
3095
+
3096
+ // MARK: - Bluetooth
3097
+ private func startBluetoothScan(callbackId: String?) {
3098
+ centralManager = CBCentralManager(delegate: self, queue: nil)
3099
+ pendingCallbackId = callbackId
3100
+ // Scanning starts in centralManagerDidUpdateState
3101
+ }
3102
+
3103
+ private func stopBluetoothScan() {
3104
+ centralManager?.stopScan()
3105
+ centralManager = nil
3106
+ discoveredPeripherals.removeAll()
3107
+ }
3108
+
3109
+ // MARK: - NFC
3110
+ private func scanNFC(callbackId: String?) {
3111
+ guard NFCNDEFReaderSession.readingAvailable else {
3112
+ rejectCallback(callbackId, error: "NFC not available")
3113
+ return
3114
+ }
3115
+
3116
+ pendingCallbackId = callbackId
3117
+ let session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
3118
+ session.alertMessage = "Hold your iPhone near the NFC tag"
3119
+ session.begin()
3120
+ }
3121
+
3122
+ // MARK: - Health
3123
+ private func requestHealthAuthorization(types: [String], callbackId: String?) {
3124
+ guard let healthStore = healthStore else {
3125
+ rejectCallback(callbackId, error: "HealthKit not available")
3126
+ return
3127
+ }
3128
+
3129
+ var readTypes: Set<HKObjectType> = []
3130
+
3131
+ for type in types {
3132
+ switch type {
3133
+ case "steps":
3134
+ if let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) {
3135
+ readTypes.insert(stepType)
3136
+ }
3137
+ case "heartRate":
3138
+ if let heartType = HKQuantityType.quantityType(forIdentifier: .heartRate) {
3139
+ readTypes.insert(heartType)
3140
+ }
3141
+ case "activeEnergy":
3142
+ if let energyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
3143
+ readTypes.insert(energyType)
3144
+ }
3145
+ case "distance":
3146
+ if let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning) {
3147
+ readTypes.insert(distanceType)
3148
+ }
3149
+ default:
3150
+ break
3151
+ }
3152
+ }
3153
+
3154
+ healthStore.requestAuthorization(toShare: nil, read: readTypes) { [weak self] success, error in
3155
+ if success {
3156
+ self?.resolveCallback(callbackId, result: true)
3157
+ } else {
3158
+ self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Authorization failed")
3159
+ }
3160
+ }
3161
+ }
3162
+
3163
+ private func getHealthData(type: String, startDate: Double?, endDate: Double?, callbackId: String?) {
3164
+ guard let healthStore = healthStore else {
3165
+ rejectCallback(callbackId, error: "HealthKit not available")
3166
+ return
3167
+ }
3168
+
3169
+ var quantityType: HKQuantityType?
3170
+ var unit: HKUnit?
3171
+
3172
+ switch type {
3173
+ case "steps":
3174
+ quantityType = HKQuantityType.quantityType(forIdentifier: .stepCount)
3175
+ unit = .count()
3176
+ case "heartRate":
3177
+ quantityType = HKQuantityType.quantityType(forIdentifier: .heartRate)
3178
+ unit = HKUnit(from: "count/min")
3179
+ case "activeEnergy":
3180
+ quantityType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)
3181
+ unit = .kilocalorie()
3182
+ case "distance":
3183
+ quantityType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)
3184
+ unit = .meter()
3185
+ default:
3186
+ rejectCallback(callbackId, error: "Unknown health data type")
3187
+ return
3188
+ }
3189
+
3190
+ guard let qType = quantityType, let qUnit = unit else {
3191
+ rejectCallback(callbackId, error: "Invalid health data type")
3192
+ return
3193
+ }
3194
+
3195
+ let start = startDate != nil ? Date(timeIntervalSince1970: startDate! / 1000) : Calendar.current.date(byAdding: .day, value: -7, to: Date())!
3196
+ let end = endDate != nil ? Date(timeIntervalSince1970: endDate! / 1000) : Date()
3197
+
3198
+ let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
3199
+
3200
+ let query = HKStatisticsQuery(quantityType: qType, quantitySamplePredicate: predicate, options: .cumulativeSum) { [weak self] _, result, error in
3201
+ if let error = error {
3202
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
3203
+ return
3204
+ }
3205
+
3206
+ let value = result?.sumQuantity()?.doubleValue(for: qUnit) ?? 0
3207
+ self?.resolveCallback(callbackId, result: ["value": value, "unit": qUnit.unitString])
3208
+ }
3209
+
3210
+ healthStore.execute(query)
3211
+ }
3212
+
3213
+ // MARK: - Screen Capture
3214
+ private func takeScreenshot(callbackId: String?) {
3215
+ DispatchQueue.main.async {
3216
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
3217
+ let window = windowScene.windows.first else {
3218
+ self.rejectCallback(callbackId, error: "No window available")
3219
+ return
3220
+ }
3221
+
3222
+ let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
3223
+ let image = renderer.image { context in
3224
+ window.layer.render(in: context.cgContext)
3225
+ }
3226
+
3227
+ if let imageData = image.pngData() {
3228
+ let base64 = "data:image/png;base64," + imageData.base64EncodedString()
3229
+ self.resolveCallback(callbackId, result: base64)
3230
+ } else {
3231
+ self.rejectCallback(callbackId, error: "Failed to capture screenshot")
3232
+ }
3233
+ }
3234
+ }
3235
+
3236
+ // MARK: - Background Tasks
3237
+ private var registeredBackgroundTasks: Set<String> = []
3238
+
3239
+ private func registerBackgroundTask(taskId: String, callbackId: String?) {
3240
+ let fullTaskId = "\(Bundle.main.bundleIdentifier ?? "com.craft.app").\(taskId)"
3241
+ registeredBackgroundTasks.insert(fullTaskId)
3242
+ resolveCallback(callbackId, result: ["taskId": fullTaskId, "registered": true])
3243
+ }
3244
+
3245
+ private func scheduleBackgroundTask(taskId: String, delay: Double, requiresNetwork: Bool, requiresCharging: Bool, callbackId: String?) {
3246
+ let fullTaskId = "\(Bundle.main.bundleIdentifier ?? "com.craft.app").\(taskId)"
3247
+
3248
+ // Use BGAppRefreshTaskRequest for short tasks (default)
3249
+ let request = BGAppRefreshTaskRequest(identifier: fullTaskId)
3250
+ request.earliestBeginDate = Date(timeIntervalSinceNow: delay)
3251
+
3252
+ do {
3253
+ try BGTaskScheduler.shared.submit(request)
3254
+ resolveCallback(callbackId, result: ["taskId": fullTaskId, "scheduled": true])
3255
+ } catch {
3256
+ rejectCallback(callbackId, error: error.localizedDescription)
3257
+ }
3258
+ }
3259
+
3260
+ private func cancelBackgroundTask(taskId: String, callbackId: String?) {
3261
+ let fullTaskId = "\(Bundle.main.bundleIdentifier ?? "com.craft.app").\(taskId)"
3262
+ BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: fullTaskId)
3263
+ resolveCallback(callbackId, result: ["taskId": fullTaskId, "cancelled": true])
3264
+ }
3265
+
3266
+ private func cancelAllBackgroundTasks(callbackId: String?) {
3267
+ BGTaskScheduler.shared.cancelAllTaskRequests()
3268
+ resolveCallback(callbackId, result: ["cancelled": true])
3269
+ }
3270
+
3271
+ // MARK: - PDF Viewer
3272
+ private var pdfViewController: UIViewController?
3273
+
3274
+ private func openPDF(source: String, page: Int, callbackId: String?) {
3275
+ DispatchQueue.main.async {
3276
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
3277
+ let rootVC = windowScene.windows.first?.rootViewController else {
3278
+ self.rejectCallback(callbackId, error: "No root view controller")
3279
+ return
3280
+ }
3281
+
3282
+ var pdfDocument: PDFDocument?
3283
+
3284
+ // Check if source is a URL or base64 data
3285
+ if source.hasPrefix("data:") {
3286
+ // Base64 encoded PDF
3287
+ let base64String = source.replacingOccurrences(of: "data:application/pdf;base64,", with: "")
3288
+ if let data = Data(base64Encoded: base64String) {
3289
+ pdfDocument = PDFDocument(data: data)
3290
+ }
3291
+ } else if let url = URL(string: source) {
3292
+ // URL - could be remote or local
3293
+ pdfDocument = PDFDocument(url: url)
3294
+ }
3295
+
3296
+ guard let document = pdfDocument else {
3297
+ self.rejectCallback(callbackId, error: "Failed to load PDF")
3298
+ return
3299
+ }
3300
+
3301
+ let pdfView = PDFView(frame: .zero)
3302
+ pdfView.document = document
3303
+ pdfView.autoScales = true
3304
+ pdfView.displayMode = .singlePageContinuous
3305
+ pdfView.displayDirection = .vertical
3306
+
3307
+ // Go to specific page if requested
3308
+ if page > 0, let targetPage = document.page(at: page) {
3309
+ pdfView.go(to: targetPage)
3310
+ }
3311
+
3312
+ let vc = UIViewController()
3313
+ vc.view = pdfView
3314
+ vc.view.backgroundColor = .systemBackground
3315
+ vc.modalPresentationStyle = .fullScreen
3316
+
3317
+ // Add close button
3318
+ let closeButton = UIButton(type: .system)
3319
+ closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
3320
+ closeButton.tintColor = .systemGray
3321
+ closeButton.addTarget(self, action: #selector(self.dismissPDF), for: .touchUpInside)
3322
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
3323
+ vc.view.addSubview(closeButton)
3324
+
3325
+ NSLayoutConstraint.activate([
3326
+ closeButton.topAnchor.constraint(equalTo: vc.view.safeAreaLayoutGuide.topAnchor, constant: 16),
3327
+ closeButton.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor, constant: -16),
3328
+ closeButton.widthAnchor.constraint(equalToConstant: 32),
3329
+ closeButton.heightAnchor.constraint(equalToConstant: 32)
3330
+ ])
3331
+
3332
+ self.pdfViewController = vc
3333
+ rootVC.present(vc, animated: true)
3334
+ self.resolveCallback(callbackId, result: ["opened": true, "pageCount": document.pageCount])
3335
+ }
3336
+ }
3337
+
3338
+ @objc private func dismissPDF() {
3339
+ pdfViewController?.dismiss(animated: true)
3340
+ pdfViewController = nil
3341
+ }
3342
+
3343
+ private func closePDF(callbackId: String?) {
3344
+ DispatchQueue.main.async {
3345
+ self.pdfViewController?.dismiss(animated: true)
3346
+ self.pdfViewController = nil
3347
+ self.resolveCallback(callbackId, result: true)
3348
+ }
3349
+ }
3350
+
3351
+ // MARK: - Contacts Picker
3352
+ private func pickContact(multiple: Bool, callbackId: String?) {
3353
+ DispatchQueue.main.async {
3354
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
3355
+ let rootVC = windowScene.windows.first?.rootViewController else {
3356
+ self.rejectCallback(callbackId, error: "No root view controller")
3357
+ return
3358
+ }
3359
+
3360
+ self.pendingCallbackId = callbackId
3361
+
3362
+ let picker = CNContactPickerViewController()
3363
+ picker.delegate = self
3364
+ picker.predicateForEnablingContact = NSPredicate(value: true)
3365
+
3366
+ if multiple {
3367
+ picker.predicateForSelectionOfContact = nil
3368
+ } else {
3369
+ picker.predicateForSelectionOfContact = NSPredicate(value: true)
3370
+ }
3371
+
3372
+ rootVC.present(picker, animated: true)
3373
+ }
3374
+ }
3375
+
3376
+ // MARK: - App Shortcuts
3377
+ private func setAppShortcuts(shortcuts: [[String: Any]], callbackId: String?) {
3378
+ var shortcutItems: [UIApplicationShortcutItem] = []
3379
+
3380
+ for shortcut in shortcuts {
3381
+ guard let type = shortcut["type"] as? String,
3382
+ let title = shortcut["title"] as? String else { continue }
3383
+
3384
+ let subtitle = shortcut["subtitle"] as? String
3385
+ let iconName = shortcut["iconName"] as? String
3386
+
3387
+ var icon: UIApplicationShortcutIcon?
3388
+ if let name = iconName {
3389
+ icon = UIApplicationShortcutIcon(systemImageName: name)
3390
+ }
3391
+
3392
+ let item = UIApplicationShortcutItem(
3393
+ type: type,
3394
+ localizedTitle: title,
3395
+ localizedSubtitle: subtitle,
3396
+ icon: icon,
3397
+ userInfo: shortcut["userInfo"] as? [String: NSSecureCoding]
3398
+ )
3399
+ shortcutItems.append(item)
3400
+ }
3401
+
3402
+ DispatchQueue.main.async {
3403
+ UIApplication.shared.shortcutItems = shortcutItems
3404
+ self.resolveCallback(callbackId, result: ["count": shortcutItems.count])
3405
+ }
3406
+ }
3407
+
3408
+ private func clearAppShortcuts(callbackId: String?) {
3409
+ DispatchQueue.main.async {
3410
+ UIApplication.shared.shortcutItems = nil
3411
+ self.resolveCallback(callbackId, result: true)
3412
+ }
3413
+ }
3414
+
3415
+ // MARK: - Keychain Sharing
3416
+ private func setSharedKeychainItem(key: String, value: String, group: String?, callbackId: String?) {
3417
+ let data = value.data(using: .utf8)!
3418
+
3419
+ var query: [String: Any] = [
3420
+ kSecClass as String: kSecClassGenericPassword,
3421
+ kSecAttrAccount as String: key,
3422
+ kSecValueData as String: data,
3423
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
3424
+ ]
3425
+
3426
+ // Add access group if specified (requires Keychain Sharing entitlement)
3427
+ if let accessGroup = group {
3428
+ query[kSecAttrAccessGroup as String] = accessGroup
3429
+ }
3430
+
3431
+ // Delete existing item first
3432
+ SecItemDelete(query as CFDictionary)
3433
+
3434
+ let status = SecItemAdd(query as CFDictionary, nil)
3435
+ if status == errSecSuccess {
3436
+ resolveCallback(callbackId, result: ["success": true, "key": key])
3437
+ } else {
3438
+ rejectCallback(callbackId, error: "Keychain error: \(status)")
3439
+ }
3440
+ }
3441
+
3442
+ private func getSharedKeychainItem(key: String, group: String?, callbackId: String?) {
3443
+ var query: [String: Any] = [
3444
+ kSecClass as String: kSecClassGenericPassword,
3445
+ kSecAttrAccount as String: key,
3446
+ kSecReturnData as String: true,
3447
+ kSecMatchLimit as String: kSecMatchLimitOne
3448
+ ]
3449
+
3450
+ if let accessGroup = group {
3451
+ query[kSecAttrAccessGroup as String] = accessGroup
3452
+ }
3453
+
3454
+ var result: AnyObject?
3455
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
3456
+
3457
+ if status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) {
3458
+ resolveCallback(callbackId, result: ["value": value, "key": key])
3459
+ } else if status == errSecItemNotFound {
3460
+ resolveCallback(callbackId, result: ["value": NSNull(), "key": key])
3461
+ } else {
3462
+ rejectCallback(callbackId, error: "Keychain error: \(status)")
3463
+ }
3464
+ }
3465
+
3466
+ private func removeSharedKeychainItem(key: String, group: String?, callbackId: String?) {
3467
+ var query: [String: Any] = [
3468
+ kSecClass as String: kSecClassGenericPassword,
3469
+ kSecAttrAccount as String: key
3470
+ ]
3471
+
3472
+ if let accessGroup = group {
3473
+ query[kSecAttrAccessGroup as String] = accessGroup
3474
+ }
3475
+
3476
+ let status = SecItemDelete(query as CFDictionary)
3477
+ if status == errSecSuccess || status == errSecItemNotFound {
3478
+ resolveCallback(callbackId, result: ["success": true, "key": key])
3479
+ } else {
3480
+ rejectCallback(callbackId, error: "Keychain error: \(status)")
3481
+ }
3482
+ }
3483
+
3484
+ // MARK: - Local Auth Persistence
3485
+ private var biometricSessionExpiry: Date?
3486
+ private var biometricSessionDuration: TimeInterval = 300 // 5 minutes default
3487
+
3488
+ private func setBiometricPersistence(enabled: Bool, duration: Double, callbackId: String?) {
3489
+ if enabled {
3490
+ biometricSessionDuration = duration
3491
+ biometricSessionExpiry = Date(timeIntervalSinceNow: duration)
3492
+ resolveCallback(callbackId, result: ["enabled": true, "duration": duration, "expiresAt": biometricSessionExpiry!.timeIntervalSince1970 * 1000])
3493
+ } else {
3494
+ biometricSessionExpiry = nil
3495
+ resolveCallback(callbackId, result: ["enabled": false])
3496
+ }
3497
+ }
3498
+
3499
+ private func checkBiometricPersistence(callbackId: String?) {
3500
+ if let expiry = biometricSessionExpiry {
3501
+ let isValid = Date() < expiry
3502
+ let remaining = isValid ? expiry.timeIntervalSince(Date()) : 0
3503
+ resolveCallback(callbackId, result: ["isValid": isValid, "remainingSeconds": remaining])
3504
+ } else {
3505
+ resolveCallback(callbackId, result: ["isValid": false, "remainingSeconds": 0])
3506
+ }
3507
+ }
3508
+
3509
+ private func clearBiometricPersistence(callbackId: String?) {
3510
+ biometricSessionExpiry = nil
3511
+ resolveCallback(callbackId, result: ["cleared": true])
3512
+ }
3513
+
3514
+ // MARK: - AR (ARKit)
3515
+ private var arSession: ARSession?
3516
+ private var arView: ARSCNView?
3517
+ private var arObjects: [String: SCNNode] = [:]
3518
+ private var detectedPlanes: [UUID: ARPlaneAnchor] = [:]
3519
+
3520
+ private func startAR(options: [String: Any], callbackId: String?) {
3521
+ guard ARWorldTrackingConfiguration.isSupported else {
3522
+ rejectCallback(callbackId, error: "AR not supported on this device")
3523
+ return
3524
+ }
3525
+
3526
+ DispatchQueue.main.async { [weak self] in
3527
+ guard let self = self else { return }
3528
+
3529
+ // Create AR view
3530
+ let arView = ARSCNView(frame: UIScreen.main.bounds)
3531
+ arView.delegate = self
3532
+ arView.autoenablesDefaultLighting = true
3533
+ arView.tag = 9999 // For removal later
3534
+
3535
+ // Configure AR session
3536
+ let configuration = ARWorldTrackingConfiguration()
3537
+ configuration.planeDetection = [.horizontal, .vertical]
3538
+ configuration.environmentTexturing = .automatic
3539
+
3540
+ self.arView = arView
3541
+ self.arSession = arView.session
3542
+
3543
+ // Add AR view to window
3544
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
3545
+ let window = windowScene.windows.first {
3546
+ window.addSubview(arView)
3547
+ }
3548
+
3549
+ arView.session.run(configuration)
3550
+ self.resolveCallback(callbackId, result: ["started": true])
3551
+ }
3552
+ }
3553
+
3554
+ private func stopAR(callbackId: String?) {
3555
+ DispatchQueue.main.async { [weak self] in
3556
+ guard let self = self else { return }
3557
+
3558
+ self.arSession?.pause()
3559
+ self.arView?.removeFromSuperview()
3560
+ self.arView = nil
3561
+ self.arSession = nil
3562
+ self.arObjects.removeAll()
3563
+ self.detectedPlanes.removeAll()
3564
+
3565
+ self.resolveCallback(callbackId, result: ["stopped": true])
3566
+ }
3567
+ }
3568
+
3569
+ private func placeARObject(model: String, position: [String: Double]?, callbackId: String?) {
3570
+ DispatchQueue.main.async { [weak self] in
3571
+ guard let self = self, let arView = self.arView else {
3572
+ self?.rejectCallback(callbackId, error: "AR not started")
3573
+ return
3574
+ }
3575
+
3576
+ let objectId = UUID().uuidString
3577
+
3578
+ // Create a simple box if no model URL
3579
+ let node: SCNNode
3580
+ if model.hasSuffix(".usdz") || model.hasSuffix(".scn") {
3581
+ // Load from URL/bundle
3582
+ if let url = URL(string: model) {
3583
+ do {
3584
+ let scene = try SCNScene(url: url, options: nil)
3585
+ node = SCNNode()
3586
+ for child in scene.rootNode.childNodes {
3587
+ node.addChildNode(child)
3588
+ }
3589
+ } catch {
3590
+ self.rejectCallback(callbackId, error: "Failed to load model: \\(error.localizedDescription)")
3591
+ return
3592
+ }
3593
+ } else {
3594
+ self.rejectCallback(callbackId, error: "Invalid model URL")
3595
+ return
3596
+ }
3597
+ } else {
3598
+ // Create a simple geometry based on model name
3599
+ let geometry: SCNGeometry
3600
+ switch model {
3601
+ case "box":
3602
+ geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
3603
+ case "sphere":
3604
+ geometry = SCNSphere(radius: 0.05)
3605
+ case "cylinder":
3606
+ geometry = SCNCylinder(radius: 0.05, height: 0.1)
3607
+ case "cone":
3608
+ geometry = SCNCone(topRadius: 0, bottomRadius: 0.05, height: 0.1)
3609
+ default:
3610
+ geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.01)
3611
+ }
3612
+ geometry.firstMaterial?.diffuse.contents = UIColor.systemBlue
3613
+ node = SCNNode(geometry: geometry)
3614
+ }
3615
+
3616
+ // Set position
3617
+ if let pos = position {
3618
+ node.position = SCNVector3(
3619
+ Float(pos["x"] ?? 0),
3620
+ Float(pos["y"] ?? 0),
3621
+ Float(pos["z"] ?? -0.5)
3622
+ )
3623
+ } else {
3624
+ node.position = SCNVector3(0, 0, -0.5)
3625
+ }
3626
+
3627
+ node.name = objectId
3628
+ arView.scene.rootNode.addChildNode(node)
3629
+ self.arObjects[objectId] = node
3630
+
3631
+ self.resolveCallback(callbackId, result: ["objectId": objectId, "placed": true])
3632
+ }
3633
+ }
3634
+
3635
+ private func removeARObject(objectId: String, callbackId: String?) {
3636
+ DispatchQueue.main.async { [weak self] in
3637
+ guard let self = self else { return }
3638
+
3639
+ if let node = self.arObjects[objectId] {
3640
+ node.removeFromParentNode()
3641
+ self.arObjects.removeValue(forKey: objectId)
3642
+ self.resolveCallback(callbackId, result: ["removed": true])
3643
+ } else {
3644
+ self.rejectCallback(callbackId, error: "Object not found")
3645
+ }
3646
+ }
3647
+ }
3648
+
3649
+ private func getARPlanes(callbackId: String?) {
3650
+ var planes: [[String: Any]] = []
3651
+ for (_, anchor) in detectedPlanes {
3652
+ planes.append([
3653
+ "id": anchor.identifier.uuidString,
3654
+ "alignment": anchor.alignment == .horizontal ? "horizontal" : "vertical",
3655
+ "center": ["x": anchor.center.x, "y": anchor.center.y, "z": anchor.center.z],
3656
+ "extent": ["width": anchor.extent.x, "height": anchor.extent.z]
3657
+ ])
3658
+ }
3659
+ resolveCallback(callbackId, result: planes)
3660
+ }
3661
+
3662
+ // MARK: - ML (Core ML / Vision)
3663
+ private func classifyImage(imageBase64: String, callbackId: String?) {
3664
+ guard let imageData = Data(base64Encoded: imageBase64),
3665
+ let image = UIImage(data: imageData),
3666
+ let cgImage = image.cgImage else {
3667
+ rejectCallback(callbackId, error: "Invalid image data")
3668
+ return
3669
+ }
3670
+
3671
+ // Use Vision for image classification
3672
+ let request = VNClassifyImageRequest { [weak self] request, error in
3673
+ if let error = error {
3674
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
3675
+ return
3676
+ }
3677
+
3678
+ guard let observations = request.results as? [VNClassificationObservation] else {
3679
+ self?.rejectCallback(callbackId, error: "No classification results")
3680
+ return
3681
+ }
3682
+
3683
+ let classifications = observations.prefix(10).map { obs in
3684
+ ["label": obs.identifier, "confidence": obs.confidence]
3685
+ }
3686
+ self?.resolveCallback(callbackId, result: classifications)
3687
+ }
3688
+
3689
+ let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
3690
+ DispatchQueue.global(qos: .userInitiated).async {
3691
+ do {
3692
+ try handler.perform([request])
3693
+ } catch {
3694
+ self.rejectCallback(callbackId, error: error.localizedDescription)
3695
+ }
3696
+ }
3697
+ }
3698
+
3699
+ private func detectObjects(imageBase64: String, callbackId: String?) {
3700
+ guard let imageData = Data(base64Encoded: imageBase64),
3701
+ let image = UIImage(data: imageData),
3702
+ let cgImage = image.cgImage else {
3703
+ rejectCallback(callbackId, error: "Invalid image data")
3704
+ return
3705
+ }
3706
+
3707
+ // Use Vision for object detection
3708
+ let request = VNRecognizeAnimalsRequest { [weak self] request, error in
3709
+ if let error = error {
3710
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
3711
+ return
3712
+ }
3713
+
3714
+ guard let observations = request.results as? [VNRecognizedObjectObservation] else {
3715
+ self?.rejectCallback(callbackId, error: "No detection results")
3716
+ return
3717
+ }
3718
+
3719
+ let detections = observations.map { obs in
3720
+ [
3721
+ "labels": obs.labels.map { ["label": $0.identifier, "confidence": $0.confidence] },
3722
+ "boundingBox": [
3723
+ "x": obs.boundingBox.origin.x,
3724
+ "y": obs.boundingBox.origin.y,
3725
+ "width": obs.boundingBox.width,
3726
+ "height": obs.boundingBox.height
3727
+ ],
3728
+ "confidence": obs.confidence
3729
+ ] as [String : Any]
3730
+ }
3731
+ self?.resolveCallback(callbackId, result: detections)
3732
+ }
3733
+
3734
+ let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
3735
+ DispatchQueue.global(qos: .userInitiated).async {
3736
+ do {
3737
+ try handler.perform([request])
3738
+ } catch {
3739
+ self.rejectCallback(callbackId, error: error.localizedDescription)
3740
+ }
3741
+ }
3742
+ }
3743
+
3744
+ private func recognizeText(imageBase64: String, callbackId: String?) {
3745
+ guard let imageData = Data(base64Encoded: imageBase64),
3746
+ let image = UIImage(data: imageData),
3747
+ let cgImage = image.cgImage else {
3748
+ rejectCallback(callbackId, error: "Invalid image data")
3749
+ return
3750
+ }
3751
+
3752
+ let request = VNRecognizeTextRequest { [weak self] request, error in
3753
+ if let error = error {
3754
+ self?.rejectCallback(callbackId, error: error.localizedDescription)
3755
+ return
3756
+ }
3757
+
3758
+ guard let observations = request.results as? [VNRecognizedTextObservation] else {
3759
+ self?.rejectCallback(callbackId, error: "No text results")
3760
+ return
3761
+ }
3762
+
3763
+ var textResults: [[String: Any]] = []
3764
+ for observation in observations {
3765
+ if let candidate = observation.topCandidates(1).first {
3766
+ textResults.append([
3767
+ "text": candidate.string,
3768
+ "confidence": candidate.confidence,
3769
+ "boundingBox": [
3770
+ "x": observation.boundingBox.origin.x,
3771
+ "y": observation.boundingBox.origin.y,
3772
+ "width": observation.boundingBox.width,
3773
+ "height": observation.boundingBox.height
3774
+ ]
3775
+ ])
3776
+ }
3777
+ }
3778
+ self?.resolveCallback(callbackId, result: textResults)
3779
+ }
3780
+ request.recognitionLevel = .accurate
3781
+
3782
+ let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
3783
+ DispatchQueue.global(qos: .userInitiated).async {
3784
+ do {
3785
+ try handler.perform([request])
3786
+ } catch {
3787
+ self.rejectCallback(callbackId, error: error.localizedDescription)
3788
+ }
3789
+ }
3790
+ }
3791
+
3792
+ // MARK: - Widget
3793
+ private let widgetDefaults = UserDefaults(suiteName: "group.{{BUNDLE_ID}}.widget")
3794
+
3795
+ private func updateWidget(data: [String: Any], callbackId: String?) {
3796
+ if let title = data["title"] as? String {
3797
+ widgetDefaults?.set(title, forKey: "widget_title")
3798
+ }
3799
+ if let subtitle = data["subtitle"] as? String {
3800
+ widgetDefaults?.set(subtitle, forKey: "widget_subtitle")
3801
+ }
3802
+ if let value = data["value"] as? String {
3803
+ widgetDefaults?.set(value, forKey: "widget_value")
3804
+ }
3805
+ if let icon = data["icon"] as? String {
3806
+ widgetDefaults?.set(icon, forKey: "widget_icon")
3807
+ }
3808
+
3809
+ // Reload widgets
3810
+ WidgetCenter.shared.reloadAllTimelines()
3811
+ resolveCallback(callbackId, result: ["updated": true])
3812
+ }
3813
+
3814
+ private func reloadAllWidgets(callbackId: String?) {
3815
+ WidgetCenter.shared.reloadAllTimelines()
3816
+ resolveCallback(callbackId, result: ["reloaded": true])
3817
+ }
3818
+
3819
+ // MARK: - Siri Shortcuts
3820
+ private func registerSiriShortcut(phrase: String, action: String, callbackId: String?) {
3821
+ let activity = NSUserActivity(activityType: "{{BUNDLE_ID}}.\(action)")
3822
+ activity.title = phrase
3823
+ activity.isEligibleForSearch = true
3824
+ activity.isEligibleForPrediction = true
3825
+ activity.persistentIdentifier = NSUserActivityPersistentIdentifier(action)
3826
+ activity.suggestedInvocationPhrase = phrase
3827
+
3828
+ activity.userInfo = ["action": action]
3829
+
3830
+ // Donate the shortcut
3831
+ activity.becomeCurrent()
3832
+
3833
+ resolveCallback(callbackId, result: ["registered": true, "action": action, "phrase": phrase])
3834
+ }
3835
+
3836
+ private func removeSiriShortcut(action: String, callbackId: String?) {
3837
+ NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: [action]) {
3838
+ self.resolveCallback(callbackId, result: ["removed": true, "action": action])
3839
+ }
3840
+ }
3841
+
3842
+ // MARK: - Watch Connectivity
3843
+ private var wcSession: WCSession?
3844
+
3845
+ private func setupWatchConnectivity() {
3846
+ if WCSession.isSupported() {
3847
+ wcSession = WCSession.default
3848
+ wcSession?.delegate = self
3849
+ wcSession?.activate()
3850
+ }
3851
+ }
3852
+
3853
+ private func sendMessageToWatch(message: [String: Any], callbackId: String?) {
3854
+ guard let session = wcSession, session.isReachable else {
3855
+ rejectCallback(callbackId, error: "Watch not reachable")
3856
+ return
3857
+ }
3858
+
3859
+ session.sendMessage(message, replyHandler: { reply in
3860
+ self.resolveCallback(callbackId, result: reply)
3861
+ }, errorHandler: { error in
3862
+ self.rejectCallback(callbackId, error: error.localizedDescription)
3863
+ })
3864
+ }
3865
+
3866
+ private func updateWatchContext(context: [String: Any], callbackId: String?) {
3867
+ guard let session = wcSession else {
3868
+ rejectCallback(callbackId, error: "Watch session not available")
3869
+ return
3870
+ }
3871
+
3872
+ do {
3873
+ try session.updateApplicationContext(context)
3874
+ resolveCallback(callbackId, result: ["updated": true])
3875
+ } catch {
3876
+ rejectCallback(callbackId, error: error.localizedDescription)
3877
+ }
3878
+ }
3879
+
3880
+ private func isWatchReachable(callbackId: String?) {
3881
+ let reachable = wcSession?.isReachable ?? false
3882
+ resolveCallback(callbackId, result: ["reachable": reachable])
3883
+ }
3884
+
3885
+ // MARK: - Web Communication
3886
+ private func sendToWeb(_ event: String, data: [String: Any]) {
3887
+ guard let webView = webView else { return }
3888
+ do {
3889
+ let jsonData = try JSONSerialization.data(withJSONObject: data)
3890
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
3891
+ let script = "window.dispatchEvent(new CustomEvent('\(event)', {detail: \(jsonString)}));"
3892
+ DispatchQueue.main.async { webView.evaluateJavaScript(script, completionHandler: nil) }
3893
+ }
3894
+ } catch {
3895
+ print("Failed to serialize: \(error)")
3896
+ }
3897
+ }
3898
+ }
3899
+ }
3900
+
3901
+ // MARK: - UIColor Extension
3902
+ extension UIColor {
3903
+ convenience init?(hex: String) {
3904
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
3905
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
3906
+
3907
+ var rgb: UInt64 = 0
3908
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
3909
+
3910
+ let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
3911
+ let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
3912
+ let b = CGFloat(rgb & 0x0000FF) / 255.0
3913
+
3914
+ self.init(red: r, green: g, blue: b, alpha: 1.0)
3915
+ }
3916
+ }
3917
+
3918
+ // MARK: - DataScanner Delegate (QR/Barcode)
3919
+ @available(iOS 16.0, *)
3920
+ extension CraftWebView.Coordinator: DataScannerViewControllerDelegate {
3921
+ func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
3922
+ switch item {
3923
+ case .barcode(let barcode):
3924
+ dataScanner.dismiss(animated: true)
3925
+ resolveCallback(pendingCallbackId, result: [
3926
+ "type": barcode.observation.symbology.rawValue,
3927
+ "data": barcode.payloadStringValue ?? ""
3928
+ ])
3929
+ pendingCallbackId = nil
3930
+ default:
3931
+ break
3932
+ }
3933
+ }
3934
+ }
3935
+
3936
+ // MARK: - Document Picker Delegate
3937
+ extension CraftWebView.Coordinator: UIDocumentPickerDelegate {
3938
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
3939
+ guard let url = urls.first else {
3940
+ rejectCallback(pendingCallbackId, error: "No file selected")
3941
+ return
3942
+ }
3943
+
3944
+ // Get file data as base64
3945
+ if let data = try? Data(contentsOf: url) {
3946
+ let mimeType = url.mimeType
3947
+ let base64 = "data:\(mimeType);base64," + data.base64EncodedString()
3948
+ resolveCallback(pendingCallbackId, result: [
3949
+ "name": url.lastPathComponent,
3950
+ "path": url.path,
3951
+ "data": base64,
3952
+ "mimeType": mimeType
3953
+ ])
3954
+ } else {
3955
+ resolveCallback(pendingCallbackId, result: [
3956
+ "name": url.lastPathComponent,
3957
+ "path": url.path
3958
+ ])
3959
+ }
3960
+ pendingCallbackId = nil
3961
+ }
3962
+
3963
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
3964
+ rejectCallback(pendingCallbackId, error: "Cancelled")
3965
+ pendingCallbackId = nil
3966
+ }
3967
+ }
3968
+
3969
+ // MARK: - Apple Sign In Delegate
3970
+ extension CraftWebView.Coordinator: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
3971
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
3972
+ if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
3973
+ let userId = appleIDCredential.user
3974
+ let email = appleIDCredential.email
3975
+ let fullName = appleIDCredential.fullName
3976
+
3977
+ var name = ""
3978
+ if let givenName = fullName?.givenName {
3979
+ name = givenName
3980
+ }
3981
+ if let familyName = fullName?.familyName {
3982
+ name += (name.isEmpty ? "" : " ") + familyName
3983
+ }
3984
+
3985
+ var result: [String: Any] = ["userId": userId]
3986
+ if let email = email { result["email"] = email }
3987
+ if !name.isEmpty { result["name"] = name }
3988
+
3989
+ if let identityToken = appleIDCredential.identityToken,
3990
+ let tokenString = String(data: identityToken, encoding: .utf8) {
3991
+ result["identityToken"] = tokenString
3992
+ }
3993
+
3994
+ resolveCallback(pendingCallbackId, result: result)
3995
+ }
3996
+ pendingCallbackId = nil
3997
+ }
3998
+
3999
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
4000
+ rejectCallback(pendingCallbackId, error: error.localizedDescription)
4001
+ pendingCallbackId = nil
4002
+ }
4003
+
4004
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
4005
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
4006
+ let window = windowScene.windows.first else {
4007
+ return UIWindow()
4008
+ }
4009
+ return window
4010
+ }
4011
+ }
4012
+
4013
+ // MARK: - Bluetooth Delegate
4014
+ extension CraftWebView.Coordinator: CBCentralManagerDelegate {
4015
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
4016
+ if central.state == .poweredOn {
4017
+ central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
4018
+ resolveCallback(pendingCallbackId, result: true)
4019
+ } else {
4020
+ rejectCallback(pendingCallbackId, error: "Bluetooth not available")
4021
+ }
4022
+ pendingCallbackId = nil
4023
+ }
4024
+
4025
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
4026
+ if !discoveredPeripherals.contains(peripheral) {
4027
+ discoveredPeripherals.append(peripheral)
4028
+ sendToWeb("craftBluetoothDevice", data: [
4029
+ "id": peripheral.identifier.uuidString,
4030
+ "name": peripheral.name ?? "Unknown",
4031
+ "rssi": RSSI.intValue
4032
+ ])
4033
+ }
4034
+ }
4035
+ }
4036
+
4037
+ // MARK: - NFC Delegate
4038
+ extension CraftWebView.Coordinator: NFCNDEFReaderSessionDelegate {
4039
+ func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
4040
+ var records: [[String: Any]] = []
4041
+ for message in messages {
4042
+ for record in message.records {
4043
+ var recordData: [String: Any] = [
4044
+ "typeNameFormat": record.typeNameFormat.rawValue,
4045
+ "type": String(data: record.type, encoding: .utf8) ?? "",
4046
+ "identifier": String(data: record.identifier, encoding: .utf8) ?? ""
4047
+ ]
4048
+ if let payload = String(data: record.payload, encoding: .utf8) {
4049
+ recordData["payload"] = payload
4050
+ } else {
4051
+ recordData["payload"] = record.payload.base64EncodedString()
4052
+ }
4053
+ records.append(recordData)
4054
+ }
4055
+ }
4056
+ resolveCallback(pendingCallbackId, result: records)
4057
+ pendingCallbackId = nil
4058
+ }
4059
+
4060
+ func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
4061
+ if (error as NSError).code != 200 { // 200 is user cancelled
4062
+ rejectCallback(pendingCallbackId, error: error.localizedDescription)
4063
+ }
4064
+ pendingCallbackId = nil
4065
+ }
4066
+ }
4067
+
4068
+ // MARK: - ARSCNViewDelegate Extension
4069
+ extension CraftWebView.Coordinator {
4070
+ func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
4071
+ guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
4072
+
4073
+ // Store plane
4074
+ detectedPlanes[anchor.identifier] = planeAnchor
4075
+
4076
+ // Create plane visualization
4077
+ let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
4078
+ plane.firstMaterial?.diffuse.contents = UIColor.systemBlue.withAlphaComponent(0.3)
4079
+
4080
+ let planeNode = SCNNode(geometry: plane)
4081
+ planeNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)
4082
+ planeNode.eulerAngles.x = -.pi / 2
4083
+
4084
+ node.addChildNode(planeNode)
4085
+
4086
+ // Dispatch plane detected event
4087
+ DispatchQueue.main.async { [weak self] in
4088
+ self?.sendToWeb("craftARPlane", data: [
4089
+ "type": "added",
4090
+ "id": anchor.identifier.uuidString,
4091
+ "alignment": planeAnchor.alignment == .horizontal ? "horizontal" : "vertical",
4092
+ "center": ["x": planeAnchor.center.x, "y": planeAnchor.center.y, "z": planeAnchor.center.z],
4093
+ "extent": ["width": planeAnchor.extent.x, "height": planeAnchor.extent.z]
4094
+ ])
4095
+ }
4096
+ }
4097
+
4098
+ func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
4099
+ guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
4100
+
4101
+ // Update stored plane
4102
+ detectedPlanes[anchor.identifier] = planeAnchor
4103
+
4104
+ // Update plane visualization
4105
+ if let planeNode = node.childNodes.first,
4106
+ let plane = planeNode.geometry as? SCNPlane {
4107
+ plane.width = CGFloat(planeAnchor.extent.x)
4108
+ plane.height = CGFloat(planeAnchor.extent.z)
4109
+ planeNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)
4110
+ }
4111
+ }
4112
+
4113
+ func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
4114
+ guard anchor is ARPlaneAnchor else { return }
4115
+
4116
+ // Remove from stored planes
4117
+ detectedPlanes.removeValue(forKey: anchor.identifier)
4118
+
4119
+ DispatchQueue.main.async { [weak self] in
4120
+ self?.sendToWeb("craftARPlane", data: [
4121
+ "type": "removed",
4122
+ "id": anchor.identifier.uuidString
4123
+ ])
4124
+ }
4125
+ }
4126
+ }
4127
+
4128
+ // MARK: - URL Extension for MIME types
4129
+ extension URL {
4130
+ var mimeType: String {
4131
+ if let utType = UTType(filenameExtension: pathExtension) {
4132
+ return utType.preferredMIMEType ?? "application/octet-stream"
4133
+ }
4134
+ return "application/octet-stream"
4135
+ }
4136
+ }
4137
+
4138
+ // MARK: - Contact Picker Delegate
4139
+ extension CraftWebView.Coordinator: CNContactPickerDelegate {
4140
+ func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
4141
+ let contactData = formatContact(contact)
4142
+ resolveCallback(pendingCallbackId, result: contactData)
4143
+ pendingCallbackId = nil
4144
+ }
4145
+
4146
+ func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
4147
+ let contactsData = contacts.map { formatContact($0) }
4148
+ resolveCallback(pendingCallbackId, result: contactsData)
4149
+ pendingCallbackId = nil
4150
+ }
4151
+
4152
+ func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
4153
+ rejectCallback(pendingCallbackId, error: "Cancelled")
4154
+ pendingCallbackId = nil
4155
+ }
4156
+
4157
+ private func formatContact(_ contact: CNContact) -> [String: Any] {
4158
+ var data: [String: Any] = [
4159
+ "id": contact.identifier,
4160
+ "givenName": contact.givenName,
4161
+ "familyName": contact.familyName,
4162
+ "displayName": CNContactFormatter.string(from: contact, style: .fullName) ?? ""
4163
+ ]
4164
+
4165
+ // Phone numbers
4166
+ var phones: [[String: String]] = []
4167
+ for phone in contact.phoneNumbers {
4168
+ phones.append([
4169
+ "label": CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: phone.label ?? ""),
4170
+ "number": phone.value.stringValue
4171
+ ])
4172
+ }
4173
+ data["phoneNumbers"] = phones
4174
+
4175
+ // Email addresses
4176
+ var emails: [[String: String]] = []
4177
+ for email in contact.emailAddresses {
4178
+ emails.append([
4179
+ "label": CNLabeledValue<NSString>.localizedString(forLabel: email.label ?? ""),
4180
+ "address": email.value as String
4181
+ ])
4182
+ }
4183
+ data["emailAddresses"] = emails
4184
+
4185
+ return data
4186
+ }
4187
+ }
4188
+
4189
+ // MARK: - Watch Connectivity Delegate
4190
+ extension CraftWebView.Coordinator: WCSessionDelegate {
4191
+ func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
4192
+ if let error = error {
4193
+ print("WCSession activation failed: \(error.localizedDescription)")
4194
+ } else {
4195
+ print("WCSession activated with state: \(activationState.rawValue)")
4196
+ }
4197
+ }
4198
+
4199
+ func sessionDidBecomeInactive(_ session: WCSession) {
4200
+ print("WCSession became inactive")
4201
+ }
4202
+
4203
+ func sessionDidDeactivate(_ session: WCSession) {
4204
+ // Reactivate session on new paired device
4205
+ session.activate()
4206
+ }
4207
+
4208
+ func sessionReachabilityDidChange(_ session: WCSession) {
4209
+ DispatchQueue.main.async { [weak self] in
4210
+ self?.sendToWeb("craftWatchReachability", data: [
4211
+ "reachable": session.isReachable
4212
+ ])
4213
+ }
4214
+ }
4215
+
4216
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
4217
+ DispatchQueue.main.async { [weak self] in
4218
+ self?.sendToWeb("craftWatchMessage", data: message)
4219
+ }
4220
+ }
4221
+
4222
+ func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
4223
+ DispatchQueue.main.async { [weak self] in
4224
+ self?.sendToWeb("craftWatchMessage", data: message)
4225
+ }
4226
+ // Default reply - apps can customize this behavior
4227
+ replyHandler(["received": true])
4228
+ }
4229
+
4230
+ func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
4231
+ DispatchQueue.main.async { [weak self] in
4232
+ self?.sendToWeb("craftWatchContext", data: applicationContext)
4233
+ }
4234
+ }
4235
+
4236
+ func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) {
4237
+ DispatchQueue.main.async { [weak self] in
4238
+ self?.sendToWeb("craftWatchUserInfo", data: userInfo)
4239
+ }
4240
+ }
4241
+ }