craft-native 0.0.369 → 0.0.374

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.
@@ -1,6 +1,8 @@
1
1
  import SwiftUI
2
2
  import WebKit
3
+ #if CRAFT_PERMISSION_SPEECH
3
4
  import Speech
5
+ #endif
4
6
  import AVFoundation
5
7
  import LocalAuthentication
6
8
  import Security
@@ -761,6 +763,8 @@ final class CraftChrome: ObservableObject {
761
763
  var displayTitle: String { titleOverride ?? title }
762
764
  var accessibilityLabel: String? = nil
763
765
  var image: String? = nil
766
+ /// A compact floating icon, instead of the system full-width accessory shelf.
767
+ var iconOnly = false
764
768
  }
765
769
 
766
770
  @Published var tabs: [Tab] = []
@@ -895,7 +899,8 @@ final class CraftChrome: ObservableObject {
895
899
  let title = item["title"] as? String {
896
900
  next = Tab(id: path, title: title, symbol: item["symbol"] as? String ?? "message",
897
901
  badge: (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) },
898
- accessibilityLabel: item["accessibilityLabel"] as? String)
902
+ accessibilityLabel: item["accessibilityLabel"] as? String,
903
+ iconOnly: item["iconOnly"] as? Bool ?? false)
899
904
  } else {
900
905
  next = nil
901
906
  }
@@ -2259,9 +2264,11 @@ struct CraftWebView: UIViewRepresentable {
2259
2264
  // MARK: - Coordinator (Native Bridge)
2260
2265
  class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, UIImagePickerControllerDelegate, UINavigationControllerDelegate, CLLocationManagerDelegate, ARSCNViewDelegate {
2261
2266
  let config: CraftConfig
2267
+ #if CRAFT_PERMISSION_SPEECH
2262
2268
  private var speechRecognizer: SFSpeechRecognizer?
2263
2269
  private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
2264
2270
  private var recognitionTask: SFSpeechRecognitionTask?
2271
+ #endif
2265
2272
  private var audioEngine = AVAudioEngine()
2266
2273
  private weak var webView: WKWebView?
2267
2274
  private var pendingCallbackId: String?
@@ -2392,9 +2399,12 @@ struct CraftWebView: UIViewRepresentable {
2392
2399
  self.recordingDirectory = recordingDirectory
2393
2400
  self.config = config
2394
2401
  super.init()
2402
+ #if CRAFT_PERMISSION_SPEECH
2395
2403
  if config.enableSpeechRecognition {
2396
2404
  speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
2397
2405
  }
2406
+ #endif
2407
+ #if CRAFT_PERMISSION_LOCATION
2398
2408
  if config.enableGeolocation {
2399
2409
  locationManager = suppliedLocationManager ?? CLLocationManager()
2400
2410
  locationManager?.desiredAccuracy = kCLLocationAccuracyBest
@@ -2408,15 +2418,22 @@ struct CraftWebView: UIViewRepresentable {
2408
2418
  catch { locationRecordingRestoreError = error }
2409
2419
  }
2410
2420
  }
2421
+ #endif
2422
+ #if CRAFT_PERMISSION_CONTACTS
2411
2423
  if config.enableContacts {
2412
2424
  contactStore = CNContactStore()
2413
2425
  }
2426
+ #endif
2427
+ #if CRAFT_PERMISSION_CALENDAR
2414
2428
  if config.enableCalendar {
2415
2429
  eventStore = EKEventStore()
2416
2430
  }
2431
+ #endif
2432
+ #if CRAFT_PERMISSION_MOTION
2417
2433
  if config.enableMotionSensors {
2418
2434
  motionManager = CMMotionManager()
2419
2435
  }
2436
+ #endif
2420
2437
  if config.enableHealthKit {
2421
2438
  // Apple Health's heart rate as it arrives
2422
2439
  // (craft.health.startHeartRateUpdates) reaches the page, and
@@ -3377,6 +3394,12 @@ struct CraftWebView: UIViewRepresentable {
3377
3394
  } else {
3378
3395
  rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
3379
3396
  }
3397
+ case "discardHealthWorkout":
3398
+ if config.enableHealthKit {
3399
+ CraftHealth.shared.discardWorkout { [weak self] in self?.answer(callbackId, $0) }
3400
+ } else {
3401
+ rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
3402
+ }
3380
3403
 
3381
3404
  // MARK: - Live Activities
3382
3405
  case "isLiveActivityAvailable":
@@ -4120,11 +4143,13 @@ struct CraftWebView: UIViewRepresentable {
4120
4143
  /// The bridge, as the source the page runs. Only in a trusted page:
4121
4144
  /// the check is the first thing it does.
4122
4145
  func bridgeScript() -> String {
4123
- let laContext = LAContext()
4124
4146
  var biometricAvailable = false
4147
+ #if CRAFT_PERMISSION_BIOMETRIC
4148
+ let laContext = LAContext()
4125
4149
  if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
4126
4150
  biometricAvailable = true
4127
4151
  }
4152
+ #endif
4128
4153
 
4129
4154
  let script = """
4130
4155
  window.craft = {
@@ -5980,7 +6005,8 @@ struct CraftWebView: UIViewRepresentable {
5980
6005
  startWorkout: function(options) { return craft._invoke('startHealthWorkout', options || {}); },
5981
6006
  pauseWorkout: function() { return craft._invoke('pauseHealthWorkout'); },
5982
6007
  resumeWorkout: function() { return craft._invoke('resumeHealthWorkout'); },
5983
- endWorkout: function() { return craft._invoke('endHealthWorkout'); }
6008
+ endWorkout: function() { return craft._invoke('endHealthWorkout'); },
6009
+ discardWorkout: function() { return craft._invoke('discardHealthWorkout'); }
5984
6010
  };
5985
6011
  // The Music app's song and transport. Nothing is read until
5986
6012
  // requestAuthorization() has been allowed; observe(true) makes
@@ -6340,6 +6366,7 @@ struct CraftWebView: UIViewRepresentable {
6340
6366
 
6341
6367
  // MARK: - Speech Recognition
6342
6368
  private func startSpeechRecognition() {
6369
+ #if CRAFT_PERMISSION_SPEECH
6343
6370
  SFSpeechRecognizer.requestAuthorization { [weak self] status in
6344
6371
  guard status == .authorized else {
6345
6372
  self?.sendToWeb("craftSpeechError", data: ["error": "Not authorized"])
@@ -6347,9 +6374,14 @@ struct CraftWebView: UIViewRepresentable {
6347
6374
  }
6348
6375
  DispatchQueue.main.async { self?.beginRecording() }
6349
6376
  }
6377
+
6378
+ #else
6379
+ sendToWeb("craftSpeechError", data: ["error": "Speech recognition was not enabled when this app was built"])
6380
+ #endif
6350
6381
  }
6351
6382
 
6352
6383
  private func beginRecording() {
6384
+ #if CRAFT_PERMISSION_SPEECH
6353
6385
  if recognitionTask != nil {
6354
6386
  recognitionTask?.cancel()
6355
6387
  recognitionTask = nil
@@ -6401,9 +6433,14 @@ struct CraftWebView: UIViewRepresentable {
6401
6433
  } catch {
6402
6434
  sendToWeb("craftSpeechError", data: ["error": "Audio engine failed"])
6403
6435
  }
6436
+
6437
+ #else
6438
+
6439
+ #endif
6404
6440
  }
6405
6441
 
6406
6442
  private func stopSpeechRecognition() {
6443
+ #if CRAFT_PERMISSION_SPEECH
6407
6444
  audioEngine.stop()
6408
6445
  audioEngine.inputNode.removeTap(onBus: 0)
6409
6446
  recognitionRequest?.endAudio()
@@ -6412,6 +6449,10 @@ struct CraftWebView: UIViewRepresentable {
6412
6449
  recognitionTask = nil
6413
6450
  sendToWeb("craftSpeechEnd", data: [:])
6414
6451
  CraftNativeActions.triggerHaptic(style: "light")
6452
+
6453
+ #else
6454
+
6455
+ #endif
6415
6456
  }
6416
6457
 
6417
6458
  // MARK: - Share
@@ -6453,6 +6494,7 @@ struct CraftWebView: UIViewRepresentable {
6453
6494
 
6454
6495
  // MARK: - Camera & Photo Library
6455
6496
  private func openCamera() {
6497
+ #if CRAFT_PERMISSION_CAMERA
6456
6498
  DispatchQueue.main.async {
6457
6499
  guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
6458
6500
  self.rejectCallback(self.pendingCallbackId, error: "Camera not available")
@@ -6465,9 +6507,14 @@ struct CraftWebView: UIViewRepresentable {
6465
6507
  picker.delegate = self
6466
6508
  rootVC.present(picker, animated: true)
6467
6509
  }
6510
+
6511
+ #else
6512
+ rejectCallback(pendingCallbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6513
+ #endif
6468
6514
  }
6469
6515
 
6470
6516
  private func pickImage() {
6517
+ #if CRAFT_PERMISSION_PHOTOS
6471
6518
  DispatchQueue.main.async {
6472
6519
  guard let rootVC = CraftPresenter.topViewController() else { return }
6473
6520
 
@@ -6476,6 +6523,10 @@ struct CraftWebView: UIViewRepresentable {
6476
6523
  picker.delegate = self
6477
6524
  rootVC.present(picker, animated: true)
6478
6525
  }
6526
+
6527
+ #else
6528
+ rejectCallback(pendingCallbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6529
+ #endif
6479
6530
  }
6480
6531
 
6481
6532
  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
@@ -6513,6 +6564,7 @@ struct CraftWebView: UIViewRepresentable {
6513
6564
 
6514
6565
  // MARK: - Biometric Authentication
6515
6566
  private func authenticate(reason: String, callbackId: String?) {
6567
+ #if CRAFT_PERMISSION_BIOMETRIC
6516
6568
  let context = LAContext()
6517
6569
  var error: NSError?
6518
6570
 
@@ -6529,6 +6581,10 @@ struct CraftWebView: UIViewRepresentable {
6529
6581
  } else {
6530
6582
  rejectCallback(callbackId, error: error?.localizedDescription ?? "Biometric not available")
6531
6583
  }
6584
+
6585
+ #else
6586
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6587
+ #endif
6532
6588
  }
6533
6589
 
6534
6590
  // MARK: - Push Notifications
@@ -6661,31 +6717,63 @@ struct CraftWebView: UIViewRepresentable {
6661
6717
  }
6662
6718
  switch permission {
6663
6719
  case "location", "locationAlways":
6720
+ #if CRAFT_PERMISSION_LOCATION
6664
6721
  let status = (locationManager ?? CLLocationManager()).authorizationStatus
6665
6722
  let granted = status == .authorizedAlways || (permission == "location" && status == .authorizedWhenInUse)
6666
6723
  resolveCallback(callbackId, result: permissionStatus(granted, denied: status == .denied, restricted: status == .restricted))
6724
+ #else
6725
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6726
+ #endif
6667
6727
  case "camera":
6728
+ #if CRAFT_PERMISSION_CAMERA
6668
6729
  let status = AVCaptureDevice.authorizationStatus(for: .video)
6669
6730
  resolveCallback(callbackId, result: permissionStatus(status == .authorized, denied: status == .denied, restricted: status == .restricted))
6731
+ #else
6732
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6733
+ #endif
6670
6734
  case "microphone":
6735
+ #if CRAFT_PERMISSION_MICROPHONE
6671
6736
  let status = AVAudioSession.sharedInstance().recordPermission
6672
6737
  resolveCallback(callbackId, result: permissionStatus(status == .granted, denied: status == .denied))
6738
+ #else
6739
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6740
+ #endif
6673
6741
  case "photos":
6742
+ #if CRAFT_PERMISSION_PHOTOS
6674
6743
  let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
6675
6744
  resolveCallback(callbackId, result: permissionStatus(status == .authorized || status == .limited, denied: status == .denied, restricted: status == .restricted))
6745
+ #else
6746
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6747
+ #endif
6676
6748
  case "contacts":
6749
+ #if CRAFT_PERMISSION_CONTACTS
6677
6750
  let status = CNContactStore.authorizationStatus(for: .contacts)
6678
6751
  resolveCallback(callbackId, result: permissionStatus(status == .authorized, denied: status == .denied, restricted: status == .restricted))
6752
+ #else
6753
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6754
+ #endif
6679
6755
  case "calendar", "reminders":
6756
+ #if CRAFT_PERMISSION_CALENDAR
6680
6757
  let entity: EKEntityType = permission == "calendar" ? .event : .reminder
6681
6758
  let status = EKEventStore.authorizationStatus(for: entity)
6682
6759
  resolveCallback(callbackId, result: permissionStatus(status == .authorized || status.rawValue >= 4, denied: status == .denied, restricted: status == .restricted))
6760
+ #else
6761
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6762
+ #endif
6683
6763
  case "motion":
6764
+ #if CRAFT_PERMISSION_MOTION
6684
6765
  let status = CMMotionActivityManager.authorizationStatus()
6685
6766
  resolveCallback(callbackId, result: permissionStatus(status == .authorized, denied: status == .denied, restricted: status == .restricted))
6767
+ #else
6768
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6769
+ #endif
6686
6770
  case "bluetooth":
6771
+ #if CRAFT_PERMISSION_BLUETOOTH
6687
6772
  let status = CBManager.authorization
6688
6773
  resolveCallback(callbackId, result: permissionStatus(status == .allowedAlways, denied: status == .denied, restricted: status == .restricted))
6774
+ #else
6775
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6776
+ #endif
6689
6777
  case "notifications":
6690
6778
  // Zig answers UnknownAction for this permission, so Swift
6691
6779
  // serves it on both runtimes and this is the only answer.
@@ -6719,6 +6807,7 @@ struct CraftWebView: UIViewRepresentable {
6719
6807
  }
6720
6808
  switch permission {
6721
6809
  case "location", "locationAlways":
6810
+ #if CRAFT_PERMISSION_LOCATION
6722
6811
  guard let manager = locationManager else {
6723
6812
  rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
6724
6813
  return
@@ -6741,23 +6830,43 @@ struct CraftWebView: UIViewRepresentable {
6741
6830
  } else {
6742
6831
  manager.requestWhenInUseAuthorization()
6743
6832
  }
6833
+ #else
6834
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6835
+ #endif
6744
6836
  case "camera":
6837
+ #if CRAFT_PERMISSION_CAMERA
6745
6838
  AVCaptureDevice.requestAccess(for: .video) { granted in
6746
6839
  self.resolveCallback(callbackId, result: granted ? "granted" : "denied")
6747
6840
  }
6841
+ #else
6842
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6843
+ #endif
6748
6844
  case "microphone":
6845
+ #if CRAFT_PERMISSION_MICROPHONE
6749
6846
  AVAudioSession.sharedInstance().requestRecordPermission { granted in
6750
6847
  self.resolveCallback(callbackId, result: granted ? "granted" : "denied")
6751
6848
  }
6849
+ #else
6850
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6851
+ #endif
6752
6852
  case "photos":
6853
+ #if CRAFT_PERMISSION_PHOTOS
6753
6854
  PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
6754
6855
  self.resolveCallback(callbackId, result: status == .authorized || status == .limited ? "granted" : "denied")
6755
6856
  }
6857
+ #else
6858
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6859
+ #endif
6756
6860
  case "contacts":
6861
+ #if CRAFT_PERMISSION_CONTACTS
6757
6862
  (contactStore ?? CNContactStore()).requestAccess(for: .contacts) { granted, _ in
6758
6863
  self.resolveCallback(callbackId, result: granted ? "granted" : "denied")
6759
6864
  }
6865
+ #else
6866
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6867
+ #endif
6760
6868
  case "calendar", "reminders":
6869
+ #if CRAFT_PERMISSION_CALENDAR
6761
6870
  let store = eventStore ?? EKEventStore()
6762
6871
  let entity: EKEntityType = permission == "calendar" ? .event : .reminder
6763
6872
  if #available(iOS 17.0, *) {
@@ -6775,6 +6884,9 @@ struct CraftWebView: UIViewRepresentable {
6775
6884
  self.resolveCallback(callbackId, result: granted ? "granted" : "denied")
6776
6885
  }
6777
6886
  }
6887
+ #else
6888
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
6889
+ #endif
6778
6890
  case "notifications":
6779
6891
  UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
6780
6892
  self.resolveCallback(callbackId, result: granted ? "granted" : "denied")
@@ -6814,6 +6926,7 @@ struct CraftWebView: UIViewRepresentable {
6814
6926
  }
6815
6927
 
6816
6928
  private func continueSingleLocationRequest(_ manager: CLLocationManager) {
6929
+ #if CRAFT_PERMISSION_LOCATION
6817
6930
  guard let callbackId = singleLocationCallbackId else { return }
6818
6931
  switch manager.authorizationStatus {
6819
6932
  case .notDetermined:
@@ -6846,6 +6959,10 @@ struct CraftWebView: UIViewRepresentable {
6846
6959
  singleLocationTimeoutWorkItem = timeoutWorkItem
6847
6960
  DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(min(timeoutMs + 100, Double(Int.max)))), execute: timeoutWorkItem)
6848
6961
  manager.requestLocation()
6962
+
6963
+ #else
6964
+
6965
+ #endif
6849
6966
  }
6850
6967
 
6851
6968
  private func finishSingleLocationRequest() {
@@ -6871,12 +6988,17 @@ struct CraftWebView: UIViewRepresentable {
6871
6988
  }
6872
6989
 
6873
6990
  private func watchPosition(callbackId: String?) {
6991
+ #if CRAFT_PERMISSION_LOCATION
6874
6992
  locationManager?.delegate = self
6875
6993
  isWatchingLocation = true
6876
6994
  requestLocationAuthorization()
6877
6995
  configureBackgroundLocationIfNeeded()
6878
6996
  locationManager?.startUpdatingLocation()
6879
6997
  resolveCallback(callbackId, result: true)
6998
+
6999
+ #else
7000
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7001
+ #endif
6880
7002
  }
6881
7003
 
6882
7004
  private func stopWatchingPosition() {
@@ -6898,16 +7020,26 @@ struct CraftWebView: UIViewRepresentable {
6898
7020
  }
6899
7021
 
6900
7022
  private func requestLocationAuthorization() {
7023
+ #if CRAFT_PERMISSION_LOCATION
6901
7024
  if config.enableBackgroundLocation {
6902
7025
  locationManager?.requestAlwaysAuthorization()
6903
7026
  } else {
6904
7027
  locationManager?.requestWhenInUseAuthorization()
6905
7028
  }
7029
+
7030
+ #else
7031
+
7032
+ #endif
6906
7033
  }
6907
7034
 
6908
7035
  private func configureBackgroundLocationIfNeeded() {
7036
+ #if CRAFT_PERMISSION_LOCATION
6909
7037
  locationManager?.allowsBackgroundLocationUpdates = config.enableBackgroundLocation
6910
7038
  locationManager?.showsBackgroundLocationIndicator = config.enableBackgroundLocation
7039
+
7040
+ #else
7041
+
7042
+ #endif
6911
7043
  }
6912
7044
 
6913
7045
  private func persistLocationRecordingState() throws {
@@ -6916,6 +7048,7 @@ struct CraftWebView: UIViewRepresentable {
6916
7048
  }
6917
7049
 
6918
7050
  private func restoreLocationRecordingState() throws {
7051
+ #if CRAFT_PERMISSION_LOCATION
6919
7052
  guard FileManager.default.fileExists(atPath: locationRecordingStateURL.path) else {
6920
7053
  if FileManager.default.fileExists(atPath: locationRecordingTrackURL.path), !(try Data(contentsOf: locationRecordingTrackURL)).isEmpty {
6921
7054
  throw CraftNativeActionError(code: "RECORDING_STORAGE_ERROR", message: "Recording metadata is missing")
@@ -6945,6 +7078,10 @@ struct CraftWebView: UIViewRepresentable {
6945
7078
  configureBackgroundLocationIfNeeded()
6946
7079
  locationManager?.startUpdatingLocation()
6947
7080
  }
7081
+
7082
+ #else
7083
+
7084
+ #endif
6948
7085
  }
6949
7086
 
6950
7087
  /// Same serial main-queue dispatch as the operation it protects.
@@ -7016,6 +7153,7 @@ struct CraftWebView: UIViewRepresentable {
7016
7153
  }
7017
7154
 
7018
7155
  private func startLocationRecording(body: [String: Any], callbackId: String?) {
7156
+ #if CRAFT_PERMISSION_LOCATION
7019
7157
  guard !isRecordingLocation else {
7020
7158
  rejectCallback(callbackId, error: "A location recording is already active", code: "RECORDING_ALREADY_ACTIVE")
7021
7159
  return
@@ -7051,6 +7189,10 @@ struct CraftWebView: UIViewRepresentable {
7051
7189
  configureBackgroundLocationIfNeeded()
7052
7190
  locationManager?.startUpdatingLocation()
7053
7191
  resolveCallback(callbackId, result: locationRecordingSummary())
7192
+
7193
+ #else
7194
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7195
+ #endif
7054
7196
  }
7055
7197
 
7056
7198
  private func pauseLocationRecording(callbackId: String?) {
@@ -7071,6 +7213,7 @@ struct CraftWebView: UIViewRepresentable {
7071
7213
  }
7072
7214
 
7073
7215
  private func resumeLocationRecording(callbackId: String?) {
7216
+ #if CRAFT_PERMISSION_LOCATION
7074
7217
  guard isRecordingLocation else {
7075
7218
  rejectCallback(callbackId, error: "No active location recording", code: "NO_ACTIVE_RECORDING")
7076
7219
  return
@@ -7091,6 +7234,10 @@ struct CraftWebView: UIViewRepresentable {
7091
7234
  configureBackgroundLocationIfNeeded()
7092
7235
  locationManager?.startUpdatingLocation()
7093
7236
  resolveCallback(callbackId, result: locationRecordingSummary())
7237
+
7238
+ #else
7239
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7240
+ #endif
7094
7241
  }
7095
7242
 
7096
7243
  private func stopLocationRecording(callbackId: String?) {
@@ -7279,6 +7426,7 @@ struct CraftWebView: UIViewRepresentable {
7279
7426
 
7280
7427
  // MARK: - Flashlight
7281
7428
  private func setFlashlight(enabled: Bool, callbackId: String?) {
7429
+ #if CRAFT_PERMISSION_CAMERA
7282
7430
  guard let device = AVCaptureDevice.default(for: .video), device.hasTorch else {
7283
7431
  rejectCallback(callbackId, error: "Flashlight not available")
7284
7432
  return
@@ -7292,6 +7440,10 @@ struct CraftWebView: UIViewRepresentable {
7292
7440
  } catch {
7293
7441
  rejectCallback(callbackId, error: error.localizedDescription)
7294
7442
  }
7443
+
7444
+ #else
7445
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7446
+ #endif
7295
7447
  }
7296
7448
 
7297
7449
  // MARK: - Vibration Pattern
@@ -7309,6 +7461,7 @@ struct CraftWebView: UIViewRepresentable {
7309
7461
 
7310
7462
  // MARK: - Contacts
7311
7463
  private func getContacts(callbackId: String?) {
7464
+ #if CRAFT_PERMISSION_CONTACTS
7312
7465
  guard let store = contactStore else {
7313
7466
  rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
7314
7467
  return
@@ -7347,9 +7500,14 @@ struct CraftWebView: UIViewRepresentable {
7347
7500
  self?.rejectCallback(callbackId, error: error.localizedDescription)
7348
7501
  }
7349
7502
  }
7503
+
7504
+ #else
7505
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7506
+ #endif
7350
7507
  }
7351
7508
 
7352
7509
  private func addContact(_ data: [String: Any], callbackId: String?) {
7510
+ #if CRAFT_PERMISSION_CONTACTS
7353
7511
  guard let store = contactStore else {
7354
7512
  rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
7355
7513
  return
@@ -7380,10 +7538,15 @@ struct CraftWebView: UIViewRepresentable {
7380
7538
  self?.rejectCallback(callbackId, error: error.localizedDescription)
7381
7539
  }
7382
7540
  }
7541
+
7542
+ #else
7543
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7544
+ #endif
7383
7545
  }
7384
7546
 
7385
7547
  // MARK: - Calendar
7386
7548
  private func getCalendarEvents(startDate: Double?, endDate: Double?, callbackId: String?) {
7549
+ #if CRAFT_PERMISSION_CALENDAR
7387
7550
  guard let store = eventStore else {
7388
7551
  rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
7389
7552
  return
@@ -7416,9 +7579,14 @@ struct CraftWebView: UIViewRepresentable {
7416
7579
 
7417
7580
  self?.resolveCallback(callbackId, result: eventData)
7418
7581
  }
7582
+
7583
+ #else
7584
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7585
+ #endif
7419
7586
  }
7420
7587
 
7421
7588
  private func createCalendarEvent(_ data: [String: Any], callbackId: String?) {
7589
+ #if CRAFT_PERMISSION_CALENDAR
7422
7590
  guard let store = eventStore else {
7423
7591
  rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
7424
7592
  return
@@ -7454,9 +7622,14 @@ struct CraftWebView: UIViewRepresentable {
7454
7622
  self?.rejectCallback(callbackId, error: error.localizedDescription)
7455
7623
  }
7456
7624
  }
7625
+
7626
+ #else
7627
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7628
+ #endif
7457
7629
  }
7458
7630
 
7459
7631
  private func deleteCalendarEvent(_ eventId: String, callbackId: String?) {
7632
+ #if CRAFT_PERMISSION_CALENDAR
7460
7633
  guard let store = eventStore else {
7461
7634
  rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
7462
7635
  return
@@ -7472,6 +7645,10 @@ struct CraftWebView: UIViewRepresentable {
7472
7645
  } catch {
7473
7646
  rejectCallback(callbackId, error: error.localizedDescription)
7474
7647
  }
7648
+
7649
+ #else
7650
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
7651
+ #endif
7475
7652
  }
7476
7653
 
7477
7654
  // MARK: - Local Notifications
@@ -7870,6 +8047,7 @@ struct CraftWebView: UIViewRepresentable {
7870
8047
 
7871
8048
  // MARK: - QR/Barcode Scanner
7872
8049
  private func scanQRCode(callbackId: String?) {
8050
+ #if CRAFT_PERMISSION_CAMERA
7873
8051
  DispatchQueue.main.async {
7874
8052
  guard let rootVC = CraftPresenter.topViewController() else { return }
7875
8053
 
@@ -7888,6 +8066,10 @@ struct CraftWebView: UIViewRepresentable {
7888
8066
  self.rejectCallback(callbackId, error: "QR scanning requires iOS 16+")
7889
8067
  }
7890
8068
  }
8069
+
8070
+ #else
8071
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8072
+ #endif
7891
8073
  }
7892
8074
 
7893
8075
  // MARK: - File Picker
@@ -7978,6 +8160,7 @@ struct CraftWebView: UIViewRepresentable {
7978
8160
 
7979
8161
  // MARK: - Audio Recording
7980
8162
  private func startAudioRecording(callbackId: String?) {
8163
+ #if CRAFT_PERMISSION_MICROPHONE
7981
8164
  AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in
7982
8165
  guard granted else {
7983
8166
  self?.rejectCallback(callbackId, error: "Microphone permission denied")
@@ -8008,9 +8191,14 @@ struct CraftWebView: UIViewRepresentable {
8008
8191
  }
8009
8192
  }
8010
8193
  }
8194
+
8195
+ #else
8196
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8197
+ #endif
8011
8198
  }
8012
8199
 
8013
8200
  private func stopAudioRecording(callbackId: String?) {
8201
+ #if CRAFT_PERMISSION_MICROPHONE
8014
8202
  audioRecorder?.stop()
8015
8203
  audioRecorder = nil
8016
8204
 
@@ -8025,10 +8213,15 @@ struct CraftWebView: UIViewRepresentable {
8025
8213
  rejectCallback(callbackId, error: "No recording found")
8026
8214
  }
8027
8215
  recordingURL = nil
8216
+
8217
+ #else
8218
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8219
+ #endif
8028
8220
  }
8029
8221
 
8030
8222
  // MARK: - Video Recording
8031
8223
  private func startVideoRecording(callbackId: String?) {
8224
+ #if CRAFT_PERMISSION_CAMERA
8032
8225
  DispatchQueue.main.async {
8033
8226
  guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
8034
8227
  self.rejectCallback(callbackId, error: "Camera not available")
@@ -8045,10 +8238,15 @@ struct CraftWebView: UIViewRepresentable {
8045
8238
  self.pendingCallbackId = callbackId
8046
8239
  rootVC.present(picker, animated: true)
8047
8240
  }
8241
+
8242
+ #else
8243
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8244
+ #endif
8048
8245
  }
8049
8246
 
8050
8247
  // MARK: - Motion Sensors
8051
8248
  private func startMotionUpdates(interval: Double, callbackId: String?) {
8249
+ #if CRAFT_PERMISSION_MOTION
8052
8250
  guard let motionManager = motionManager, motionManager.isDeviceMotionAvailable else {
8053
8251
  rejectCallback(callbackId, error: "Motion sensors not available")
8054
8252
  return
@@ -8081,11 +8279,20 @@ struct CraftWebView: UIViewRepresentable {
8081
8279
 
8082
8280
  isMotionUpdating = true
8083
8281
  resolveCallback(callbackId, result: true)
8282
+
8283
+ #else
8284
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8285
+ #endif
8084
8286
  }
8085
8287
 
8086
8288
  private func stopMotionUpdates() {
8289
+ #if CRAFT_PERMISSION_MOTION
8087
8290
  motionManager?.stopDeviceMotionUpdates()
8088
8291
  isMotionUpdating = false
8292
+
8293
+ #else
8294
+
8295
+ #endif
8089
8296
  }
8090
8297
 
8091
8298
  // MARK: - Local Database (SQLite)
@@ -8197,19 +8404,30 @@ struct CraftWebView: UIViewRepresentable {
8197
8404
 
8198
8405
  // MARK: - Bluetooth
8199
8406
  private func startBluetoothScan(callbackId: String?) {
8407
+ #if CRAFT_PERMISSION_BLUETOOTH
8200
8408
  centralManager = CBCentralManager(delegate: self, queue: nil)
8201
8409
  pendingCallbackId = callbackId
8202
8410
  // Scanning starts in centralManagerDidUpdateState
8411
+
8412
+ #else
8413
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8414
+ #endif
8203
8415
  }
8204
8416
 
8205
8417
  private func stopBluetoothScan() {
8418
+ #if CRAFT_PERMISSION_BLUETOOTH
8206
8419
  centralManager?.stopScan()
8207
8420
  centralManager = nil
8208
8421
  discoveredPeripherals.removeAll()
8422
+
8423
+ #else
8424
+
8425
+ #endif
8209
8426
  }
8210
8427
 
8211
8428
  // MARK: - NFC
8212
8429
  private func scanNFC(callbackId: String?) {
8430
+ #if CRAFT_PERMISSION_NFC
8213
8431
  guard NFCNDEFReaderSession.readingAvailable else {
8214
8432
  rejectCallback(callbackId, error: "NFC not available")
8215
8433
  return
@@ -8219,6 +8437,10 @@ struct CraftWebView: UIViewRepresentable {
8219
8437
  let session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
8220
8438
  session.alertMessage = "Hold your iPhone near the NFC tag"
8221
8439
  session.begin()
8440
+
8441
+ #else
8442
+ rejectCallback(callbackId, error: "NFC was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8443
+ #endif
8222
8444
  }
8223
8445
 
8224
8446
  // MARK: - Health
@@ -8442,6 +8664,7 @@ struct CraftWebView: UIViewRepresentable {
8442
8664
 
8443
8665
  // MARK: - Contacts Picker
8444
8666
  private func pickContact(multiple: Bool, callbackId: String?) {
8667
+ #if CRAFT_PERMISSION_CONTACTS
8445
8668
  DispatchQueue.main.async {
8446
8669
  guard let rootVC = CraftPresenter.topViewController() else {
8447
8670
  self.rejectCallback(callbackId, error: "No root view controller")
@@ -8462,6 +8685,10 @@ struct CraftWebView: UIViewRepresentable {
8462
8685
 
8463
8686
  rootVC.present(picker, animated: true)
8464
8687
  }
8688
+
8689
+ #else
8690
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8691
+ #endif
8465
8692
  }
8466
8693
 
8467
8694
  // MARK: - App Shortcuts
@@ -8611,6 +8838,7 @@ struct CraftWebView: UIViewRepresentable {
8611
8838
  private var detectedPlanes: [UUID: ARPlaneAnchor] = [:]
8612
8839
 
8613
8840
  private func startAR(options: [String: Any], callbackId: String?) {
8841
+ #if CRAFT_PERMISSION_CAMERA
8614
8842
  guard ARWorldTrackingConfiguration.isSupported else {
8615
8843
  rejectCallback(callbackId, error: "AR not supported on this device")
8616
8844
  return
@@ -8642,9 +8870,14 @@ struct CraftWebView: UIViewRepresentable {
8642
8870
  arView.session.run(configuration)
8643
8871
  self.resolveCallback(callbackId, result: ["started": true])
8644
8872
  }
8873
+
8874
+ #else
8875
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8876
+ #endif
8645
8877
  }
8646
8878
 
8647
8879
  private func stopAR(callbackId: String?) {
8880
+ #if CRAFT_PERMISSION_CAMERA
8648
8881
  DispatchQueue.main.async { [weak self] in
8649
8882
  guard let self = self else { return }
8650
8883
 
@@ -8657,9 +8890,14 @@ struct CraftWebView: UIViewRepresentable {
8657
8890
 
8658
8891
  self.resolveCallback(callbackId, result: ["stopped": true])
8659
8892
  }
8893
+
8894
+ #else
8895
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8896
+ #endif
8660
8897
  }
8661
8898
 
8662
8899
  private func placeARObject(model: String, position: [String: Double]?, callbackId: String?) {
8900
+ #if CRAFT_PERMISSION_CAMERA
8663
8901
  DispatchQueue.main.async { [weak self] in
8664
8902
  guard let self = self, let arView = self.arView else {
8665
8903
  self?.rejectCallback(callbackId, error: "AR not started")
@@ -8723,9 +8961,14 @@ struct CraftWebView: UIViewRepresentable {
8723
8961
 
8724
8962
  self.resolveCallback(callbackId, result: ["objectId": objectId, "placed": true])
8725
8963
  }
8964
+
8965
+ #else
8966
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8967
+ #endif
8726
8968
  }
8727
8969
 
8728
8970
  private func removeARObject(objectId: String, callbackId: String?) {
8971
+ #if CRAFT_PERMISSION_CAMERA
8729
8972
  DispatchQueue.main.async { [weak self] in
8730
8973
  guard let self = self else { return }
8731
8974
 
@@ -8737,9 +8980,14 @@ struct CraftWebView: UIViewRepresentable {
8737
8980
  self.rejectCallback(callbackId, error: "Object not found")
8738
8981
  }
8739
8982
  }
8983
+
8984
+ #else
8985
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
8986
+ #endif
8740
8987
  }
8741
8988
 
8742
8989
  private func getARPlanes(callbackId: String?) {
8990
+ #if CRAFT_PERMISSION_CAMERA
8743
8991
  var planes: [[String: Any]] = []
8744
8992
  for (_, anchor) in detectedPlanes {
8745
8993
  planes.append([
@@ -8750,6 +8998,10 @@ struct CraftWebView: UIViewRepresentable {
8750
8998
  ])
8751
8999
  }
8752
9000
  resolveCallback(callbackId, result: planes)
9001
+
9002
+ #else
9003
+ rejectCallback(callbackId, error: "Capability was not enabled when this app was built", code: "CAPABILITY_DISABLED")
9004
+ #endif
8753
9005
  }
8754
9006
 
8755
9007
  // MARK: - ML (Core ML / Vision)
@@ -9142,6 +9394,7 @@ extension CraftWebView.Coordinator: UIDocumentPickerDelegate {
9142
9394
  }
9143
9395
 
9144
9396
  // MARK: - Bluetooth Delegate
9397
+ #if CRAFT_PERMISSION_BLUETOOTH
9145
9398
  extension CraftWebView.Coordinator: CBCentralManagerDelegate {
9146
9399
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
9147
9400
  if central.state == .poweredOn {
@@ -9164,8 +9417,10 @@ extension CraftWebView.Coordinator: CBCentralManagerDelegate {
9164
9417
  }
9165
9418
  }
9166
9419
  }
9420
+ #endif
9167
9421
 
9168
9422
  // MARK: - NFC Delegate
9423
+ #if CRAFT_PERMISSION_NFC
9169
9424
  extension CraftWebView.Coordinator: NFCNDEFReaderSessionDelegate {
9170
9425
  func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
9171
9426
  var records: [[String: Any]] = []
@@ -9195,6 +9450,7 @@ extension CraftWebView.Coordinator: NFCNDEFReaderSessionDelegate {
9195
9450
  pendingCallbackId = nil
9196
9451
  }
9197
9452
  }
9453
+ #endif
9198
9454
 
9199
9455
  // MARK: - ARSCNViewDelegate Extension
9200
9456
  extension CraftWebView.Coordinator {
@@ -9333,6 +9589,24 @@ extension CraftWebView.Coordinator: WKUIDelegate {
9333
9589
  /// thing reads as a web page. Anything embedded still gets the prompt.
9334
9590
  @available(iOS 15.0, *)
9335
9591
  func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
9592
+ #if CRAFT_PERMISSION_CAMERA
9593
+ let cameraEnabled = true
9594
+ #else
9595
+ let cameraEnabled = false
9596
+ #endif
9597
+ #if CRAFT_PERMISSION_MICROPHONE
9598
+ let microphoneEnabled = true
9599
+ #else
9600
+ let microphoneEnabled = false
9601
+ #endif
9602
+ let enabled: Bool
9603
+ switch type {
9604
+ case .camera: enabled = cameraEnabled
9605
+ case .microphone: enabled = microphoneEnabled
9606
+ case .cameraAndMicrophone: enabled = cameraEnabled && microphoneEnabled
9607
+ @unknown default: enabled = false
9608
+ }
9609
+ guard enabled else { decisionHandler(.deny); return }
9336
9610
  decisionHandler(trusts(origin) && trusts(frame.securityOrigin) ? .grant : .prompt)
9337
9611
  }
9338
9612
  }
@@ -9347,6 +9621,7 @@ extension CraftWebView.Coordinator: AVSpeechSynthesizerDelegate {
9347
9621
  }
9348
9622
  }
9349
9623
 
9624
+ #if CRAFT_PERMISSION_CONTACTS
9350
9625
  extension CraftWebView.Coordinator: CNContactPickerDelegate {
9351
9626
  func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
9352
9627
  let contactData = formatContact(contact)
@@ -9396,6 +9671,7 @@ extension CraftWebView.Coordinator: CNContactPickerDelegate {
9396
9671
  return data
9397
9672
  }
9398
9673
  }
9674
+ #endif
9399
9675
 
9400
9676
  // MARK: - Watch Connectivity Delegate
9401
9677
  extension CraftWebView.Coordinator: WCSessionDelegate {