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.
- package/dist/android/src/index.js +268 -0
- package/dist/android/src/index.js.map +10 -0
- package/dist/android/templates/AndroidManifest.xml.template +69 -0
- package/dist/android/templates/CraftBridge.kt.template +3748 -0
- package/dist/android/templates/CraftBridgeExtensions.kt.template +383 -0
- package/dist/android/templates/CraftWidgetProvider.kt.template +246 -0
- package/dist/android/templates/MainActivity.kt.template +109 -0
- package/dist/android/templates/build.gradle.kts.app.template +80 -0
- package/dist/android/templates/build.gradle.kts.project.template +5 -0
- package/dist/android/templates/fastlane/Appfile +25 -0
- package/dist/android/templates/fastlane/Fastfile +192 -0
- package/dist/android/templates/fastlane/Gemfile +7 -0
- package/dist/android/templates/github-workflow-android.yml +248 -0
- package/dist/android/templates/github-workflow-test.yml +329 -0
- package/dist/android/templates/settings.gradle.kts.template +18 -0
- package/dist/android/templates/test-bridges.html +1463 -0
- package/dist/api/window.d.ts +18 -0
- package/dist/api/window.d.ts.map +1 -1
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/bridge/core.d.ts +2 -0
- package/dist/bridge/core.d.ts.map +1 -1
- package/dist/cli.js +4168 -0
- package/dist/components/sidebar.d.ts +5 -1
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/dev/devtools.d.ts +1 -1
- package/dist/dev/devtools.d.ts.map +1 -1
- package/dist/dev/hot-reload.d.ts +12 -3
- package/dist/dev/hot-reload.d.ts.map +1 -1
- package/dist/index.cjs +573 -208
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +585 -216
- package/dist/index.js.map +45 -0
- package/dist/ios/src/index.js +186 -0
- package/dist/ios/src/index.js.map +10 -0
- package/dist/ios/templates/CraftApp.swift +4241 -0
- package/dist/ios/templates/CraftAppExtensions.swift +361 -0
- package/dist/ios/templates/CraftWidget.swift +247 -0
- package/dist/ios/templates/Info.plist.template +65 -0
- package/dist/ios/templates/fastlane/Appfile +45 -0
- package/dist/ios/templates/fastlane/Fastfile +189 -0
- package/dist/ios/templates/fastlane/Gemfile +7 -0
- package/dist/ios/templates/fastlane/Matchfile +58 -0
- package/dist/ios/templates/github-workflow-ios.yml +192 -0
- package/dist/ios/templates/github-workflow-release.yml +175 -0
- package/dist/ios/templates/github-workflow-test.yml +272 -0
- package/dist/ios/templates/project.yml.template +25 -0
- package/dist/ios/templates/test-bridges.html +1463 -0
- package/dist/package.d.ts +106 -2
- package/dist/package.d.ts.map +1 -1
- package/dist/package.test.d.ts +2 -0
- package/dist/package.test.d.ts.map +1 -0
- package/dist/styles/sidebar-templates.d.ts.map +1 -1
- package/dist/styles/sidebars.d.ts +12 -8
- package/dist/styles/sidebars.d.ts.map +1 -1
- package/dist/templates/projects/blank/craft.config.ts +19 -0
- package/dist/templates/projects/blank/index.html +27 -0
- package/dist/templates/projects/blank/package.json +19 -0
- package/dist/templates/projects/blank/src/main.ts +26 -0
- package/dist/templates/projects/dashboard/craft.config.ts +42 -0
- package/dist/templates/projects/dashboard/index.html +13 -0
- package/dist/templates/projects/dashboard/package.json +20 -0
- package/dist/templates/projects/dashboard/src/main.ts +163 -0
- package/dist/templates/projects/dashboard/src/styles.css +280 -0
- package/dist/templates/projects/drawer/craft.config.ts +47 -0
- package/dist/templates/projects/drawer/index.html +13 -0
- package/dist/templates/projects/drawer/package.json +21 -0
- package/dist/templates/projects/drawer/src/main.ts +406 -0
- package/dist/templates/projects/drawer/src/styles.css +468 -0
- package/dist/templates/projects/ecommerce/craft.config.ts +32 -0
- package/dist/templates/projects/ecommerce/index.html +13 -0
- package/dist/templates/projects/ecommerce/package.json +19 -0
- package/dist/templates/projects/ecommerce/src/main.ts +194 -0
- package/dist/templates/projects/ecommerce/src/styles.css +169 -0
- package/dist/templates/projects/social/craft.config.ts +32 -0
- package/dist/templates/projects/social/index.html +13 -0
- package/dist/templates/projects/social/package.json +19 -0
- package/dist/templates/projects/social/src/main.ts +270 -0
- package/dist/templates/projects/social/src/styles.css +216 -0
- package/dist/templates/projects/tabs/craft.config.ts +47 -0
- package/dist/templates/projects/tabs/index.html +13 -0
- package/dist/templates/projects/tabs/package.json +21 -0
- package/dist/templates/projects/tabs/src/main.ts +177 -0
- package/dist/templates/projects/tabs/src/styles.css +163 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/updater/index.d.ts +1 -0
- package/dist/updater/index.d.ts.map +1 -1
- package/package.json +5 -12
- 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
|
+
}
|