craft-native 0.0.89 → 0.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/src/index.js +285 -44
- package/dist/android/src/promise-runtime.d.ts +3 -0
- package/dist/android/templates/CraftBridge.kt.template +1951 -1193
- package/dist/android/templates/CraftHealthConnect.kt.template +45 -6
- package/dist/android/templates/CraftHealthConnectStub.kt.template +7 -1
- package/dist/android/templates/CraftNative.kt.template +2231 -0
- package/dist/android/templates/LocationRecordingService.kt.template +34 -9
- package/dist/android/templates/MainActivity.kt.template +25 -2
- package/dist/android/templates/proguard-rules.pro.template +4 -1
- package/dist/android/templates/test-bridges.html +10 -33
- package/dist/api/index.d.ts +1 -1
- package/dist/api/ios-advanced.d.ts +8 -5
- package/dist/api/live-activity-handle.d.ts +6 -0
- package/dist/api/mobile.d.ts +13 -5
- package/dist/api/window.d.ts +2 -0
- package/dist/cli.js +404 -128
- package/dist/index.cjs +65 -17
- package/dist/index.js +65 -17
- package/dist/ios/src/index.js +22 -4
- package/dist/ios/templates/CraftApp.swift +473 -60
- package/dist/ios/templates/CraftWatchApp.swift.template +8 -0
- package/dist/ios/templates/WatchApp.Info.plist.template +2 -0
- package/dist/ios/templates/WatchExtension.Info.plist.template +34 -0
- package/dist/ios/templates/project.yml.template +10 -4
- package/dist/mobile.js +36 -13
- package/dist/scaffold-version.d.ts +5 -0
- package/package.json +1 -1
- package/dist/android/templates/CraftBridgeExtensions.kt.template +0 -383
- package/dist/android/templates/CraftWidgetProvider.kt.template +0 -246
|
@@ -40,9 +40,24 @@ extension Notification.Name {
|
|
|
40
40
|
final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
|
41
41
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
42
42
|
UNUserNotificationCenter.current().delegate = self
|
|
43
|
+
if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
|
|
44
|
+
CraftEventManager.shared.handleShortcut(shortcut)
|
|
45
|
+
// Returning false tells UIKit the launch-time item was handled and
|
|
46
|
+
// prevents a second performActionFor callback for the same tap.
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
43
49
|
return true
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
|
53
|
+
CraftEventManager.shared.handleShortcut(shortcutItem)
|
|
54
|
+
completionHandler(true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
|
58
|
+
CraftEventManager.shared.handleSiriActivity(userActivity)
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
47
62
|
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
48
63
|
NotificationCenter.default.post(name: .craftPushToken, object: token)
|
|
@@ -62,6 +77,71 @@ final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificatio
|
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
// MARK: - Native Event Manager
|
|
81
|
+
class CraftEventManager {
|
|
82
|
+
static let shared = CraftEventManager()
|
|
83
|
+
|
|
84
|
+
private weak var webView: WKWebView?
|
|
85
|
+
private var isReady = false
|
|
86
|
+
private var pendingEvents: [(name: String, data: [String: Any])] = []
|
|
87
|
+
|
|
88
|
+
private init() {}
|
|
89
|
+
|
|
90
|
+
func setWebView(_ webView: WKWebView) {
|
|
91
|
+
self.webView = webView
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func setLoading() {
|
|
95
|
+
isReady = false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func setReady() {
|
|
99
|
+
isReady = true
|
|
100
|
+
let events = pendingEvents
|
|
101
|
+
pendingEvents.removeAll()
|
|
102
|
+
for event in events {
|
|
103
|
+
dispatch(event.name, data: event.data)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func handleShortcut(_ shortcut: UIApplicationShortcutItem) {
|
|
108
|
+
sendToWeb("craftShortcut", data: ["type": shortcut.type])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func handleSiriActivity(_ activity: NSUserActivity) -> Bool {
|
|
112
|
+
let prefix = "\(Bundle.main.bundleIdentifier ?? "{{BUNDLE_ID}}")."
|
|
113
|
+
guard activity.activityType.hasPrefix(prefix) else { return false }
|
|
114
|
+
|
|
115
|
+
let action = activity.userInfo?["action"] as? String
|
|
116
|
+
?? String(activity.activityType.dropFirst(prefix.count))
|
|
117
|
+
var data: [String: Any] = [:]
|
|
118
|
+
for (key, value) in activity.userInfo ?? [:] {
|
|
119
|
+
guard let key = key as? String, key != "action" else { continue }
|
|
120
|
+
data[key] = value
|
|
121
|
+
}
|
|
122
|
+
sendToWeb("craftSiriShortcut", data: ["action": action, "data": data])
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func sendToWeb(_ event: String, data: [String: Any]) {
|
|
127
|
+
guard isReady, webView != nil else {
|
|
128
|
+
pendingEvents.append((event, data))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
dispatch(event, data: data)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func dispatch(_ event: String, data: [String: Any]) {
|
|
135
|
+
guard let webView = webView,
|
|
136
|
+
let jsonData = try? JSONSerialization.data(withJSONObject: data),
|
|
137
|
+
let json = String(data: jsonData, encoding: .utf8) else { return }
|
|
138
|
+
let script = "window.dispatchEvent(new CustomEvent('\(event)', {detail: \(json)}));"
|
|
139
|
+
DispatchQueue.main.async {
|
|
140
|
+
webView.evaluateJavaScript(script, completionHandler: nil)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
65
145
|
// MARK: - App Entry Point
|
|
66
146
|
@main
|
|
67
147
|
struct CraftApp: App {
|
|
@@ -117,7 +197,7 @@ class DeepLinkManager {
|
|
|
117
197
|
initialURL = url
|
|
118
198
|
}
|
|
119
199
|
|
|
120
|
-
if isReady
|
|
200
|
+
if isReady && webView != nil {
|
|
121
201
|
dispatchDeepLink(url)
|
|
122
202
|
} else {
|
|
123
203
|
// Store for later when web view is ready
|
|
@@ -346,6 +426,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
346
426
|
|
|
347
427
|
// Register with DeepLinkManager
|
|
348
428
|
DeepLinkManager.shared.setWebView(webView)
|
|
429
|
+
CraftEventManager.shared.setWebView(webView)
|
|
349
430
|
|
|
350
431
|
// Parse background color
|
|
351
432
|
let bgColor = UIColor(hex: config.backgroundColor) ?? .black
|
|
@@ -400,7 +481,9 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
400
481
|
// Location
|
|
401
482
|
private var locationManager: CLLocationManager?
|
|
402
483
|
private var singleLocationCallbackId: String?
|
|
484
|
+
private var singleLocationTimeoutWorkItem: DispatchWorkItem?
|
|
403
485
|
private var locationPermissionCallbackId: String?
|
|
486
|
+
private var locationPermissionRequiresAlways = false
|
|
404
487
|
private var isWatchingLocation = false
|
|
405
488
|
private var isRecordingLocation = false
|
|
406
489
|
private var isLocationRecordingPaused = false
|
|
@@ -683,7 +766,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
683
766
|
// Geolocation
|
|
684
767
|
case "getCurrentPosition":
|
|
685
768
|
if config.enableGeolocation {
|
|
686
|
-
getCurrentPosition(callbackId: callbackId)
|
|
769
|
+
getCurrentPosition(body: body, callbackId: callbackId)
|
|
687
770
|
} else {
|
|
688
771
|
rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
|
|
689
772
|
}
|
|
@@ -1094,7 +1177,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1094
1177
|
case "updateLiveActivity":
|
|
1095
1178
|
updateLiveActivity(body: body, callbackId: callbackId)
|
|
1096
1179
|
case "endLiveActivity":
|
|
1097
|
-
endLiveActivity(callbackId: callbackId)
|
|
1180
|
+
endLiveActivity(body: body, callbackId: callbackId)
|
|
1098
1181
|
|
|
1099
1182
|
// MARK: - Screen Capture
|
|
1100
1183
|
case "takeScreenshot":
|
|
@@ -1315,6 +1398,10 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1315
1398
|
}
|
|
1316
1399
|
}
|
|
1317
1400
|
|
|
1401
|
+
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
1402
|
+
CraftEventManager.shared.setLoading()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1318
1405
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
1319
1406
|
self.webView = webView
|
|
1320
1407
|
guard isTrustedURL(webView.url) else { return }
|
|
@@ -1782,6 +1869,114 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1782
1869
|
};
|
|
1783
1870
|
},
|
|
1784
1871
|
|
|
1872
|
+
// Flat SDK compatibility methods. The native dispatcher already
|
|
1873
|
+
// owns these actions; keep the public CraftBridge shape callable
|
|
1874
|
+
// while the versioned API below provides the namespaced form.
|
|
1875
|
+
scanNFC: function() {
|
|
1876
|
+
return this._invoke('scanNFC');
|
|
1877
|
+
},
|
|
1878
|
+
scanQRCode: function() {
|
|
1879
|
+
return this._invoke('scanQRCode');
|
|
1880
|
+
},
|
|
1881
|
+
takeScreenshot: function() {
|
|
1882
|
+
return this._invoke('takeScreenshot');
|
|
1883
|
+
},
|
|
1884
|
+
startAudioRecording: function() {
|
|
1885
|
+
return this._invoke('startAudioRecording');
|
|
1886
|
+
},
|
|
1887
|
+
stopAudioRecording: function() {
|
|
1888
|
+
return this._invoke('stopAudioRecording');
|
|
1889
|
+
},
|
|
1890
|
+
startVideoRecording: function() {
|
|
1891
|
+
return this._invoke('startVideoRecording');
|
|
1892
|
+
},
|
|
1893
|
+
pickFile: function(types) {
|
|
1894
|
+
return this._invoke('pickFile', {types: types || []});
|
|
1895
|
+
},
|
|
1896
|
+
downloadFile: function(url, filename) {
|
|
1897
|
+
return this._invoke('downloadFile', {url: url, filename: filename});
|
|
1898
|
+
},
|
|
1899
|
+
saveFile: function(data, filename, mimeType) {
|
|
1900
|
+
return this._invoke('saveFile', {data: data, filename: filename, mimeType: mimeType});
|
|
1901
|
+
},
|
|
1902
|
+
startMotionUpdates: function() {
|
|
1903
|
+
return this._invoke('startMotionUpdates');
|
|
1904
|
+
},
|
|
1905
|
+
stopMotionUpdates: function() {
|
|
1906
|
+
return this._invoke('stopMotionUpdates');
|
|
1907
|
+
},
|
|
1908
|
+
getCurrentPosition: function() {
|
|
1909
|
+
return this.geolocation.getCurrentPosition({});
|
|
1910
|
+
},
|
|
1911
|
+
watchPosition: function(callback) {
|
|
1912
|
+
return this.geolocation.watchPosition(callback);
|
|
1913
|
+
},
|
|
1914
|
+
clearWatch: function(watchId) {
|
|
1915
|
+
return this.geolocation.clearWatch(watchId);
|
|
1916
|
+
},
|
|
1917
|
+
getContacts: function() {
|
|
1918
|
+
return this._invoke('getContacts');
|
|
1919
|
+
},
|
|
1920
|
+
addContact: function(contact) {
|
|
1921
|
+
return this._invoke('addContact', {contact: contact});
|
|
1922
|
+
},
|
|
1923
|
+
getCalendarEvents: function(startDate, endDate) {
|
|
1924
|
+
return this._invoke('getCalendarEvents', {startDate: startDate, endDate: endDate});
|
|
1925
|
+
},
|
|
1926
|
+
createCalendarEvent: function(event) {
|
|
1927
|
+
return this._invoke('createCalendarEvent', {event: event});
|
|
1928
|
+
},
|
|
1929
|
+
deleteCalendarEvent: function(eventId) {
|
|
1930
|
+
return this._invoke('deleteCalendarEvent', {eventId: eventId});
|
|
1931
|
+
},
|
|
1932
|
+
scheduleNotification: function(notification) {
|
|
1933
|
+
return this._invoke('scheduleNotification', {notification: notification});
|
|
1934
|
+
},
|
|
1935
|
+
cancelNotification: function(id) {
|
|
1936
|
+
return this._invoke('cancelNotification', {id: id});
|
|
1937
|
+
},
|
|
1938
|
+
cancelAllNotifications: function() {
|
|
1939
|
+
return this._invoke('cancelAllNotifications');
|
|
1940
|
+
},
|
|
1941
|
+
getPendingNotifications: function() {
|
|
1942
|
+
return this._invoke('getPendingNotifications');
|
|
1943
|
+
},
|
|
1944
|
+
getProducts: function(productIds) {
|
|
1945
|
+
return this.iap.getProducts(productIds);
|
|
1946
|
+
},
|
|
1947
|
+
purchase: function(productId) {
|
|
1948
|
+
return this.iap.purchase(productId);
|
|
1949
|
+
},
|
|
1950
|
+
restorePurchases: function() {
|
|
1951
|
+
return this._invoke('restorePurchases');
|
|
1952
|
+
},
|
|
1953
|
+
signInWithApple: function() {
|
|
1954
|
+
return this._invoke('signInWithApple');
|
|
1955
|
+
},
|
|
1956
|
+
signInWithGoogle: function() {
|
|
1957
|
+
return Promise.reject(new Error('Google Sign-In is unavailable on iOS'));
|
|
1958
|
+
},
|
|
1959
|
+
startBluetoothScan: function() {
|
|
1960
|
+
return this._invoke('startBluetoothScan');
|
|
1961
|
+
},
|
|
1962
|
+
stopBluetoothScan: function() {
|
|
1963
|
+
return this._invoke('stopBluetoothScan');
|
|
1964
|
+
},
|
|
1965
|
+
requestHealthAuthorization: function(types) {
|
|
1966
|
+
return this._invoke('requestHealthAuthorization', {types: types || []});
|
|
1967
|
+
},
|
|
1968
|
+
getHealthData: function(type, startDate, endDate) {
|
|
1969
|
+
var start = startDate instanceof Date ? startDate.getTime() : startDate;
|
|
1970
|
+
var end = endDate instanceof Date ? endDate.getTime() : endDate;
|
|
1971
|
+
return this._invoke('getHealthData', {type: type, startDate: start, endDate: end});
|
|
1972
|
+
},
|
|
1973
|
+
requestFitnessAuthorization: function() {
|
|
1974
|
+
return Promise.reject(new Error('Android fitness APIs are unavailable on iOS'));
|
|
1975
|
+
},
|
|
1976
|
+
getFitnessData: function() {
|
|
1977
|
+
return Promise.reject(new Error('Android fitness APIs are unavailable on iOS'));
|
|
1978
|
+
},
|
|
1979
|
+
|
|
1785
1980
|
haptic: function(style) {
|
|
1786
1981
|
window.webkit.messageHandlers.craft.postMessage({action: 'haptic', style: style || 'medium'});
|
|
1787
1982
|
},
|
|
@@ -1863,12 +2058,58 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1863
2058
|
|
|
1864
2059
|
// Geolocation
|
|
1865
2060
|
geolocation: {
|
|
1866
|
-
getCurrentPosition: function() {
|
|
2061
|
+
getCurrentPosition: function(options) {
|
|
2062
|
+
options = options || {};
|
|
1867
2063
|
var self = window.craft;
|
|
1868
2064
|
var id = 'cb_' + (++self._callbackId);
|
|
1869
|
-
|
|
2065
|
+
var requestedTimeout = Number(options.timeout);
|
|
2066
|
+
var timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout >= 0
|
|
2067
|
+
? Math.min(requestedTimeout, 2147483647)
|
|
2068
|
+
: 30000;
|
|
1870
2069
|
return new Promise(function(resolve, reject) {
|
|
1871
|
-
|
|
2070
|
+
var timeout;
|
|
2071
|
+
self._callbacks[id] = {
|
|
2072
|
+
resolve: function(value) { clearTimeout(timeout); resolve(value); },
|
|
2073
|
+
reject: function(error) {
|
|
2074
|
+
clearTimeout(timeout);
|
|
2075
|
+
var locationErrorCode = error && ({
|
|
2076
|
+
'PERMISSION_DENIED': 1,
|
|
2077
|
+
'POSITION_UNAVAILABLE': 2,
|
|
2078
|
+
'NATIVE_CALL_FAILED': 2,
|
|
2079
|
+
'TIMEOUT': 3,
|
|
2080
|
+
'LOCATION_TIMEOUT': 3
|
|
2081
|
+
})[error.code];
|
|
2082
|
+
if (locationErrorCode) {
|
|
2083
|
+
error.name = 'GeolocationPositionError';
|
|
2084
|
+
error.code = locationErrorCode;
|
|
2085
|
+
}
|
|
2086
|
+
reject(error);
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
timeout = setTimeout(function() {
|
|
2090
|
+
if (!self._callbacks[id]) return;
|
|
2091
|
+
delete self._callbacks[id];
|
|
2092
|
+
var error = new Error('Location request timed out after ' + timeoutMs + 'ms');
|
|
2093
|
+
error.name = 'GeolocationPositionError';
|
|
2094
|
+
error.code = 3;
|
|
2095
|
+
error.bridge = true;
|
|
2096
|
+
reject(error);
|
|
2097
|
+
}, timeoutMs);
|
|
2098
|
+
try {
|
|
2099
|
+
window.webkit.messageHandlers.craft.postMessage({
|
|
2100
|
+
action: 'getCurrentPosition',
|
|
2101
|
+
callbackId: id,
|
|
2102
|
+
enableHighAccuracy: options.enableHighAccuracy === true,
|
|
2103
|
+
timeout: timeoutMs,
|
|
2104
|
+
maximumAge: Number.isFinite(Number(options.maximumAge))
|
|
2105
|
+
? Math.max(0, Number(options.maximumAge))
|
|
2106
|
+
: 0
|
|
2107
|
+
});
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
clearTimeout(timeout);
|
|
2110
|
+
delete self._callbacks[id];
|
|
2111
|
+
reject(error);
|
|
2112
|
+
}
|
|
1872
2113
|
});
|
|
1873
2114
|
},
|
|
1874
2115
|
watchPosition: function(callback) {
|
|
@@ -1938,7 +2179,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1938
2179
|
});
|
|
1939
2180
|
},
|
|
1940
2181
|
onNetworkChange: function(callback) {
|
|
1941
|
-
|
|
2182
|
+
this.offNetworkChange();
|
|
2183
|
+
this._networkChangeHandler = function(e) { callback(e.detail); };
|
|
2184
|
+
window.addEventListener('craftNetworkChange', this._networkChangeHandler);
|
|
2185
|
+
},
|
|
2186
|
+
offNetworkChange: function() {
|
|
2187
|
+
if (!this._networkChangeHandler) return;
|
|
2188
|
+
window.removeEventListener('craftNetworkChange', this._networkChangeHandler);
|
|
2189
|
+
this._networkChangeHandler = null;
|
|
1942
2190
|
},
|
|
1943
2191
|
|
|
1944
2192
|
// App review
|
|
@@ -1957,9 +2205,15 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1957
2205
|
var id = 'cb_' + (++this._callbackId);
|
|
1958
2206
|
window.webkit.messageHandlers.craft.postMessage({action: 'setFlashlight', enabled: enabled, callbackId: id});
|
|
1959
2207
|
return new Promise(function(resolve, reject) {
|
|
1960
|
-
self._callbacks[id] = {
|
|
2208
|
+
self._callbacks[id] = {
|
|
2209
|
+
resolve: function(value) { self._flashlightEnabled = enabled; resolve(value); },
|
|
2210
|
+
reject: reject
|
|
2211
|
+
};
|
|
1961
2212
|
});
|
|
1962
2213
|
},
|
|
2214
|
+
toggleFlashlight: function() {
|
|
2215
|
+
return this.setFlashlight(!this._flashlightEnabled);
|
|
2216
|
+
},
|
|
1963
2217
|
|
|
1964
2218
|
// Vibrate
|
|
1965
2219
|
vibrate: function(pattern) {
|
|
@@ -1985,6 +2239,18 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1985
2239
|
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
1986
2240
|
});
|
|
1987
2241
|
},
|
|
2242
|
+
onAppStateChange: function(callback) {
|
|
2243
|
+
this.offAppStateChange();
|
|
2244
|
+
this._appStateChangeHandler = function() {
|
|
2245
|
+
callback(document.visibilityState === 'visible' ? 'active' : 'background');
|
|
2246
|
+
};
|
|
2247
|
+
document.addEventListener('visibilitychange', this._appStateChangeHandler);
|
|
2248
|
+
},
|
|
2249
|
+
offAppStateChange: function() {
|
|
2250
|
+
if (!this._appStateChangeHandler) return;
|
|
2251
|
+
document.removeEventListener('visibilitychange', this._appStateChangeHandler);
|
|
2252
|
+
this._appStateChangeHandler = null;
|
|
2253
|
+
},
|
|
1988
2254
|
|
|
1989
2255
|
// Contacts
|
|
1990
2256
|
contacts: {
|
|
@@ -2471,8 +2737,6 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2471
2737
|
ota: {
|
|
2472
2738
|
_config: null,
|
|
2473
2739
|
_status: 'idle',
|
|
2474
|
-
_progressCallbacks: [],
|
|
2475
|
-
_statusCallbacks: [],
|
|
2476
2740
|
|
|
2477
2741
|
// OTA is not implemented natively. These five used to post
|
|
2478
2742
|
// to actions the switch below does not handle, and register
|
|
@@ -2513,12 +2777,10 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2513
2777
|
};
|
|
2514
2778
|
},
|
|
2515
2779
|
onProgress: function(callback) {
|
|
2516
|
-
|
|
2517
|
-
window.addEventListener('craftOTAProgress', function(e) { callback(e.detail); });
|
|
2780
|
+
throw new Error('craft.ota.onProgress is not implemented on this platform');
|
|
2518
2781
|
},
|
|
2519
2782
|
onStatusChange: function(callback) {
|
|
2520
|
-
|
|
2521
|
-
window.addEventListener('craftOTAStatus', function(e) { callback(e.detail.status); });
|
|
2783
|
+
throw new Error('craft.ota.onStatusChange is not implemented on this platform');
|
|
2522
2784
|
}
|
|
2523
2785
|
},
|
|
2524
2786
|
|
|
@@ -2629,8 +2891,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2629
2891
|
};
|
|
2630
2892
|
craft.liveActivity = {
|
|
2631
2893
|
start: function(options) { return craft._invoke('startLiveActivity', options || {}); },
|
|
2632
|
-
update: function(
|
|
2633
|
-
|
|
2894
|
+
update: function(idOrState, state) {
|
|
2895
|
+
if (typeof idOrState !== 'string') return craft._invoke('updateLiveActivity', idOrState || {});
|
|
2896
|
+
return craft._invoke('updateLiveActivity', Object.assign({}, state || {}, {id: idOrState}));
|
|
2897
|
+
},
|
|
2898
|
+
end: function(id, finalState) {
|
|
2899
|
+
if (typeof id !== 'string') return craft._invoke('endLiveActivity');
|
|
2900
|
+
return craft._invoke('endLiveActivity', Object.assign({}, finalState || {}, {id: id}));
|
|
2901
|
+
}
|
|
2634
2902
|
};
|
|
2635
2903
|
var shareApi = function(text) { return legacyShare(text); };
|
|
2636
2904
|
shareApi.share = function(options) { return craft._invoke('share', {options: options || {}}); };
|
|
@@ -2686,6 +2954,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2686
2954
|
|
|
2687
2955
|
// Mark DeepLinkManager as ready
|
|
2688
2956
|
DeepLinkManager.shared.setReady()
|
|
2957
|
+
CraftEventManager.shared.setReady()
|
|
2689
2958
|
}
|
|
2690
2959
|
|
|
2691
2960
|
// MARK: - Callback Helpers
|
|
@@ -2899,7 +3168,15 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2899
3168
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
2900
3169
|
picker.dismiss(animated: true)
|
|
2901
3170
|
|
|
2902
|
-
if let
|
|
3171
|
+
if let movieURL = info[.mediaURL] as? URL {
|
|
3172
|
+
do {
|
|
3173
|
+
let movieData = try Data(contentsOf: movieURL, options: .mappedIfSafe)
|
|
3174
|
+
let base64 = movieData.base64EncodedString()
|
|
3175
|
+
resolveCallback(pendingCallbackId, result: "data:video/quicktime;base64," + base64)
|
|
3176
|
+
} catch {
|
|
3177
|
+
rejectCallback(pendingCallbackId, error: "Failed to process video: \(error.localizedDescription)")
|
|
3178
|
+
}
|
|
3179
|
+
} else if let image = info[.originalImage] as? UIImage,
|
|
2903
3180
|
let imageData = image.jpegData(compressionQuality: 0.8) {
|
|
2904
3181
|
let base64 = imageData.base64EncodedString()
|
|
2905
3182
|
resolveCallback(pendingCallbackId, result: [
|
|
@@ -3036,7 +3313,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3036
3313
|
}
|
|
3037
3314
|
switch permission {
|
|
3038
3315
|
case "location", "locationAlways":
|
|
3039
|
-
let status = CLLocationManager.authorizationStatus
|
|
3316
|
+
let status = (locationManager ?? CLLocationManager()).authorizationStatus
|
|
3040
3317
|
let granted = status == .authorizedAlways || (permission == "location" && status == .authorizedWhenInUse)
|
|
3041
3318
|
resolveCallback(callbackId, result: permissionStatus(granted, denied: status == .denied, restricted: status == .restricted))
|
|
3042
3319
|
case "camera":
|
|
@@ -3084,11 +3361,27 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3084
3361
|
}
|
|
3085
3362
|
switch permission {
|
|
3086
3363
|
case "location", "locationAlways":
|
|
3364
|
+
guard let manager = locationManager else {
|
|
3365
|
+
rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
|
|
3366
|
+
return
|
|
3367
|
+
}
|
|
3368
|
+
manager.delegate = self
|
|
3369
|
+
let requiresAlways = permission == "locationAlways" || config.enableBackgroundLocation
|
|
3370
|
+
let status = manager.authorizationStatus
|
|
3371
|
+
let alreadyGranted = status == .authorizedAlways || (!requiresAlways && status == .authorizedWhenInUse)
|
|
3372
|
+
if alreadyGranted || status == .denied || status == .restricted {
|
|
3373
|
+
resolveCallback(callbackId, result: permissionStatus(alreadyGranted, denied: status == .denied, restricted: status == .restricted))
|
|
3374
|
+
return
|
|
3375
|
+
}
|
|
3376
|
+
if let pendingCallbackId = locationPermissionCallbackId {
|
|
3377
|
+
rejectCallback(pendingCallbackId, error: "A newer location permission request replaced this request", code: "REQUEST_REPLACED")
|
|
3378
|
+
}
|
|
3087
3379
|
locationPermissionCallbackId = callbackId
|
|
3088
|
-
|
|
3089
|
-
|
|
3380
|
+
locationPermissionRequiresAlways = requiresAlways
|
|
3381
|
+
if requiresAlways {
|
|
3382
|
+
manager.requestAlwaysAuthorization()
|
|
3090
3383
|
} else {
|
|
3091
|
-
|
|
3384
|
+
manager.requestWhenInUseAuthorization()
|
|
3092
3385
|
}
|
|
3093
3386
|
case "camera":
|
|
3094
3387
|
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
@@ -3134,11 +3427,60 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3134
3427
|
}
|
|
3135
3428
|
|
|
3136
3429
|
// MARK: - Geolocation
|
|
3137
|
-
private func getCurrentPosition(callbackId: String?) {
|
|
3138
|
-
|
|
3430
|
+
private func getCurrentPosition(body: [String: Any], callbackId: String?) {
|
|
3431
|
+
guard let manager = locationManager else {
|
|
3432
|
+
rejectCallback(callbackId, error: "Geolocation is disabled", code: "CAPABILITY_DISABLED")
|
|
3433
|
+
return
|
|
3434
|
+
}
|
|
3435
|
+
manager.delegate = self
|
|
3436
|
+
manager.desiredAccuracy = body["enableHighAccuracy"] as? Bool == true
|
|
3437
|
+
? kCLLocationAccuracyBest
|
|
3438
|
+
: kCLLocationAccuracyHundredMeters
|
|
3439
|
+
|
|
3440
|
+
if let pendingCallbackId = singleLocationCallbackId {
|
|
3441
|
+
finishSingleLocationRequest()
|
|
3442
|
+
rejectCallback(pendingCallbackId, error: "A newer location request replaced this request", code: "POSITION_UNAVAILABLE")
|
|
3443
|
+
}
|
|
3139
3444
|
singleLocationCallbackId = callbackId
|
|
3445
|
+
let maximumAge = max(0, (body["maximumAge"] as? NSNumber)?.doubleValue ?? 0)
|
|
3446
|
+
if maximumAge > 0,
|
|
3447
|
+
let cachedLocation = manager.location,
|
|
3448
|
+
max(0, Date().timeIntervalSince(cachedLocation.timestamp) * 1000) <= maximumAge {
|
|
3449
|
+
finishSingleLocationRequest()
|
|
3450
|
+
resolveCallback(callbackId, result: locationData(cachedLocation))
|
|
3451
|
+
return
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
let timeoutMs = max(0, (body["timeout"] as? NSNumber)?.doubleValue ?? 30_000)
|
|
3455
|
+
let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
|
3456
|
+
guard let self, self.singleLocationCallbackId == callbackId else { return }
|
|
3457
|
+
self.finishSingleLocationRequest()
|
|
3458
|
+
self.rejectCallback(callbackId, error: "Location request timed out", code: "LOCATION_TIMEOUT")
|
|
3459
|
+
}
|
|
3460
|
+
singleLocationTimeoutWorkItem = timeoutWorkItem
|
|
3461
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(min(timeoutMs + 100, Double(Int.max)))), execute: timeoutWorkItem)
|
|
3140
3462
|
requestLocationAuthorization()
|
|
3141
|
-
|
|
3463
|
+
manager.requestLocation()
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
private func finishSingleLocationRequest() {
|
|
3467
|
+
singleLocationTimeoutWorkItem?.cancel()
|
|
3468
|
+
singleLocationTimeoutWorkItem = nil
|
|
3469
|
+
singleLocationCallbackId = nil
|
|
3470
|
+
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
private func locationData(_ location: CLLocation) -> [String: Any] {
|
|
3474
|
+
[
|
|
3475
|
+
"latitude": location.coordinate.latitude,
|
|
3476
|
+
"longitude": location.coordinate.longitude,
|
|
3477
|
+
"altitude": location.altitude,
|
|
3478
|
+
"accuracy": location.horizontalAccuracy,
|
|
3479
|
+
"altitudeAccuracy": location.verticalAccuracy,
|
|
3480
|
+
"heading": location.course,
|
|
3481
|
+
"speed": location.speed,
|
|
3482
|
+
"timestamp": location.timestamp.timeIntervalSince1970 * 1000
|
|
3483
|
+
]
|
|
3142
3484
|
}
|
|
3143
3485
|
|
|
3144
3486
|
private func watchPosition(callbackId: String?) {
|
|
@@ -3326,20 +3668,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3326
3668
|
|
|
3327
3669
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
3328
3670
|
guard let location = locations.last else { return }
|
|
3329
|
-
let data
|
|
3330
|
-
"latitude": location.coordinate.latitude,
|
|
3331
|
-
"longitude": location.coordinate.longitude,
|
|
3332
|
-
"altitude": location.altitude,
|
|
3333
|
-
"accuracy": location.horizontalAccuracy,
|
|
3334
|
-
"altitudeAccuracy": location.verticalAccuracy,
|
|
3335
|
-
"heading": location.course,
|
|
3336
|
-
"speed": location.speed,
|
|
3337
|
-
"timestamp": location.timestamp.timeIntervalSince1970 * 1000
|
|
3338
|
-
]
|
|
3671
|
+
let data = locationData(location)
|
|
3339
3672
|
|
|
3340
3673
|
if let callbackId = singleLocationCallbackId {
|
|
3674
|
+
finishSingleLocationRequest()
|
|
3341
3675
|
resolveCallback(callbackId, result: data)
|
|
3342
|
-
singleLocationCallbackId = nil
|
|
3343
3676
|
}
|
|
3344
3677
|
|
|
3345
3678
|
appendRecordedLocation(data)
|
|
@@ -3349,8 +3682,13 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3349
3682
|
}
|
|
3350
3683
|
|
|
3351
3684
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3685
|
+
let callbackId = singleLocationCallbackId
|
|
3686
|
+
finishSingleLocationRequest()
|
|
3687
|
+
let nativeError = error as NSError
|
|
3688
|
+
let code = nativeError.domain == kCLErrorDomain && nativeError.code == CLError.Code.denied.rawValue
|
|
3689
|
+
? "PERMISSION_DENIED"
|
|
3690
|
+
: "POSITION_UNAVAILABLE"
|
|
3691
|
+
rejectCallback(callbackId, error: error.localizedDescription, code: code)
|
|
3354
3692
|
sendToWeb("craftLocationError", data: ["message": error.localizedDescription])
|
|
3355
3693
|
}
|
|
3356
3694
|
|
|
@@ -3358,9 +3696,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3358
3696
|
guard let callbackId = locationPermissionCallbackId else { return }
|
|
3359
3697
|
let status = manager.authorizationStatus
|
|
3360
3698
|
if status == .notDetermined { return }
|
|
3361
|
-
|
|
3699
|
+
if locationPermissionRequiresAlways && status == .authorizedWhenInUse { return }
|
|
3700
|
+
let granted = status == .authorizedAlways || (!locationPermissionRequiresAlways && status == .authorizedWhenInUse)
|
|
3362
3701
|
resolveCallback(callbackId, result: permissionStatus(granted, denied: status == .denied, restricted: status == .restricted))
|
|
3363
3702
|
locationPermissionCallbackId = nil
|
|
3703
|
+
locationPermissionRequiresAlways = false
|
|
3364
3704
|
}
|
|
3365
3705
|
|
|
3366
3706
|
// MARK: - Memory Usage (for Profiling)
|
|
@@ -3478,7 +3818,11 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3478
3818
|
|
|
3479
3819
|
// MARK: - Contacts
|
|
3480
3820
|
private func getContacts(callbackId: String?) {
|
|
3481
|
-
|
|
3821
|
+
guard let store = contactStore else {
|
|
3822
|
+
rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
|
|
3823
|
+
return
|
|
3824
|
+
}
|
|
3825
|
+
store.requestAccess(for: .contacts) { [weak self] granted, error in
|
|
3482
3826
|
guard granted else {
|
|
3483
3827
|
self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
|
|
3484
3828
|
return
|
|
@@ -3489,7 +3833,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3489
3833
|
|
|
3490
3834
|
var contacts: [[String: Any]] = []
|
|
3491
3835
|
do {
|
|
3492
|
-
try
|
|
3836
|
+
try store.enumerateContacts(with: request) { contact, _ in
|
|
3493
3837
|
var phones: [String] = []
|
|
3494
3838
|
for phone in contact.phoneNumbers {
|
|
3495
3839
|
phones.append(phone.value.stringValue)
|
|
@@ -3515,9 +3859,13 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3515
3859
|
}
|
|
3516
3860
|
|
|
3517
3861
|
private func addContact(_ data: [String: Any], callbackId: String?) {
|
|
3518
|
-
|
|
3862
|
+
guard let store = contactStore else {
|
|
3863
|
+
rejectCallback(callbackId, error: "Contacts access is disabled", code: "CAPABILITY_DISABLED")
|
|
3864
|
+
return
|
|
3865
|
+
}
|
|
3866
|
+
store.requestAccess(for: .contacts) { [weak self] granted, error in
|
|
3519
3867
|
guard granted else {
|
|
3520
|
-
self?.rejectCallback(callbackId, error: "Permission denied")
|
|
3868
|
+
self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
|
|
3521
3869
|
return
|
|
3522
3870
|
}
|
|
3523
3871
|
|
|
@@ -3535,7 +3883,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3535
3883
|
saveRequest.add(contact, toContainerWithIdentifier: nil)
|
|
3536
3884
|
|
|
3537
3885
|
do {
|
|
3538
|
-
try
|
|
3886
|
+
try store.execute(saveRequest)
|
|
3539
3887
|
self?.resolveCallback(callbackId, result: contact.identifier)
|
|
3540
3888
|
} catch {
|
|
3541
3889
|
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
@@ -3545,17 +3893,23 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3545
3893
|
|
|
3546
3894
|
// MARK: - Calendar
|
|
3547
3895
|
private func getCalendarEvents(startDate: Double?, endDate: Double?, callbackId: String?) {
|
|
3548
|
-
|
|
3896
|
+
guard let store = eventStore else {
|
|
3897
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
3898
|
+
return
|
|
3899
|
+
}
|
|
3900
|
+
store.requestAccess(to: .event) { [weak self] granted, error in
|
|
3549
3901
|
guard granted else {
|
|
3550
3902
|
self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
|
|
3551
3903
|
return
|
|
3552
3904
|
}
|
|
3553
3905
|
|
|
3554
|
-
let start = startDate
|
|
3555
|
-
let end = endDate
|
|
3906
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
3907
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) }
|
|
3908
|
+
?? Calendar.current.date(byAdding: .month, value: 1, to: Date())
|
|
3909
|
+
?? Date()
|
|
3556
3910
|
|
|
3557
|
-
let predicate =
|
|
3558
|
-
let events =
|
|
3911
|
+
let predicate = store.predicateForEvents(withStart: start, end: end, calendars: nil)
|
|
3912
|
+
let events = store.events(matching: predicate)
|
|
3559
3913
|
|
|
3560
3914
|
let eventData: [[String: Any]] = events.map { event in
|
|
3561
3915
|
return [
|
|
@@ -3574,13 +3928,17 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3574
3928
|
}
|
|
3575
3929
|
|
|
3576
3930
|
private func createCalendarEvent(_ data: [String: Any], callbackId: String?) {
|
|
3577
|
-
|
|
3931
|
+
guard let store = eventStore else {
|
|
3932
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
3933
|
+
return
|
|
3934
|
+
}
|
|
3935
|
+
store.requestAccess(to: .event) { [weak self] granted, error in
|
|
3578
3936
|
guard granted else {
|
|
3579
|
-
self?.rejectCallback(callbackId, error: "Permission denied")
|
|
3937
|
+
self?.rejectCallback(callbackId, error: error?.localizedDescription ?? "Permission denied")
|
|
3580
3938
|
return
|
|
3581
3939
|
}
|
|
3582
3940
|
|
|
3583
|
-
let event = EKEvent(eventStore:
|
|
3941
|
+
let event = EKEvent(eventStore: store)
|
|
3584
3942
|
event.title = data["title"] as? String ?? ""
|
|
3585
3943
|
event.location = data["location"] as? String
|
|
3586
3944
|
event.notes = data["notes"] as? String
|
|
@@ -3592,11 +3950,15 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3592
3950
|
event.endDate = Date(timeIntervalSince1970: end / 1000)
|
|
3593
3951
|
}
|
|
3594
3952
|
event.isAllDay = data["isAllDay"] as? Bool ?? false
|
|
3595
|
-
event.calendar =
|
|
3953
|
+
event.calendar = store.defaultCalendarForNewEvents
|
|
3596
3954
|
|
|
3597
3955
|
do {
|
|
3598
|
-
try
|
|
3599
|
-
|
|
3956
|
+
try store.save(event, span: .thisEvent)
|
|
3957
|
+
guard let identifier = event.eventIdentifier else {
|
|
3958
|
+
self?.rejectCallback(callbackId, error: "Saved event has no identifier")
|
|
3959
|
+
return
|
|
3960
|
+
}
|
|
3961
|
+
self?.resolveCallback(callbackId, result: identifier)
|
|
3600
3962
|
} catch {
|
|
3601
3963
|
self?.rejectCallback(callbackId, error: error.localizedDescription)
|
|
3602
3964
|
}
|
|
@@ -3604,13 +3966,17 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3604
3966
|
}
|
|
3605
3967
|
|
|
3606
3968
|
private func deleteCalendarEvent(_ eventId: String, callbackId: String?) {
|
|
3607
|
-
guard let
|
|
3969
|
+
guard let store = eventStore else {
|
|
3970
|
+
rejectCallback(callbackId, error: "Calendar access is disabled", code: "CAPABILITY_DISABLED")
|
|
3971
|
+
return
|
|
3972
|
+
}
|
|
3973
|
+
guard let event = store.event(withIdentifier: eventId) else {
|
|
3608
3974
|
rejectCallback(callbackId, error: "Event not found")
|
|
3609
3975
|
return
|
|
3610
3976
|
}
|
|
3611
3977
|
|
|
3612
3978
|
do {
|
|
3613
|
-
try
|
|
3979
|
+
try store.remove(event, span: .thisEvent)
|
|
3614
3980
|
resolveCallback(callbackId, result: true)
|
|
3615
3981
|
} catch {
|
|
3616
3982
|
rejectCallback(callbackId, error: error.localizedDescription)
|
|
@@ -4388,10 +4754,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4388
4754
|
}
|
|
4389
4755
|
|
|
4390
4756
|
private func updateLiveActivity(body: [String: Any], callbackId: String?) {
|
|
4391
|
-
guard #available(iOS 16.2, *)
|
|
4757
|
+
guard #available(iOS 16.2, *) else {
|
|
4392
4758
|
rejectCallback(callbackId, error: "No Live Activity is running")
|
|
4393
4759
|
return
|
|
4394
4760
|
}
|
|
4761
|
+
let activities = Activity<CraftActivityAttributes>.activities
|
|
4762
|
+
let activity: Activity<CraftActivityAttributes>
|
|
4763
|
+
if let activityId = body["id"] as? String {
|
|
4764
|
+
guard let matchingActivity = activities.first(where: { $0.id == activityId }) else {
|
|
4765
|
+
rejectCallback(callbackId, error: "No Live Activity with id \(activityId) is running")
|
|
4766
|
+
return
|
|
4767
|
+
}
|
|
4768
|
+
activity = matchingActivity
|
|
4769
|
+
} else {
|
|
4770
|
+
guard let currentActivity = activities.first else {
|
|
4771
|
+
rejectCallback(callbackId, error: "No Live Activity is running")
|
|
4772
|
+
return
|
|
4773
|
+
}
|
|
4774
|
+
activity = currentActivity
|
|
4775
|
+
}
|
|
4395
4776
|
let current = activity.content.state
|
|
4396
4777
|
let state = CraftActivityAttributes.ContentState(
|
|
4397
4778
|
status: body["status"] as? String ?? current.status,
|
|
@@ -4405,13 +4786,45 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
4405
4786
|
}
|
|
4406
4787
|
}
|
|
4407
4788
|
|
|
4408
|
-
private func endLiveActivity(callbackId: String?) {
|
|
4409
|
-
guard #available(iOS 16.2, *)
|
|
4789
|
+
private func endLiveActivity(body: [String: Any], callbackId: String?) {
|
|
4790
|
+
guard #available(iOS 16.2, *) else {
|
|
4410
4791
|
resolveCallback(callbackId, result: ["ended": false])
|
|
4411
4792
|
return
|
|
4412
4793
|
}
|
|
4794
|
+
let activities = Activity<CraftActivityAttributes>.activities
|
|
4795
|
+
let activity: Activity<CraftActivityAttributes>
|
|
4796
|
+
if let activityId = body["id"] as? String {
|
|
4797
|
+
guard let matchingActivity = activities.first(where: { $0.id == activityId }) else {
|
|
4798
|
+
rejectCallback(callbackId, error: "No Live Activity with id \(activityId) is running")
|
|
4799
|
+
return
|
|
4800
|
+
}
|
|
4801
|
+
activity = matchingActivity
|
|
4802
|
+
} else {
|
|
4803
|
+
guard let currentActivity = activities.first else {
|
|
4804
|
+
resolveCallback(callbackId, result: ["ended": false])
|
|
4805
|
+
return
|
|
4806
|
+
}
|
|
4807
|
+
activity = currentActivity
|
|
4808
|
+
}
|
|
4809
|
+
let hasFinalState = body["status"] != nil
|
|
4810
|
+
|| body["distanceMeters"] != nil
|
|
4811
|
+
|| body["durationSeconds"] != nil
|
|
4812
|
+
|| body["progress"] != nil
|
|
4813
|
+
let finalContent: ActivityContent<CraftActivityAttributes.ContentState>?
|
|
4814
|
+
if hasFinalState {
|
|
4815
|
+
let current = activity.content.state
|
|
4816
|
+
let state = CraftActivityAttributes.ContentState(
|
|
4817
|
+
status: body["status"] as? String ?? current.status,
|
|
4818
|
+
distanceMeters: body["distanceMeters"] as? Double ?? current.distanceMeters,
|
|
4819
|
+
durationSeconds: body["durationSeconds"] as? Double ?? current.durationSeconds,
|
|
4820
|
+
progress: min(max(body["progress"] as? Double ?? current.progress, 0), 1)
|
|
4821
|
+
)
|
|
4822
|
+
finalContent = ActivityContent(state: state, staleDate: nil)
|
|
4823
|
+
} else {
|
|
4824
|
+
finalContent = nil
|
|
4825
|
+
}
|
|
4413
4826
|
Task {
|
|
4414
|
-
await activity.end(
|
|
4827
|
+
await activity.end(finalContent, dismissalPolicy: .default)
|
|
4415
4828
|
resolveCallback(callbackId, result: ["ended": true])
|
|
4416
4829
|
}
|
|
4417
4830
|
}
|