craft-native 0.0.87 → 0.0.89
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/README.md +71 -0
- package/dist/api/window.d.ts +33 -0
- package/dist/cli.js +75 -3
- package/dist/index.cjs +384 -103
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +372 -91
- package/dist/ios/src/index.d.ts +74 -0
- package/dist/ios/src/index.js +73 -1
- package/dist/ios/templates/CraftApp.swift +510 -112
- package/dist/ios/templates/project.yml.template +1 -0
- package/dist/updater/index.d.ts +104 -1
- package/dist/updater/macos-bundle.d.ts +153 -0
- package/package.json +2 -2
|
@@ -338,6 +338,12 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
338
338
|
webView.navigationDelegate = context.coordinator
|
|
339
339
|
webView.isOpaque = false
|
|
340
340
|
|
|
341
|
+
// Before any message can be offered. Zig evaluates every reply against
|
|
342
|
+
// this webview, and without it an action would run, succeed, and reach
|
|
343
|
+
// `error.NoWebView` on the way back — the page hearing nothing while
|
|
344
|
+
// the work happened.
|
|
345
|
+
CraftZigRuntime.attach(webView)
|
|
346
|
+
|
|
341
347
|
// Register with DeepLinkManager
|
|
342
348
|
DeepLinkManager.shared.setWebView(webView)
|
|
343
349
|
|
|
@@ -363,6 +369,13 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
363
369
|
|
|
364
370
|
func updateUIView(_ webView: WKWebView, context: Context) {}
|
|
365
371
|
|
|
372
|
+
/// The teardown half of `CraftZigRuntime.attach`, called by SwiftUI when
|
|
373
|
+
/// this representable goes away. Without it Zig keeps an unretained
|
|
374
|
+
/// pointer to a deallocated webview and answers into freed memory.
|
|
375
|
+
static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
|
|
376
|
+
CraftZigRuntime.detach(uiView)
|
|
377
|
+
}
|
|
378
|
+
|
|
366
379
|
func makeCoordinator() -> Coordinator {
|
|
367
380
|
let coordinator = Coordinator(config: config)
|
|
368
381
|
// The Zig dispatcher finds CraftSwiftShim by class name; the shim finds
|
|
@@ -444,7 +457,12 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
444
457
|
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
|
445
458
|
locationManager?.activityType = .fitness
|
|
446
459
|
locationManager?.pausesLocationUpdatesAutomatically = false
|
|
447
|
-
|
|
460
|
+
// Zig owns the recorder when it is linked, including the
|
|
461
|
+
// relaunch restore — see `CraftZigRuntime.adoptLocationRecording`.
|
|
462
|
+
// Running both would leave two managers appending to one track.
|
|
463
|
+
if !CraftZigRuntime.adoptLocationRecording() {
|
|
464
|
+
restoreLocationRecordingState()
|
|
465
|
+
}
|
|
448
466
|
}
|
|
449
467
|
if config.enableContacts {
|
|
450
468
|
contactStore = CNContactStore()
|
|
@@ -516,7 +534,24 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
516
534
|
guard let body = message.body as? [String: Any],
|
|
517
535
|
let action = body["action"] as? String else { return }
|
|
518
536
|
|
|
519
|
-
|
|
537
|
+
let callbackId = body["callbackId"] as? String
|
|
538
|
+
|
|
539
|
+
// The Zig runtime first, when there is one.
|
|
540
|
+
//
|
|
541
|
+
// `offer` returns true when Zig has taken responsibility for the
|
|
542
|
+
// call — served it, or refused it and settled the page's promise
|
|
543
|
+
// on the way. Either way the answer is on its way and this method
|
|
544
|
+
// must not produce a second one. False means no Zig module
|
|
545
|
+
// recognised the action, which is every action still listed in the
|
|
546
|
+
// switch below and nothing else.
|
|
547
|
+
//
|
|
548
|
+
// In an app with no Zig runtime linked, `offer` is a `dlsym` miss
|
|
549
|
+
// that answers false for everything, and the switch serves the
|
|
550
|
+
// whole surface exactly as it did before. That is why this is one
|
|
551
|
+
// line and not a build flag.
|
|
552
|
+
if CraftZigRuntime.offer(action: action, body: body, callbackId: callbackId) { return }
|
|
553
|
+
|
|
554
|
+
dispatch(action: action, body: body, callbackId: callbackId)
|
|
520
555
|
}
|
|
521
556
|
|
|
522
557
|
/// The action switch, reachable from two directions: the
|
|
@@ -531,13 +566,19 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
531
566
|
func dispatch(action: String, body: [String: Any], callbackId: String?) {
|
|
532
567
|
switch action {
|
|
533
568
|
case "startListening":
|
|
534
|
-
if config.enableSpeechRecognition {
|
|
569
|
+
if config.enableSpeechRecognition {
|
|
570
|
+
startSpeechRecognition()
|
|
571
|
+
} else {
|
|
572
|
+
rejectCallback(callbackId, error: "Speech recognition is disabled", code: "CAPABILITY_DISABLED")
|
|
573
|
+
}
|
|
535
574
|
case "stopListening":
|
|
536
575
|
stopSpeechRecognition()
|
|
537
576
|
case "haptic":
|
|
538
577
|
if config.enableHaptics {
|
|
539
578
|
let style = body["style"] as? String ?? "medium"
|
|
540
579
|
triggerHaptic(style: style)
|
|
580
|
+
} else {
|
|
581
|
+
rejectCallback(callbackId, error: "Haptics is disabled", code: "CAPABILITY_DISABLED")
|
|
541
582
|
}
|
|
542
583
|
case "share":
|
|
543
584
|
if config.enableShare {
|
|
@@ -555,39 +596,62 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
555
596
|
if config.enableCamera {
|
|
556
597
|
pendingCallbackId = callbackId
|
|
557
598
|
openCamera()
|
|
599
|
+
} else {
|
|
600
|
+
rejectCallback(callbackId, error: "Camera is disabled", code: "CAPABILITY_DISABLED")
|
|
558
601
|
}
|
|
559
602
|
case "pickImage":
|
|
560
603
|
if config.enableCamera {
|
|
561
604
|
pendingCallbackId = callbackId
|
|
562
605
|
pickImage()
|
|
606
|
+
} else {
|
|
607
|
+
rejectCallback(callbackId, error: "Camera is disabled", code: "CAPABILITY_DISABLED")
|
|
563
608
|
}
|
|
564
609
|
case "authenticate":
|
|
565
610
|
if config.enableBiometric {
|
|
566
611
|
let reason = body["reason"] as? String ?? "Authenticate to continue"
|
|
567
612
|
authenticate(reason: reason, callbackId: callbackId)
|
|
613
|
+
} else {
|
|
614
|
+
rejectCallback(callbackId, error: "Biometric authentication is disabled", code: "CAPABILITY_DISABLED")
|
|
568
615
|
}
|
|
569
616
|
case "registerPush":
|
|
570
617
|
if config.enablePushNotifications {
|
|
571
618
|
registerPushNotifications(callbackId: callbackId)
|
|
619
|
+
} else {
|
|
620
|
+
rejectCallback(callbackId, error: "Push notifications are disabled", code: "CAPABILITY_DISABLED")
|
|
572
621
|
}
|
|
573
622
|
case "secureSet":
|
|
574
|
-
if config.enableSecureStorage
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
623
|
+
if config.enableSecureStorage {
|
|
624
|
+
if let key = body["key"] as? String,
|
|
625
|
+
let value = body["value"] as? String {
|
|
626
|
+
let success = secureStore(key: key, value: value)
|
|
627
|
+
resolveCallback(callbackId, result: success)
|
|
628
|
+
} else {
|
|
629
|
+
rejectCallback(callbackId, error: "secureSet was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
|
|
579
633
|
}
|
|
580
634
|
case "secureGet":
|
|
581
|
-
if config.enableSecureStorage
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
635
|
+
if config.enableSecureStorage {
|
|
636
|
+
if let key = body["key"] as? String {
|
|
637
|
+
let value = secureRetrieve(key: key)
|
|
638
|
+
resolveCallback(callbackId, result: value as Any)
|
|
639
|
+
} else {
|
|
640
|
+
rejectCallback(callbackId, error: "secureGet was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
|
|
585
644
|
}
|
|
586
645
|
case "secureRemove":
|
|
587
|
-
if config.enableSecureStorage
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
646
|
+
if config.enableSecureStorage {
|
|
647
|
+
if let key = body["key"] as? String {
|
|
648
|
+
let success = secureRemove(key: key)
|
|
649
|
+
resolveCallback(callbackId, result: success)
|
|
650
|
+
} else {
|
|
651
|
+
rejectCallback(callbackId, error: "secureRemove was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
|
|
591
655
|
}
|
|
592
656
|
case "secureClear":
|
|
593
657
|
if config.enableSecureStorage {
|
|
@@ -650,14 +714,22 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
650
714
|
readLocationRecording(callbackId: callbackId)
|
|
651
715
|
// Clipboard
|
|
652
716
|
case "clipboardWrite":
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
717
|
+
if config.enableClipboard {
|
|
718
|
+
if let text = body["text"] as? String {
|
|
719
|
+
UIPasteboard.general.string = text
|
|
720
|
+
resolveCallback(callbackId, result: true)
|
|
721
|
+
} else {
|
|
722
|
+
rejectCallback(callbackId, error: "clipboardWrite was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
rejectCallback(callbackId, error: "Clipboard is disabled", code: "CAPABILITY_DISABLED")
|
|
726
|
+
}
|
|
657
727
|
case "clipboardRead":
|
|
658
728
|
if config.enableClipboard {
|
|
659
729
|
let text = UIPasteboard.general.string ?? ""
|
|
660
730
|
resolveCallback(callbackId, result: text)
|
|
731
|
+
} else {
|
|
732
|
+
rejectCallback(callbackId, error: "Clipboard is disabled", code: "CAPABILITY_DISABLED")
|
|
661
733
|
}
|
|
662
734
|
// Device Info
|
|
663
735
|
case "getDeviceInfo":
|
|
@@ -705,49 +777,80 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
705
777
|
case "getContacts":
|
|
706
778
|
if config.enableContacts {
|
|
707
779
|
getContacts(callbackId: callbackId)
|
|
780
|
+
} else {
|
|
781
|
+
rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
|
|
708
782
|
}
|
|
709
783
|
case "addContact":
|
|
710
|
-
if config.enableContacts
|
|
711
|
-
|
|
712
|
-
|
|
784
|
+
if config.enableContacts {
|
|
785
|
+
if let contactData = body["contact"] as? [String: Any] {
|
|
786
|
+
addContact(contactData, callbackId: callbackId)
|
|
787
|
+
} else {
|
|
788
|
+
rejectCallback(callbackId, error: "addContact was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
|
|
713
792
|
}
|
|
714
|
-
|
|
715
793
|
// MARK: - Calendar
|
|
716
794
|
case "getCalendarEvents":
|
|
717
795
|
if config.enableCalendar {
|
|
718
796
|
let startDate = body["startDate"] as? Double
|
|
719
797
|
let endDate = body["endDate"] as? Double
|
|
720
798
|
getCalendarEvents(startDate: startDate, endDate: endDate, callbackId: callbackId)
|
|
799
|
+
} else {
|
|
800
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
721
801
|
}
|
|
722
802
|
case "createCalendarEvent":
|
|
723
|
-
if config.enableCalendar
|
|
724
|
-
|
|
725
|
-
|
|
803
|
+
if config.enableCalendar {
|
|
804
|
+
if let eventData = body["event"] as? [String: Any] {
|
|
805
|
+
createCalendarEvent(eventData, callbackId: callbackId)
|
|
806
|
+
} else {
|
|
807
|
+
rejectCallback(callbackId, error: "createCalendarEvent was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
726
811
|
}
|
|
727
812
|
case "deleteCalendarEvent":
|
|
728
|
-
if config.enableCalendar
|
|
729
|
-
|
|
730
|
-
|
|
813
|
+
if config.enableCalendar {
|
|
814
|
+
if let eventId = body["eventId"] as? String {
|
|
815
|
+
deleteCalendarEvent(eventId, callbackId: callbackId)
|
|
816
|
+
} else {
|
|
817
|
+
rejectCallback(callbackId, error: "deleteCalendarEvent was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
731
821
|
}
|
|
732
|
-
|
|
733
822
|
// MARK: - Local Notifications
|
|
734
823
|
case "scheduleNotification":
|
|
735
|
-
if config.enableLocalNotifications
|
|
736
|
-
|
|
737
|
-
|
|
824
|
+
if config.enableLocalNotifications {
|
|
825
|
+
if let notifData = body["notification"] as? [String: Any] {
|
|
826
|
+
scheduleLocalNotification(notifData, callbackId: callbackId)
|
|
827
|
+
} else {
|
|
828
|
+
rejectCallback(callbackId, error: "scheduleNotification was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
|
|
738
832
|
}
|
|
739
833
|
case "cancelNotification":
|
|
740
|
-
if config.enableLocalNotifications
|
|
741
|
-
|
|
742
|
-
|
|
834
|
+
if config.enableLocalNotifications {
|
|
835
|
+
if let notifId = body["id"] as? String {
|
|
836
|
+
cancelLocalNotification(notifId, callbackId: callbackId)
|
|
837
|
+
} else {
|
|
838
|
+
rejectCallback(callbackId, error: "cancelNotification was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
|
|
743
842
|
}
|
|
744
843
|
case "cancelAllNotifications":
|
|
745
844
|
if config.enableLocalNotifications {
|
|
746
845
|
cancelAllLocalNotifications(callbackId: callbackId)
|
|
846
|
+
} else {
|
|
847
|
+
rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
|
|
747
848
|
}
|
|
748
849
|
case "getPendingNotifications":
|
|
749
850
|
if config.enableLocalNotifications {
|
|
750
851
|
getPendingNotifications(callbackId: callbackId)
|
|
852
|
+
} else {
|
|
853
|
+
rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
|
|
751
854
|
}
|
|
752
855
|
|
|
753
856
|
// MARK: - Deep Links
|
|
@@ -755,6 +858,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
755
858
|
if config.enableDeepLinks {
|
|
756
859
|
// Handler is registered in JS
|
|
757
860
|
resolveCallback(callbackId, result: true)
|
|
861
|
+
} else {
|
|
862
|
+
rejectCallback(callbackId, error: "Deep links are disabled", code: "CAPABILITY_DISABLED")
|
|
758
863
|
}
|
|
759
864
|
case "getInitialURL":
|
|
760
865
|
if config.enableDeepLinks {
|
|
@@ -769,46 +874,73 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
769
874
|
} else {
|
|
770
875
|
resolveCallback(callbackId, result: NSNull())
|
|
771
876
|
}
|
|
877
|
+
} else {
|
|
878
|
+
rejectCallback(callbackId, error: "Deep links are disabled", code: "CAPABILITY_DISABLED")
|
|
772
879
|
}
|
|
773
880
|
|
|
774
881
|
// MARK: - In-App Purchase
|
|
775
882
|
case "getProducts":
|
|
776
|
-
if config.enableInAppPurchase
|
|
777
|
-
|
|
778
|
-
|
|
883
|
+
if config.enableInAppPurchase {
|
|
884
|
+
if let productIds = body["productIds"] as? [String] {
|
|
885
|
+
getProducts(productIds, callbackId: callbackId)
|
|
886
|
+
} else {
|
|
887
|
+
rejectCallback(callbackId, error: "getProducts was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
rejectCallback(callbackId, error: "In-app purchase is disabled", code: "CAPABILITY_DISABLED")
|
|
779
891
|
}
|
|
780
892
|
case "purchase":
|
|
781
|
-
if config.enableInAppPurchase
|
|
782
|
-
|
|
783
|
-
|
|
893
|
+
if config.enableInAppPurchase {
|
|
894
|
+
if let productId = body["productId"] as? String {
|
|
895
|
+
purchaseProduct(productId, callbackId: callbackId)
|
|
896
|
+
} else {
|
|
897
|
+
rejectCallback(callbackId, error: "purchase was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
rejectCallback(callbackId, error: "In-app purchase is disabled", code: "CAPABILITY_DISABLED")
|
|
784
901
|
}
|
|
785
902
|
case "restorePurchases":
|
|
786
903
|
if config.enableInAppPurchase {
|
|
787
904
|
restorePurchases(callbackId: callbackId)
|
|
905
|
+
} else {
|
|
906
|
+
rejectCallback(callbackId, error: "In-app purchase is disabled", code: "CAPABILITY_DISABLED")
|
|
788
907
|
}
|
|
789
908
|
|
|
790
909
|
// MARK: - Keep Awake
|
|
791
910
|
case "setKeepAwake":
|
|
792
|
-
if config.enableKeepAwake
|
|
793
|
-
|
|
794
|
-
|
|
911
|
+
if config.enableKeepAwake {
|
|
912
|
+
if let enabled = body["enabled"] as? Bool {
|
|
913
|
+
setKeepAwake(enabled, callbackId: callbackId)
|
|
914
|
+
} else {
|
|
915
|
+
rejectCallback(callbackId, error: "setKeepAwake was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
rejectCallback(callbackId, error: "Keep awake is disabled", code: "CAPABILITY_DISABLED")
|
|
795
919
|
}
|
|
796
|
-
|
|
797
920
|
// MARK: - Orientation Lock
|
|
798
921
|
case "lockOrientation":
|
|
799
|
-
if config.enableOrientationLock
|
|
800
|
-
|
|
801
|
-
|
|
922
|
+
if config.enableOrientationLock {
|
|
923
|
+
if let orientation = body["orientation"] as? String {
|
|
924
|
+
lockOrientation(orientation, callbackId: callbackId)
|
|
925
|
+
} else {
|
|
926
|
+
rejectCallback(callbackId, error: "lockOrientation was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
rejectCallback(callbackId, error: "Orientation lock is disabled", code: "CAPABILITY_DISABLED")
|
|
802
930
|
}
|
|
803
931
|
case "unlockOrientation":
|
|
804
932
|
if config.enableOrientationLock {
|
|
805
933
|
unlockOrientation(callbackId: callbackId)
|
|
934
|
+
} else {
|
|
935
|
+
rejectCallback(callbackId, error: "Orientation lock is disabled", code: "CAPABILITY_DISABLED")
|
|
806
936
|
}
|
|
807
937
|
|
|
808
938
|
// MARK: - QR/Barcode Scanner
|
|
809
939
|
case "scanQRCode":
|
|
810
940
|
if config.enableQRScanner {
|
|
811
941
|
scanQRCode(callbackId: callbackId)
|
|
942
|
+
} else {
|
|
943
|
+
rejectCallback(callbackId, error: "QR scanning is disabled", code: "CAPABILITY_DISABLED")
|
|
812
944
|
}
|
|
813
945
|
|
|
814
946
|
// MARK: - File Picker
|
|
@@ -816,42 +948,59 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
816
948
|
if config.enableFilePicker {
|
|
817
949
|
let types = body["types"] as? [String]
|
|
818
950
|
pickFile(types: types, callbackId: callbackId)
|
|
951
|
+
} else {
|
|
952
|
+
rejectCallback(callbackId, error: "File picker is disabled", code: "CAPABILITY_DISABLED")
|
|
819
953
|
}
|
|
820
954
|
|
|
821
955
|
// MARK: - File Download
|
|
822
956
|
case "downloadFile":
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
957
|
+
if config.enableFileDownload {
|
|
958
|
+
if let url = body["url"] as? String, let filename = body["filename"] as? String {
|
|
959
|
+
downloadFile(url: url, filename: filename, callbackId: callbackId)
|
|
960
|
+
} else {
|
|
961
|
+
rejectCallback(callbackId, error: "downloadFile was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
rejectCallback(callbackId, error: "File download is disabled", code: "CAPABILITY_DISABLED")
|
|
965
|
+
}
|
|
828
966
|
case "saveFile":
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
967
|
+
if config.enableFileDownload {
|
|
968
|
+
if let data = body["data"] as? String, let filename = body["filename"] as? String {
|
|
969
|
+
saveFile(data: data, filename: filename, callbackId: callbackId)
|
|
970
|
+
} else {
|
|
971
|
+
rejectCallback(callbackId, error: "saveFile was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
rejectCallback(callbackId, error: "File download is disabled", code: "CAPABILITY_DISABLED")
|
|
975
|
+
}
|
|
835
976
|
// MARK: - Social Auth
|
|
836
977
|
case "signInWithApple":
|
|
837
978
|
if config.enableSocialAuth {
|
|
838
979
|
signInWithApple(callbackId: callbackId)
|
|
980
|
+
} else {
|
|
981
|
+
rejectCallback(callbackId, error: "Social sign-in is disabled", code: "CAPABILITY_DISABLED")
|
|
839
982
|
}
|
|
840
983
|
|
|
841
984
|
// MARK: - Audio Recording
|
|
842
985
|
case "startAudioRecording":
|
|
843
986
|
if config.enableAudioRecording {
|
|
844
987
|
startAudioRecording(callbackId: callbackId)
|
|
988
|
+
} else {
|
|
989
|
+
rejectCallback(callbackId, error: "Audio recording is disabled", code: "CAPABILITY_DISABLED")
|
|
845
990
|
}
|
|
846
991
|
case "stopAudioRecording":
|
|
847
992
|
if config.enableAudioRecording {
|
|
848
993
|
stopAudioRecording(callbackId: callbackId)
|
|
994
|
+
} else {
|
|
995
|
+
rejectCallback(callbackId, error: "Audio recording is disabled", code: "CAPABILITY_DISABLED")
|
|
849
996
|
}
|
|
850
997
|
|
|
851
998
|
// MARK: - Video Recording
|
|
852
999
|
case "startVideoRecording":
|
|
853
1000
|
if config.enableVideoRecording {
|
|
854
1001
|
startVideoRecording(callbackId: callbackId)
|
|
1002
|
+
} else {
|
|
1003
|
+
rejectCallback(callbackId, error: "Video recording is disabled", code: "CAPABILITY_DISABLED")
|
|
855
1004
|
}
|
|
856
1005
|
|
|
857
1006
|
// MARK: - Motion Sensors
|
|
@@ -859,6 +1008,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
859
1008
|
if config.enableMotionSensors {
|
|
860
1009
|
let interval = body["interval"] as? Double ?? 100
|
|
861
1010
|
startMotionUpdates(interval: interval, callbackId: callbackId)
|
|
1011
|
+
} else {
|
|
1012
|
+
rejectCallback(callbackId, error: "Motion sensors are disabled", code: "CAPABILITY_DISABLED")
|
|
862
1013
|
}
|
|
863
1014
|
case "stopMotionUpdates":
|
|
864
1015
|
stopMotionUpdates()
|
|
@@ -866,22 +1017,33 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
866
1017
|
|
|
867
1018
|
// MARK: - Local Database
|
|
868
1019
|
case "dbExecute":
|
|
869
|
-
if config.enableLocalDatabase
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1020
|
+
if config.enableLocalDatabase {
|
|
1021
|
+
if let sql = body["sql"] as? String {
|
|
1022
|
+
let params = body["params"] as? [Any]
|
|
1023
|
+
dbExecute(sql: sql, params: params, callbackId: callbackId)
|
|
1024
|
+
} else {
|
|
1025
|
+
rejectCallback(callbackId, error: "dbExecute was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1026
|
+
}
|
|
1027
|
+
} else {
|
|
1028
|
+
rejectCallback(callbackId, error: "Local database is disabled", code: "CAPABILITY_DISABLED")
|
|
873
1029
|
}
|
|
874
1030
|
case "dbQuery":
|
|
875
|
-
if config.enableLocalDatabase
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1031
|
+
if config.enableLocalDatabase {
|
|
1032
|
+
if let sql = body["sql"] as? String {
|
|
1033
|
+
let params = body["params"] as? [Any]
|
|
1034
|
+
dbQuery(sql: sql, params: params, callbackId: callbackId)
|
|
1035
|
+
} else {
|
|
1036
|
+
rejectCallback(callbackId, error: "dbQuery was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1037
|
+
}
|
|
1038
|
+
} else {
|
|
1039
|
+
rejectCallback(callbackId, error: "Local database is disabled", code: "CAPABILITY_DISABLED")
|
|
879
1040
|
}
|
|
880
|
-
|
|
881
1041
|
// MARK: - Bluetooth
|
|
882
1042
|
case "startBluetoothScan":
|
|
883
1043
|
if config.enableBluetooth {
|
|
884
1044
|
startBluetoothScan(callbackId: callbackId)
|
|
1045
|
+
} else {
|
|
1046
|
+
rejectCallback(callbackId, error: "Bluetooth is disabled", code: "CAPABILITY_DISABLED")
|
|
885
1047
|
}
|
|
886
1048
|
case "stopBluetoothScan":
|
|
887
1049
|
stopBluetoothScan()
|
|
@@ -891,6 +1053,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
891
1053
|
case "scanNFC":
|
|
892
1054
|
if config.enableNFC {
|
|
893
1055
|
scanNFC(callbackId: callbackId)
|
|
1056
|
+
} else {
|
|
1057
|
+
rejectCallback(callbackId, error: "NFC is disabled", code: "CAPABILITY_DISABLED")
|
|
894
1058
|
}
|
|
895
1059
|
|
|
896
1060
|
// MARK: - Health
|
|
@@ -898,13 +1062,20 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
898
1062
|
if config.enableHealthKit {
|
|
899
1063
|
let types = body["types"] as? [String] ?? []
|
|
900
1064
|
requestHealthAuthorization(types: types, callbackId: callbackId)
|
|
1065
|
+
} else {
|
|
1066
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
901
1067
|
}
|
|
902
1068
|
case "getHealthData":
|
|
903
|
-
if config.enableHealthKit
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1069
|
+
if config.enableHealthKit {
|
|
1070
|
+
if let dataType = body["type"] as? String {
|
|
1071
|
+
let startDate = body["startDate"] as? Double
|
|
1072
|
+
let endDate = body["endDate"] as? Double
|
|
1073
|
+
getHealthData(type: dataType, startDate: startDate, endDate: endDate, callbackId: callbackId)
|
|
1074
|
+
} else {
|
|
1075
|
+
rejectCallback(callbackId, error: "getHealthData was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
908
1079
|
}
|
|
909
1080
|
case "saveHealthWorkout":
|
|
910
1081
|
if config.enableHealthKit {
|
|
@@ -929,38 +1100,62 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
929
1100
|
case "takeScreenshot":
|
|
930
1101
|
if config.enableScreenCapture {
|
|
931
1102
|
takeScreenshot(callbackId: callbackId)
|
|
1103
|
+
} else {
|
|
1104
|
+
rejectCallback(callbackId, error: "Screen capture is disabled", code: "CAPABILITY_DISABLED")
|
|
932
1105
|
}
|
|
933
1106
|
|
|
934
1107
|
// MARK: - Background Tasks
|
|
935
1108
|
case "registerBackgroundTask":
|
|
936
|
-
if config.enableBackgroundTasks
|
|
937
|
-
|
|
938
|
-
|
|
1109
|
+
if config.enableBackgroundTasks {
|
|
1110
|
+
if let taskId = body["taskId"] as? String {
|
|
1111
|
+
registerBackgroundTask(taskId: taskId, callbackId: callbackId)
|
|
1112
|
+
} else {
|
|
1113
|
+
rejectCallback(callbackId, error: "registerBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
|
|
939
1117
|
}
|
|
940
1118
|
case "scheduleBackgroundTask":
|
|
941
|
-
if config.enableBackgroundTasks
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1119
|
+
if config.enableBackgroundTasks {
|
|
1120
|
+
if let taskId = body["taskId"] as? String {
|
|
1121
|
+
let delay = body["delay"] as? Double ?? 900 // 15 minutes default
|
|
1122
|
+
let requiresNetwork = body["requiresNetwork"] as? Bool ?? false
|
|
1123
|
+
let requiresCharging = body["requiresCharging"] as? Bool ?? false
|
|
1124
|
+
scheduleBackgroundTask(taskId: taskId, delay: delay, requiresNetwork: requiresNetwork, requiresCharging: requiresCharging, callbackId: callbackId)
|
|
1125
|
+
} else {
|
|
1126
|
+
rejectCallback(callbackId, error: "scheduleBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
|
|
947
1130
|
}
|
|
948
1131
|
case "cancelBackgroundTask":
|
|
949
|
-
if config.enableBackgroundTasks
|
|
950
|
-
|
|
951
|
-
|
|
1132
|
+
if config.enableBackgroundTasks {
|
|
1133
|
+
if let taskId = body["taskId"] as? String {
|
|
1134
|
+
cancelBackgroundTask(taskId: taskId, callbackId: callbackId)
|
|
1135
|
+
} else {
|
|
1136
|
+
rejectCallback(callbackId, error: "cancelBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
|
|
952
1140
|
}
|
|
953
1141
|
case "cancelAllBackgroundTasks":
|
|
954
1142
|
if config.enableBackgroundTasks {
|
|
955
1143
|
cancelAllBackgroundTasks(callbackId: callbackId)
|
|
1144
|
+
} else {
|
|
1145
|
+
rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
|
|
956
1146
|
}
|
|
957
1147
|
|
|
958
1148
|
// MARK: - PDF Viewer
|
|
959
1149
|
case "openPDF":
|
|
960
|
-
if config.enablePDFViewer
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1150
|
+
if config.enablePDFViewer {
|
|
1151
|
+
if let source = body["source"] as? String {
|
|
1152
|
+
let page = body["page"] as? Int ?? 0
|
|
1153
|
+
openPDF(source: source, page: page, callbackId: callbackId)
|
|
1154
|
+
} else {
|
|
1155
|
+
rejectCallback(callbackId, error: "openPDF was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
rejectCallback(callbackId, error: "PDF viewing is disabled", code: "CAPABILITY_DISABLED")
|
|
964
1159
|
}
|
|
965
1160
|
case "closePDF":
|
|
966
1161
|
closePDF(callbackId: callbackId)
|
|
@@ -970,6 +1165,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
970
1165
|
if config.enableContacts {
|
|
971
1166
|
let multiple = body["multiple"] as? Bool ?? false
|
|
972
1167
|
pickContact(multiple: multiple, callbackId: callbackId)
|
|
1168
|
+
} else {
|
|
1169
|
+
rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
|
|
973
1170
|
}
|
|
974
1171
|
|
|
975
1172
|
// MARK: - App Shortcuts
|
|
@@ -1014,44 +1211,74 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1014
1211
|
if config.enableAR {
|
|
1015
1212
|
let options = body["options"] as? [String: Any] ?? [:]
|
|
1016
1213
|
startAR(options: options, callbackId: callbackId)
|
|
1214
|
+
} else {
|
|
1215
|
+
rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
|
|
1017
1216
|
}
|
|
1018
1217
|
case "stopAR":
|
|
1019
1218
|
if config.enableAR {
|
|
1020
1219
|
stopAR(callbackId: callbackId)
|
|
1220
|
+
} else {
|
|
1221
|
+
rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
|
|
1021
1222
|
}
|
|
1022
1223
|
case "placeARObject":
|
|
1023
|
-
if config.enableAR
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1224
|
+
if config.enableAR {
|
|
1225
|
+
if let model = body["model"] as? String {
|
|
1226
|
+
let position = body["position"] as? [String: Double]
|
|
1227
|
+
placeARObject(model: model, position: position, callbackId: callbackId)
|
|
1228
|
+
} else {
|
|
1229
|
+
rejectCallback(callbackId, error: "placeARObject was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1230
|
+
}
|
|
1231
|
+
} else {
|
|
1232
|
+
rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
|
|
1027
1233
|
}
|
|
1028
1234
|
case "removeARObject":
|
|
1029
|
-
if config.enableAR
|
|
1030
|
-
|
|
1031
|
-
|
|
1235
|
+
if config.enableAR {
|
|
1236
|
+
if let objectId = body["objectId"] as? String {
|
|
1237
|
+
removeARObject(objectId: objectId, callbackId: callbackId)
|
|
1238
|
+
} else {
|
|
1239
|
+
rejectCallback(callbackId, error: "removeARObject was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
|
|
1032
1243
|
}
|
|
1033
1244
|
case "getARPlanes":
|
|
1034
1245
|
if config.enableAR {
|
|
1035
1246
|
getARPlanes(callbackId: callbackId)
|
|
1247
|
+
} else {
|
|
1248
|
+
rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
|
|
1036
1249
|
}
|
|
1037
1250
|
|
|
1038
1251
|
// MARK: - ML (Core ML / Vision)
|
|
1039
1252
|
case "classifyImage":
|
|
1040
|
-
if config.enableMLKit
|
|
1041
|
-
|
|
1042
|
-
|
|
1253
|
+
if config.enableMLKit {
|
|
1254
|
+
if let imageBase64 = body["image"] as? String {
|
|
1255
|
+
classifyImage(imageBase64: imageBase64, callbackId: callbackId)
|
|
1256
|
+
} else {
|
|
1257
|
+
rejectCallback(callbackId, error: "classifyImage was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
|
|
1043
1261
|
}
|
|
1044
1262
|
case "detectObjects":
|
|
1045
|
-
if config.enableMLKit
|
|
1046
|
-
|
|
1047
|
-
|
|
1263
|
+
if config.enableMLKit {
|
|
1264
|
+
if let imageBase64 = body["image"] as? String {
|
|
1265
|
+
detectObjects(imageBase64: imageBase64, callbackId: callbackId)
|
|
1266
|
+
} else {
|
|
1267
|
+
rejectCallback(callbackId, error: "detectObjects was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1268
|
+
}
|
|
1269
|
+
} else {
|
|
1270
|
+
rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
|
|
1048
1271
|
}
|
|
1049
1272
|
case "recognizeText":
|
|
1050
|
-
if config.enableMLKit
|
|
1051
|
-
|
|
1052
|
-
|
|
1273
|
+
if config.enableMLKit {
|
|
1274
|
+
if let imageBase64 = body["image"] as? String {
|
|
1275
|
+
recognizeText(imageBase64: imageBase64, callbackId: callbackId)
|
|
1276
|
+
} else {
|
|
1277
|
+
rejectCallback(callbackId, error: "recognizeText was called without the values it needs", code: "INVALID_ARGUMENT")
|
|
1278
|
+
}
|
|
1279
|
+
} else {
|
|
1280
|
+
rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
|
|
1053
1281
|
}
|
|
1054
|
-
|
|
1055
1282
|
// MARK: - Widget
|
|
1056
1283
|
case "updateWidget":
|
|
1057
1284
|
if let data = body["data"] as? [String: Any] {
|
|
@@ -1063,11 +1290,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1063
1290
|
// MARK: - Siri Shortcuts
|
|
1064
1291
|
case "registerSiriShortcut":
|
|
1065
1292
|
if let phrase = body["phrase"] as? String,
|
|
1066
|
-
let action = body["
|
|
1293
|
+
let action = body["shortcutAction"] as? String {
|
|
1067
1294
|
registerSiriShortcut(phrase: phrase, action: action, callbackId: callbackId)
|
|
1068
1295
|
}
|
|
1069
1296
|
case "removeSiriShortcut":
|
|
1070
|
-
if let action = body["
|
|
1297
|
+
if let action = body["shortcutAction"] as? String {
|
|
1071
1298
|
removeSiriShortcut(action: action, callbackId: callbackId)
|
|
1072
1299
|
}
|
|
1073
1300
|
|
|
@@ -2164,7 +2391,15 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2164
2391
|
register: function(phrase, action) {
|
|
2165
2392
|
var self = window.craft;
|
|
2166
2393
|
var id = 'cb_' + (++self._callbackId);
|
|
2167
|
-
|
|
2394
|
+
// `shortcutAction`, not `action`. This object had two
|
|
2395
|
+
// keys called `action` — the message's own, and the
|
|
2396
|
+
// shortcut's — and the later one wins, so every call
|
|
2397
|
+
// arrived labelled with the shortcut's identifier
|
|
2398
|
+
// instead of 'registerSiriShortcut'. No switch arm
|
|
2399
|
+
// matched, nothing answered, and these wrappers park in
|
|
2400
|
+
// `_callbacks` with no timeout: the promise never
|
|
2401
|
+
// settled at all.
|
|
2402
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'registerSiriShortcut', phrase: phrase, shortcutAction: action, callbackId: id});
|
|
2168
2403
|
return new Promise(function(resolve, reject) {
|
|
2169
2404
|
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2170
2405
|
});
|
|
@@ -2172,7 +2407,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2172
2407
|
remove: function(action) {
|
|
2173
2408
|
var self = window.craft;
|
|
2174
2409
|
var id = 'cb_' + (++self._callbackId);
|
|
2175
|
-
|
|
2410
|
+
// The same duplicate key as `register` above.
|
|
2411
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'removeSiriShortcut', shortcutAction: action, callbackId: id});
|
|
2176
2412
|
return new Promise(function(resolve, reject) {
|
|
2177
2413
|
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2178
2414
|
});
|
|
@@ -2421,6 +2657,27 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2421
2657
|
}
|
|
2422
2658
|
})(window.craft);
|
|
2423
2659
|
|
|
2660
|
+
// The reply route for actions the Zig runtime serves.
|
|
2661
|
+
//
|
|
2662
|
+
// Zig owns one wire format and calls these two functions by name;
|
|
2663
|
+
// this page owns `_callbacks`, keyed by the 'cb_<n>' ids `_invoke`
|
|
2664
|
+
// hands out. The whole adaptation is turning the numeric id Zig
|
|
2665
|
+
// carries back into that key. Nothing else is translated, because
|
|
2666
|
+
// nothing else differs: `craftSpeechStart` and friends already
|
|
2667
|
+
// arrive as plain CustomEvents from both sides.
|
|
2668
|
+
//
|
|
2669
|
+
// A null id means the page sent no callback — the tray-style
|
|
2670
|
+
// fire-and-forget posts — so there is nothing to settle and
|
|
2671
|
+
// dropping it is correct rather than lossy.
|
|
2672
|
+
window.__craftBridgeResult = function (action, result, id) {
|
|
2673
|
+
if (id === null || id === undefined) return;
|
|
2674
|
+
window.craft._resolveCallback('cb_' + id, result);
|
|
2675
|
+
};
|
|
2676
|
+
window.__craftBridgeError = function (ctx) {
|
|
2677
|
+
if (!ctx || ctx.id === null || ctx.id === undefined) return;
|
|
2678
|
+
window.craft._rejectCallback('cb_' + ctx.id, ctx.message, ctx.code);
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2424
2681
|
// Dispatch ready event
|
|
2425
2682
|
window.dispatchEvent(new CustomEvent('craftReady', {detail: window.craft}));
|
|
2426
2683
|
console.log('Craft iOS bridge initialized');
|
|
@@ -5193,6 +5450,147 @@ extension CraftWebView.Coordinator: WCSessionDelegate {
|
|
|
5193
5450
|
|
|
5194
5451
|
// MARK: - Zig hand-off shim
|
|
5195
5452
|
|
|
5453
|
+
/// The seam the page's messages reach the Zig runtime through.
|
|
5454
|
+
///
|
|
5455
|
+
/// The opposite direction from `CraftSwiftShim` below, and the two together
|
|
5456
|
+
/// are the whole hand-off: this class offers each message to Zig, Zig serves
|
|
5457
|
+
/// what it has migrated and declines the rest, and for anything Zig *did*
|
|
5458
|
+
/// take that needs Swift work, `CraftSwiftShim` is how it comes back.
|
|
5459
|
+
///
|
|
5460
|
+
/// Discovery is `dlsym`, exactly as the shim's is, and for the same reason:
|
|
5461
|
+
/// neither side links the other. An app built without the Zig static library
|
|
5462
|
+
/// finds nothing here, `offer` answers false for every action, and the
|
|
5463
|
+
/// coordinator's switch serves the entire surface as it always has. There is
|
|
5464
|
+
/// no build flag and no second code path — the absence of a symbol is the
|
|
5465
|
+
/// off switch.
|
|
5466
|
+
@objc(CraftZigRuntime)
|
|
5467
|
+
final class CraftZigRuntime: NSObject {
|
|
5468
|
+
private typealias SetWebViewFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
|
|
5469
|
+
private typealias ClearWebViewFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
|
|
5470
|
+
private typealias HandleActionFn = @convention(c) (
|
|
5471
|
+
UnsafePointer<CChar>, UInt, UnsafePointer<CChar>, UInt, Int64
|
|
5472
|
+
) -> Bool
|
|
5473
|
+
private typealias AdoptLocationRecordingFn = @convention(c) () -> Bool
|
|
5474
|
+
|
|
5475
|
+
private static let image: UnsafeMutableRawPointer? = dlopen(nil, RTLD_NOW)
|
|
5476
|
+
|
|
5477
|
+
private static let setWebViewFn: SetWebViewFn? = {
|
|
5478
|
+
guard let sym = dlsym(image, "craft_ios_set_webview") else { return nil }
|
|
5479
|
+
return unsafeBitCast(sym, to: SetWebViewFn.self)
|
|
5480
|
+
}()
|
|
5481
|
+
|
|
5482
|
+
private static let clearWebViewFn: ClearWebViewFn? = {
|
|
5483
|
+
guard let sym = dlsym(image, "craft_ios_clear_webview") else { return nil }
|
|
5484
|
+
return unsafeBitCast(sym, to: ClearWebViewFn.self)
|
|
5485
|
+
}()
|
|
5486
|
+
|
|
5487
|
+
private static let handleActionFn: HandleActionFn? = {
|
|
5488
|
+
guard let sym = dlsym(image, "craft_ios_handle_action") else { return nil }
|
|
5489
|
+
return unsafeBitCast(sym, to: HandleActionFn.self)
|
|
5490
|
+
}()
|
|
5491
|
+
|
|
5492
|
+
private static let adoptLocationRecordingFn: AdoptLocationRecordingFn? = {
|
|
5493
|
+
guard let sym = dlsym(image, "craft_ios_adopt_location_recording") else { return nil }
|
|
5494
|
+
return unsafeBitCast(sym, to: AdoptLocationRecordingFn.self)
|
|
5495
|
+
}()
|
|
5496
|
+
|
|
5497
|
+
/// Whether this build has a Zig runtime at all. Read by nothing here; kept
|
|
5498
|
+
/// because "is Zig linked" is the first question to ask when an action
|
|
5499
|
+
/// behaves like the Swift one after a migration was supposed to move it.
|
|
5500
|
+
@objc static var isLinked: Bool { handleActionFn != nil }
|
|
5501
|
+
|
|
5502
|
+
/// Give Zig the webview its replies are evaluated against.
|
|
5503
|
+
///
|
|
5504
|
+
/// Unretained on purpose, on both sides: this is the app's own root view
|
|
5505
|
+
/// and it outlives the runtime. A retain here would be a cycle with
|
|
5506
|
+
/// nothing to break it.
|
|
5507
|
+
static func attach(_ webView: WKWebView) {
|
|
5508
|
+
setWebViewFn?(Unmanaged.passUnretained(webView).toOpaque())
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
/// Hand a recording that outlived the last launch to whichever runtime owns
|
|
5512
|
+
/// the recorder.
|
|
5513
|
+
///
|
|
5514
|
+
/// Returns true when Zig took it, and the caller must then *not* run
|
|
5515
|
+
/// `restoreLocationRecordingState()`. This is the one place the usual
|
|
5516
|
+
/// "offer it to Zig, fall back on false" pattern cannot be used through
|
|
5517
|
+
/// `handleAction`: there is no page message at launch, and the restore
|
|
5518
|
+
/// happens in `Coordinator.init` — before `attach`, because SwiftUI builds
|
|
5519
|
+
/// the coordinator before the view. Whoever restores first owns the
|
|
5520
|
+
/// `CLLocationManager` for the rest of the launch, so the decision has to
|
|
5521
|
+
/// be made here rather than at the first `stopLocationRecording`.
|
|
5522
|
+
///
|
|
5523
|
+
/// False in a build with no Zig runtime, which is exactly when Swift's own
|
|
5524
|
+
/// restore is still the right thing to run.
|
|
5525
|
+
static func adoptLocationRecording() -> Bool {
|
|
5526
|
+
adoptLocationRecordingFn?() ?? false
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
/// Forget this webview, if Zig is still holding it.
|
|
5530
|
+
///
|
|
5531
|
+
/// `attach` hands over an unretained pointer, so the moment SwiftUI
|
|
5532
|
+
/// deallocates the view Zig is holding freed memory and every later reply
|
|
5533
|
+
/// is an `objc_msgSend` into it. Nothing called this before, and nothing
|
|
5534
|
+
/// had to while the app had exactly one webview for its whole life — but
|
|
5535
|
+
/// a `UIViewRepresentable` is rebuilt whenever its identity changes, and
|
|
5536
|
+
/// the second rebuild is where that assumption stops holding.
|
|
5537
|
+
///
|
|
5538
|
+
/// The webview is passed rather than implied: Zig clears only if this is
|
|
5539
|
+
/// still the pointer it has, so a rebuild that makes the replacement
|
|
5540
|
+
/// before dismantling the original cannot blank the live one.
|
|
5541
|
+
static func detach(_ webView: WKWebView) {
|
|
5542
|
+
clearWebViewFn?(Unmanaged.passUnretained(webView).toOpaque())
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
/// Offer one page message to the Zig dispatcher.
|
|
5546
|
+
///
|
|
5547
|
+
/// Returns true when Zig has taken responsibility — the caller must not
|
|
5548
|
+
/// answer as well. False when no Zig module recognised the action, or when
|
|
5549
|
+
/// there is no runtime to ask.
|
|
5550
|
+
static func offer(action: String, body: [String: Any], callbackId: String?) -> Bool {
|
|
5551
|
+
guard let handleActionFn else { return false }
|
|
5552
|
+
|
|
5553
|
+
// `_invoke` flattens the payload into the message —
|
|
5554
|
+
// `Object.assign({}, payload, {action, callbackId})` — so the payload
|
|
5555
|
+
// Zig's handlers parse is the message minus the two envelope keys.
|
|
5556
|
+
// This is the exact inverse of what `CraftSwiftShim.handleAction` does
|
|
5557
|
+
// when a call travels the other way, and the two must stay inverses:
|
|
5558
|
+
// leaving `action` in would put a key in the payload no handler
|
|
5559
|
+
// expects, and dropping a real field would hand a handler defaults the
|
|
5560
|
+
// page never asked for.
|
|
5561
|
+
var payload = body
|
|
5562
|
+
payload.removeValue(forKey: "action")
|
|
5563
|
+
payload.removeValue(forKey: "callbackId")
|
|
5564
|
+
|
|
5565
|
+
// A payload that will not serialise is not offered. Falling through to
|
|
5566
|
+
// the Swift switch is the safe direction: it reads `body` directly and
|
|
5567
|
+
// never needs the round trip through JSON that just failed.
|
|
5568
|
+
guard JSONSerialization.isValidJSONObject(payload),
|
|
5569
|
+
let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
5570
|
+
let json = String(data: data, encoding: .utf8)
|
|
5571
|
+
else { return false }
|
|
5572
|
+
|
|
5573
|
+
return action.withCString { a in
|
|
5574
|
+
json.withCString { p in
|
|
5575
|
+
handleActionFn(a, UInt(strlen(a)), p, UInt(strlen(p)), requestId(from: callbackId))
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
|
|
5580
|
+
/// "cb_7" -> 7, and -1 for a message with no callback waiting on it.
|
|
5581
|
+
///
|
|
5582
|
+
/// -1 rather than 0, matching what `craft_ios_deliver_result` already
|
|
5583
|
+
/// treats as "no id": zero is a perfectly good callback number, and a
|
|
5584
|
+
/// sentinel that collides with a real id would deliver one caller's answer
|
|
5585
|
+
/// to another.
|
|
5586
|
+
private static func requestId(from callbackId: String?) -> Int64 {
|
|
5587
|
+
guard let callbackId, callbackId.hasPrefix("cb_"),
|
|
5588
|
+
let n = Int64(callbackId.dropFirst(3))
|
|
5589
|
+
else { return -1 }
|
|
5590
|
+
return n
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5196
5594
|
/// The seam the Zig dispatcher hands unmigrated actions through.
|
|
5197
5595
|
///
|
|
5198
5596
|
/// Discovery is symmetric and both directions are runtime-only. Zig finds this
|