ReviewOS

also looking at this

craft-native/craft

fix(ios): no gated dispatcher arm can hang the page any more (58 → 0)

#120
Merged glennmichael123 wants to merge fix/ios-gated-arms-reject into main
1 file +298 -106

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/ios/templates/CraftApp.swiftmodified+238-106
Changes to packages/ios/templates/CraftApp.swift
@@ -561,7 +561,11 @@ struct CraftWebView: UIViewRepresentable {
561561 func dispatch(action: String, body: [String: Any], callbackId: String?) {
562562 switch action {
563563 case "startListening":
564 if config.enableSpeechRecognition { startSpeechRecognition() }
564 if config.enableSpeechRecognition {
565 startSpeechRecognition()
566 } else {
567 rejectCallback(callbackId, error: "Speech recognition is disabled", code: "CAPABILITY_DISABLED")
568 }
565569 case "stopListening":
566570 stopSpeechRecognition()
567571 case "haptic":
@@ -611,23 +615,38 @@ struct CraftWebView: UIViewRepresentable {
611615 rejectCallback(callbackId, error: "Push notifications are disabled", code: "CAPABILITY_DISABLED")
612616 }
613617 case "secureSet":
614 if config.enableSecureStorage,
615 let key = body["key"] as? String,
616 let value = body["value"] as? String {
617 let success = secureStore(key: key, value: value)
618 resolveCallback(callbackId, result: success)
618 if config.enableSecureStorage {
619 if let key = body["key"] as? String,
620 let value = body["value"] as? String {
621 let success = secureStore(key: key, value: value)
622 resolveCallback(callbackId, result: success)
623 } else {
624 rejectCallback(callbackId, error: "secureSet was called without the values it needs", code: "INVALID_ARGUMENT")
625 }
626 } else {
627 rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
619628 }
620629 case "secureGet":
621 if config.enableSecureStorage,
622 let key = body["key"] as? String {
623 let value = secureRetrieve(key: key)
624 resolveCallback(callbackId, result: value as Any)
630 if config.enableSecureStorage {
631 if let key = body["key"] as? String {
632 let value = secureRetrieve(key: key)
633 resolveCallback(callbackId, result: value as Any)
634 } else {
635 rejectCallback(callbackId, error: "secureGet was called without the values it needs", code: "INVALID_ARGUMENT")
636 }
637 } else {
638 rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
625639 }
626640 case "secureRemove":
627 if config.enableSecureStorage,
628 let key = body["key"] as? String {
629 let success = secureRemove(key: key)
630 resolveCallback(callbackId, result: success)
641 if config.enableSecureStorage {
642 if let key = body["key"] as? String {
643 let success = secureRemove(key: key)
644 resolveCallback(callbackId, result: success)
645 } else {
646 rejectCallback(callbackId, error: "secureRemove was called without the values it needs", code: "INVALID_ARGUMENT")
647 }
648 } else {
649 rejectCallback(callbackId, error: "Secure storage is disabled", code: "CAPABILITY_DISABLED")
631650 }
632651 case "secureClear":
633652 if config.enableSecureStorage {
@@ -690,10 +709,16 @@ struct CraftWebView: UIViewRepresentable {
690709 readLocationRecording(callbackId: callbackId)
691710 // Clipboard
692711 case "clipboardWrite":
693 if config.enableClipboard, let text = body["text"] as? String {
694 UIPasteboard.general.string = text
695 resolveCallback(callbackId, result: true)
696 }
712 if config.enableClipboard {
713 if let text = body["text"] as? String {
714 UIPasteboard.general.string = text
715 resolveCallback(callbackId, result: true)
716 } else {
717 rejectCallback(callbackId, error: "clipboardWrite was called without the values it needs", code: "INVALID_ARGUMENT")
718 }
719 } else {
720 rejectCallback(callbackId, error: "Clipboard is disabled", code: "CAPABILITY_DISABLED")
721 }
697722 case "clipboardRead":
698723 if config.enableClipboard {
699724 let text = UIPasteboard.general.string ?? ""
@@ -751,11 +776,15 @@ struct CraftWebView: UIViewRepresentable {
751776 rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
752777 }
753778 case "addContact":
754 if config.enableContacts,
755 let contactData = body["contact"] as? [String: Any] {
756 addContact(contactData, callbackId: callbackId)
779 if config.enableContacts {
780 if let contactData = body["contact"] as? [String: Any] {
781 addContact(contactData, callbackId: callbackId)
782 } else {
783 rejectCallback(callbackId, error: "addContact was called without the values it needs", code: "INVALID_ARGUMENT")
784 }
785 } else {
786 rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
757787 }
758
759788 // MARK: - Calendar
760789 case "getCalendarEvents":
761790 if config.enableCalendar {
@@ -766,26 +795,45 @@ struct CraftWebView: UIViewRepresentable {
766795 rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
767796 }
768797 case "createCalendarEvent":
769 if config.enableCalendar,
770 let eventData = body["event"] as? [String: Any] {
771 createCalendarEvent(eventData, callbackId: callbackId)
798 if config.enableCalendar {
799 if let eventData = body["event"] as? [String: Any] {
800 createCalendarEvent(eventData, callbackId: callbackId)
801 } else {
802 rejectCallback(callbackId, error: "createCalendarEvent was called without the values it needs", code: "INVALID_ARGUMENT")
803 }
804 } else {
805 rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
772806 }
773807 case "deleteCalendarEvent":
774 if config.enableCalendar,
775 let eventId = body["eventId"] as? String {
776 deleteCalendarEvent(eventId, callbackId: callbackId)
808 if config.enableCalendar {
809 if let eventId = body["eventId"] as? String {
810 deleteCalendarEvent(eventId, callbackId: callbackId)
811 } else {
812 rejectCallback(callbackId, error: "deleteCalendarEvent was called without the values it needs", code: "INVALID_ARGUMENT")
813 }
814 } else {
815 rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
777816 }
778
779817 // MARK: - Local Notifications
780818 case "scheduleNotification":
781 if config.enableLocalNotifications,
782 let notifData = body["notification"] as? [String: Any] {
783 scheduleLocalNotification(notifData, callbackId: callbackId)
819 if config.enableLocalNotifications {
820 if let notifData = body["notification"] as? [String: Any] {
821 scheduleLocalNotification(notifData, callbackId: callbackId)
822 } else {
823 rejectCallback(callbackId, error: "scheduleNotification was called without the values it needs", code: "INVALID_ARGUMENT")
824 }
825 } else {
826 rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
784827 }
785828 case "cancelNotification":
786 if config.enableLocalNotifications,
787 let notifId = body["id"] as? String {
788 cancelLocalNotification(notifId, callbackId: callbackId)
829 if config.enableLocalNotifications {
830 if let notifId = body["id"] as? String {
831 cancelLocalNotification(notifId, callbackId: callbackId)
832 } else {
833 rejectCallback(callbackId, error: "cancelNotification was called without the values it needs", code: "INVALID_ARGUMENT")
834 }
835 } else {
836 rejectCallback(callbackId, error: "Local notifications are disabled", code: "CAPABILITY_DISABLED")
789837 }
790838 case "cancelAllNotifications":
791839 if config.enableLocalNotifications {
@@ -827,14 +875,24 @@ struct CraftWebView: UIViewRepresentable {
827875
828876 // MARK: - In-App Purchase
829877 case "getProducts":
830 if config.enableInAppPurchase,
831 let productIds = body["productIds"] as? [String] {
832 getProducts(productIds, callbackId: callbackId)
878 if config.enableInAppPurchase {
879 if let productIds = body["productIds"] as? [String] {
880 getProducts(productIds, callbackId: callbackId)
881 } else {
882 rejectCallback(callbackId, error: "getProducts was called without the values it needs", code: "INVALID_ARGUMENT")
883 }
884 } else {
885 rejectCallback(callbackId, error: "In-app purchase is disabled", code: "CAPABILITY_DISABLED")
833886 }
834887 case "purchase":
835 if config.enableInAppPurchase,
836 let productId = body["productId"] as? String {
837 purchaseProduct(productId, callbackId: callbackId)
888 if config.enableInAppPurchase {
889 if let productId = body["productId"] as? String {
890 purchaseProduct(productId, callbackId: callbackId)
891 } else {
892 rejectCallback(callbackId, error: "purchase was called without the values it needs", code: "INVALID_ARGUMENT")
893 }
894 } else {
895 rejectCallback(callbackId, error: "In-app purchase is disabled", code: "CAPABILITY_DISABLED")
838896 }
839897 case "restorePurchases":
840898 if config.enableInAppPurchase {
@@ -845,16 +903,25 @@ struct CraftWebView: UIViewRepresentable {
845903
846904 // MARK: - Keep Awake
847905 case "setKeepAwake":
848 if config.enableKeepAwake,
849 let enabled = body["enabled"] as? Bool {
850 setKeepAwake(enabled, callbackId: callbackId)
906 if config.enableKeepAwake {
907 if let enabled = body["enabled"] as? Bool {
908 setKeepAwake(enabled, callbackId: callbackId)
909 } else {
910 rejectCallback(callbackId, error: "setKeepAwake was called without the values it needs", code: "INVALID_ARGUMENT")
911 }
912 } else {
913 rejectCallback(callbackId, error: "Keep awake is disabled", code: "CAPABILITY_DISABLED")
851914 }
852
853915 // MARK: - Orientation Lock
854916 case "lockOrientation":
855 if config.enableOrientationLock,
856 let orientation = body["orientation"] as? String {
857 lockOrientation(orientation, callbackId: callbackId)
917 if config.enableOrientationLock {
918 if let orientation = body["orientation"] as? String {
919 lockOrientation(orientation, callbackId: callbackId)
920 } else {
921 rejectCallback(callbackId, error: "lockOrientation was called without the values it needs", code: "INVALID_ARGUMENT")
922 }
923 } else {
924 rejectCallback(callbackId, error: "Orientation lock is disabled", code: "CAPABILITY_DISABLED")
858925 }
859926 case "unlockOrientation":
860927 if config.enableOrientationLock {
@@ -882,18 +949,25 @@ struct CraftWebView: UIViewRepresentable {
882949
883950 // MARK: - File Download
884951 case "downloadFile":
885 if config.enableFileDownload,
886 let url = body["url"] as? String,
887 let filename = body["filename"] as? String {
888 downloadFile(url: url, filename: filename, callbackId: callbackId)
889 }
952 if config.enableFileDownload {
953 if let url = body["url"] as? String, let filename = body["filename"] as? String {
954 downloadFile(url: url, filename: filename, callbackId: callbackId)
955 } else {
956 rejectCallback(callbackId, error: "downloadFile was called without the values it needs", code: "INVALID_ARGUMENT")
957 }
958 } else {
959 rejectCallback(callbackId, error: "File download is disabled", code: "CAPABILITY_DISABLED")
960 }
890961 case "saveFile":
891 if config.enableFileDownload,
892 let data = body["data"] as? String,
893 let filename = body["filename"] as? String {
894 saveFile(data: data, filename: filename, callbackId: callbackId)
895 }
896
962 if config.enableFileDownload {
963 if let data = body["data"] as? String, let filename = body["filename"] as? String {
964 saveFile(data: data, filename: filename, callbackId: callbackId)
965 } else {
966 rejectCallback(callbackId, error: "saveFile was called without the values it needs", code: "INVALID_ARGUMENT")
967 }
968 } else {
969 rejectCallback(callbackId, error: "File download is disabled", code: "CAPABILITY_DISABLED")
970 }
897971 // MARK: - Social Auth
898972 case "signInWithApple":
899973 if config.enableSocialAuth {
@@ -938,18 +1012,27 @@ struct CraftWebView: UIViewRepresentable {
9381012
9391013 // MARK: - Local Database
9401014 case "dbExecute":
941 if config.enableLocalDatabase,
942 let sql = body["sql"] as? String {
943 let params = body["params"] as? [Any]
944 dbExecute(sql: sql, params: params, callbackId: callbackId)
1015 if config.enableLocalDatabase {
1016 if let sql = body["sql"] as? String {
1017 let params = body["params"] as? [Any]
1018 dbExecute(sql: sql, params: params, callbackId: callbackId)
1019 } else {
1020 rejectCallback(callbackId, error: "dbExecute was called without the values it needs", code: "INVALID_ARGUMENT")
1021 }
1022 } else {
1023 rejectCallback(callbackId, error: "Local database is disabled", code: "CAPABILITY_DISABLED")
9451024 }
9461025 case "dbQuery":
947 if config.enableLocalDatabase,
948 let sql = body["sql"] as? String {
949 let params = body["params"] as? [Any]
950 dbQuery(sql: sql, params: params, callbackId: callbackId)
1026 if config.enableLocalDatabase {
1027 if let sql = body["sql"] as? String {
1028 let params = body["params"] as? [Any]
1029 dbQuery(sql: sql, params: params, callbackId: callbackId)
1030 } else {
1031 rejectCallback(callbackId, error: "dbQuery was called without the values it needs", code: "INVALID_ARGUMENT")
1032 }
1033 } else {
1034 rejectCallback(callbackId, error: "Local database is disabled", code: "CAPABILITY_DISABLED")
9511035 }
952
9531036 // MARK: - Bluetooth
9541037 case "startBluetoothScan":
9551038 if config.enableBluetooth {
@@ -978,11 +1061,16 @@ struct CraftWebView: UIViewRepresentable {
9781061 rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
9791062 }
9801063 case "getHealthData":
981 if config.enableHealthKit,
982 let dataType = body["type"] as? String {
983 let startDate = body["startDate"] as? Double
984 let endDate = body["endDate"] as? Double
985 getHealthData(type: dataType, startDate: startDate, endDate: endDate, callbackId: callbackId)
1064 if config.enableHealthKit {
1065 if let dataType = body["type"] as? String {
1066 let startDate = body["startDate"] as? Double
1067 let endDate = body["endDate"] as? Double
1068 getHealthData(type: dataType, startDate: startDate, endDate: endDate, callbackId: callbackId)
1069 } else {
1070 rejectCallback(callbackId, error: "getHealthData was called without the values it needs", code: "INVALID_ARGUMENT")
1071 }
1072 } else {
1073 rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
9861074 }
9871075 case "saveHealthWorkout":
9881076 if config.enableHealthKit {
@@ -1013,22 +1101,37 @@ struct CraftWebView: UIViewRepresentable {
10131101
10141102 // MARK: - Background Tasks
10151103 case "registerBackgroundTask":
1016 if config.enableBackgroundTasks,
1017 let taskId = body["taskId"] as? String {
1018 registerBackgroundTask(taskId: taskId, callbackId: callbackId)
1104 if config.enableBackgroundTasks {
1105 if let taskId = body["taskId"] as? String {
1106 registerBackgroundTask(taskId: taskId, callbackId: callbackId)
1107 } else {
1108 rejectCallback(callbackId, error: "registerBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
1109 }
1110 } else {
1111 rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
10191112 }
10201113 case "scheduleBackgroundTask":
1021 if config.enableBackgroundTasks,
1022 let taskId = body["taskId"] as? String {
1023 let delay = body["delay"] as? Double ?? 900 // 15 minutes default
1024 let requiresNetwork = body["requiresNetwork"] as? Bool ?? false
1025 let requiresCharging = body["requiresCharging"] as? Bool ?? false
1026 scheduleBackgroundTask(taskId: taskId, delay: delay, requiresNetwork: requiresNetwork, requiresCharging: requiresCharging, callbackId: callbackId)
1114 if config.enableBackgroundTasks {
1115 if let taskId = body["taskId"] as? String {
1116 let delay = body["delay"] as? Double ?? 900 // 15 minutes default
1117 let requiresNetwork = body["requiresNetwork"] as? Bool ?? false
1118 let requiresCharging = body["requiresCharging"] as? Bool ?? false
1119 scheduleBackgroundTask(taskId: taskId, delay: delay, requiresNetwork: requiresNetwork, requiresCharging: requiresCharging, callbackId: callbackId)
1120 } else {
1121 rejectCallback(callbackId, error: "scheduleBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
1122 }
1123 } else {
1124 rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
10271125 }
10281126 case "cancelBackgroundTask":
1029 if config.enableBackgroundTasks,
1030 let taskId = body["taskId"] as? String {
1031 cancelBackgroundTask(taskId: taskId, callbackId: callbackId)
1127 if config.enableBackgroundTasks {
1128 if let taskId = body["taskId"] as? String {
1129 cancelBackgroundTask(taskId: taskId, callbackId: callbackId)
1130 } else {
1131 rejectCallback(callbackId, error: "cancelBackgroundTask was called without the values it needs", code: "INVALID_ARGUMENT")
1132 }
1133 } else {
1134 rejectCallback(callbackId, error: "Background tasks are disabled", code: "CAPABILITY_DISABLED")
10321135 }
10331136 case "cancelAllBackgroundTasks":
10341137 if config.enableBackgroundTasks {
@@ -1039,10 +1142,15 @@ struct CraftWebView: UIViewRepresentable {
10391142
10401143 // MARK: - PDF Viewer
10411144 case "openPDF":
1042 if config.enablePDFViewer,
1043 let source = body["source"] as? String {
1044 let page = body["page"] as? Int ?? 0
1045 openPDF(source: source, page: page, callbackId: callbackId)
1145 if config.enablePDFViewer {
1146 if let source = body["source"] as? String {
1147 let page = body["page"] as? Int ?? 0
1148 openPDF(source: source, page: page, callbackId: callbackId)
1149 } else {
1150 rejectCallback(callbackId, error: "openPDF was called without the values it needs", code: "INVALID_ARGUMENT")
1151 }
1152 } else {
1153 rejectCallback(callbackId, error: "PDF viewing is disabled", code: "CAPABILITY_DISABLED")
10461154 }
10471155 case "closePDF":
10481156 closePDF(callbackId: callbackId)
@@ -1108,15 +1216,25 @@ struct CraftWebView: UIViewRepresentable {
11081216 rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
11091217 }
11101218 case "placeARObject":
1111 if config.enableAR,
1112 let model = body["model"] as? String {
1113 let position = body["position"] as? [String: Double]
1114 placeARObject(model: model, position: position, callbackId: callbackId)
1219 if config.enableAR {
1220 if let model = body["model"] as? String {
1221 let position = body["position"] as? [String: Double]
1222 placeARObject(model: model, position: position, callbackId: callbackId)
1223 } else {
1224 rejectCallback(callbackId, error: "placeARObject was called without the values it needs", code: "INVALID_ARGUMENT")
1225 }
1226 } else {
1227 rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
11151228 }
11161229 case "removeARObject":
1117 if config.enableAR,
1118 let objectId = body["objectId"] as? String {
1119 removeARObject(objectId: objectId, callbackId: callbackId)
1230 if config.enableAR {
1231 if let objectId = body["objectId"] as? String {
1232 removeARObject(objectId: objectId, callbackId: callbackId)
1233 } else {
1234 rejectCallback(callbackId, error: "removeARObject was called without the values it needs", code: "INVALID_ARGUMENT")
1235 }
1236 } else {
1237 rejectCallback(callbackId, error: "AR is disabled", code: "CAPABILITY_DISABLED")
11201238 }
11211239 case "getARPlanes":
11221240 if config.enableAR {
@@ -1127,21 +1245,35 @@ struct CraftWebView: UIViewRepresentable {
11271245
11281246 // MARK: - ML (Core ML / Vision)
11291247 case "classifyImage":
1130 if config.enableMLKit,
1131 let imageBase64 = body["image"] as? String {
1132 classifyImage(imageBase64: imageBase64, callbackId: callbackId)
1248 if config.enableMLKit {
1249 if let imageBase64 = body["image"] as? String {
1250 classifyImage(imageBase64: imageBase64, callbackId: callbackId)
1251 } else {
1252 rejectCallback(callbackId, error: "classifyImage was called without the values it needs", code: "INVALID_ARGUMENT")
1253 }
1254 } else {
1255 rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
11331256 }
11341257 case "detectObjects":
1135 if config.enableMLKit,
1136 let imageBase64 = body["image"] as? String {
1137 detectObjects(imageBase64: imageBase64, callbackId: callbackId)
1258 if config.enableMLKit {
1259 if let imageBase64 = body["image"] as? String {
1260 detectObjects(imageBase64: imageBase64, callbackId: callbackId)
1261 } else {
1262 rejectCallback(callbackId, error: "detectObjects was called without the values it needs", code: "INVALID_ARGUMENT")
1263 }
1264 } else {
1265 rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
11381266 }
11391267 case "recognizeText":
1140 if config.enableMLKit,
1141 let imageBase64 = body["image"] as? String {
1142 recognizeText(imageBase64: imageBase64, callbackId: callbackId)
1268 if config.enableMLKit {
1269 if let imageBase64 = body["image"] as? String {
1270 recognizeText(imageBase64: imageBase64, callbackId: callbackId)
1271 } else {
1272 rejectCallback(callbackId, error: "recognizeText was called without the values it needs", code: "INVALID_ARGUMENT")
1273 }
1274 } else {
1275 rejectCallback(callbackId, error: "Vision is disabled", code: "CAPABILITY_DISABLED")
11431276 }
1144
11451277 // MARK: - Widget
11461278 case "updateWidget":
11471279 if let data = body["data"] as? [String: Any] {