craft-native 0.0.91 → 0.0.92
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.d.ts +36 -1
- package/dist/android/src/index.js +47 -0
- package/dist/android/templates/CraftBridge.kt.template +263 -31
- package/dist/android/templates/CraftNative.kt.template +26 -7
- package/dist/android/templates/MainActivity.kt.template +19 -8
- package/dist/api/mobile.d.ts +12 -2
- package/dist/cli.js +50 -3
- package/dist/index.cjs +12 -5
- package/dist/index.js +12 -5
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +233 -32
- package/dist/mobile.js +16 -8
- package/package.json +1 -1
|
@@ -172,9 +172,16 @@ class DeepLinkManager {
|
|
|
172
172
|
static let shared = DeepLinkManager()
|
|
173
173
|
|
|
174
174
|
private var initialURL: URL?
|
|
175
|
-
|
|
175
|
+
// Every link that arrived while no page could receive it, in order. This
|
|
176
|
+
// was a single slot, so a second link before the bridge was ready replaced
|
|
177
|
+
// the first; on a cold start, that could be the link that launched the app.
|
|
178
|
+
private var pendingURLs: [URL] = []
|
|
176
179
|
private weak var webView: WKWebView?
|
|
177
180
|
private var isReady = false
|
|
181
|
+
// Whether a page has ever become ready. A link that arrives before that is
|
|
182
|
+
// the one the app was opened with; a link that arrives later is not, and
|
|
183
|
+
// is no answer to getInitialURL.
|
|
184
|
+
private var hasBeenReady = false
|
|
178
185
|
|
|
179
186
|
private init() {}
|
|
180
187
|
|
|
@@ -182,26 +189,33 @@ class DeepLinkManager {
|
|
|
182
189
|
self.webView = webView
|
|
183
190
|
}
|
|
184
191
|
|
|
192
|
+
// A navigation started, so the page that would have received a link is
|
|
193
|
+
// going away. Without this a link arriving during a reload was dispatched
|
|
194
|
+
// into the page being torn down and lost.
|
|
195
|
+
func setLoading() {
|
|
196
|
+
isReady = false
|
|
197
|
+
}
|
|
198
|
+
|
|
185
199
|
func setReady() {
|
|
186
200
|
isReady = true
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
201
|
+
let firstPage = !hasBeenReady
|
|
202
|
+
hasBeenReady = true
|
|
203
|
+
let urls = pendingURLs
|
|
204
|
+
pendingURLs.removeAll()
|
|
205
|
+
for url in urls {
|
|
206
|
+
dispatchDeepLink(url, initial: firstPage && url == initialURL)
|
|
191
207
|
}
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
func handleURL(_ url: URL) {
|
|
195
|
-
|
|
196
|
-
if initialURL == nil {
|
|
211
|
+
if initialURL == nil && !hasBeenReady {
|
|
197
212
|
initialURL = url
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
if isReady && webView != nil {
|
|
201
|
-
dispatchDeepLink(url)
|
|
216
|
+
dispatchDeepLink(url, initial: false)
|
|
202
217
|
} else {
|
|
203
|
-
|
|
204
|
-
pendingURL = url
|
|
218
|
+
pendingURLs.append(url)
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
|
|
@@ -209,7 +223,7 @@ class DeepLinkManager {
|
|
|
209
223
|
return initialURL
|
|
210
224
|
}
|
|
211
225
|
|
|
212
|
-
private func dispatchDeepLink(_ url: URL) {
|
|
226
|
+
private func dispatchDeepLink(_ url: URL, initial: Bool) {
|
|
213
227
|
guard let webView = webView else { return }
|
|
214
228
|
|
|
215
229
|
// Parse URL components
|
|
@@ -218,7 +232,10 @@ class DeepLinkManager {
|
|
|
218
232
|
"scheme": url.scheme ?? "",
|
|
219
233
|
"host": url.host ?? "",
|
|
220
234
|
"path": url.path,
|
|
221
|
-
"query": url.query ?? ""
|
|
235
|
+
"query": url.query ?? "",
|
|
236
|
+
// Lets the page tell the launch link apart from later ones, so a
|
|
237
|
+
// page that reads getInitialURL is not also handed it again.
|
|
238
|
+
"initial": initial
|
|
222
239
|
]
|
|
223
240
|
|
|
224
241
|
// Parse query parameters
|
|
@@ -650,16 +667,23 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
650
667
|
switch action {
|
|
651
668
|
case "startListening":
|
|
652
669
|
if config.enableSpeechRecognition {
|
|
670
|
+
// Answers that the request was taken. Authorization and the
|
|
671
|
+
// recognizer answer later, as craftSpeech* events; waiting
|
|
672
|
+
// for them here would leave the promise open on every path
|
|
673
|
+
// that ends without one.
|
|
653
674
|
startSpeechRecognition()
|
|
675
|
+
resolveCallback(callbackId, result: true)
|
|
654
676
|
} else {
|
|
655
677
|
rejectCallback(callbackId, error: "Speech recognition is disabled", code: "CAPABILITY_DISABLED")
|
|
656
678
|
}
|
|
657
679
|
case "stopListening":
|
|
658
680
|
stopSpeechRecognition()
|
|
681
|
+
resolveCallback(callbackId, result: true)
|
|
659
682
|
case "haptic":
|
|
660
683
|
if config.enableHaptics {
|
|
661
684
|
let style = body["style"] as? String ?? "medium"
|
|
662
685
|
triggerHaptic(style: style)
|
|
686
|
+
resolveCallback(callbackId, result: true)
|
|
663
687
|
} else {
|
|
664
688
|
rejectCallback(callbackId, error: "Haptics is disabled", code: "CAPABILITY_DISABLED")
|
|
665
689
|
}
|
|
@@ -1400,6 +1424,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1400
1424
|
|
|
1401
1425
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
1402
1426
|
CraftEventManager.shared.setLoading()
|
|
1427
|
+
DeepLinkManager.shared.setLoading()
|
|
1403
1428
|
}
|
|
1404
1429
|
|
|
1405
1430
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
@@ -1445,11 +1470,29 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1445
1470
|
|
|
1446
1471
|
private func isTrustedURL(_ url: URL?) -> Bool {
|
|
1447
1472
|
guard let url = url else { return false }
|
|
1448
|
-
if url.scheme == "craft" && url.host == "app" { return true }
|
|
1449
1473
|
return isTrustedOrigin(scheme: url.scheme ?? "", host: url.host ?? "", port: url.port ?? 0)
|
|
1450
1474
|
}
|
|
1451
1475
|
|
|
1476
|
+
/// The single answer to "may this origin reach native?".
|
|
1477
|
+
///
|
|
1478
|
+
/// The bundled app is served from craft://app by
|
|
1479
|
+
/// `BundledAssetSchemeHandler`, and it is the only content a generated
|
|
1480
|
+
/// app loads when no dev server is configured. That origin used to be
|
|
1481
|
+
/// trusted for *navigation* only: `isTrustedURL` carried the clause,
|
|
1482
|
+
/// while the `userContentController` guard called this function
|
|
1483
|
+
/// directly and fell through to the https/localhost check. So every
|
|
1484
|
+
/// bridge call from the app's own page was answered with
|
|
1485
|
+
///
|
|
1486
|
+
/// Blocked Craft bridge message from untrusted origin: craft://app
|
|
1487
|
+
///
|
|
1488
|
+
/// and its promise never settled — the whole native surface was dead
|
|
1489
|
+
/// in the default configuration, which is the one every generated app
|
|
1490
|
+
/// ships with. Two guards that had to agree, and did not. There is one
|
|
1491
|
+
/// now, and `scripts/mobile-e2e.ts` runs a real round trip on a booted
|
|
1492
|
+
/// simulator so a future hardening pass cannot quietly take the bridge
|
|
1493
|
+
/// away again.
|
|
1452
1494
|
private func isTrustedOrigin(scheme: String, host: String, port: Int) -> Bool {
|
|
1495
|
+
if scheme == "craft" && host == "app" { return true }
|
|
1453
1496
|
if scheme == "file" { return true }
|
|
1454
1497
|
guard scheme == "https" || (scheme == "http" && ["localhost", "127.0.0.1", "::1"].contains(host)) else { return false }
|
|
1455
1498
|
let defaultPort = scheme == "https" ? 443 : 80
|
|
@@ -1977,20 +2020,62 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1977
2020
|
return Promise.reject(new Error('Android fitness APIs are unavailable on iOS'));
|
|
1978
2021
|
},
|
|
1979
2022
|
|
|
2023
|
+
// Resolves true once UIKit has taken the haptic, and rejects
|
|
2024
|
+
// CAPABILITY_DISABLED when enableHaptics is off.
|
|
1980
2025
|
haptic: function(style) {
|
|
1981
|
-
window.
|
|
2026
|
+
var self = window.craft;
|
|
2027
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2028
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'haptic', style: style || 'medium', callbackId: id});
|
|
2029
|
+
return new Promise(function(resolve, reject) {
|
|
2030
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2031
|
+
});
|
|
1982
2032
|
},
|
|
1983
2033
|
|
|
2034
|
+
// Resolves true once native has taken the request, not once
|
|
2035
|
+
// audio is flowing. What happens next (a prompt declined, no
|
|
2036
|
+
// recognizer, a transcript) still arrives as craftSpeech*
|
|
2037
|
+
// events, because none of it is known when this answers.
|
|
1984
2038
|
startListening: function() {
|
|
1985
|
-
window.
|
|
2039
|
+
var self = window.craft;
|
|
2040
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2041
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'startListening', callbackId: id});
|
|
2042
|
+
return new Promise(function(resolve, reject) {
|
|
2043
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2044
|
+
});
|
|
1986
2045
|
},
|
|
1987
2046
|
|
|
1988
2047
|
stopListening: function() {
|
|
1989
|
-
window.
|
|
2048
|
+
var self = window.craft;
|
|
2049
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2050
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'stopListening', callbackId: id});
|
|
2051
|
+
return new Promise(function(resolve, reject) {
|
|
2052
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2053
|
+
});
|
|
1990
2054
|
},
|
|
1991
2055
|
|
|
1992
2056
|
share: function(text) {
|
|
1993
|
-
|
|
2057
|
+
return this._share({text: text});
|
|
2058
|
+
},
|
|
2059
|
+
|
|
2060
|
+
// Both share entry points come through here, because the
|
|
2061
|
+
// native side answers them the same way: `true` when the
|
|
2062
|
+
// person finished an activity, `false` when they dismissed
|
|
2063
|
+
// the sheet, a rejection when sharing is disabled or there is
|
|
2064
|
+
// nothing to share. The flat `share` used to post with no
|
|
2065
|
+
// callbackId, so every one of those answers stopped at the nil
|
|
2066
|
+
// guard in resolveCallback and the page got `undefined`.
|
|
2067
|
+
//
|
|
2068
|
+
// No timeout, unlike `_invoke`'s thirty seconds. The sheet
|
|
2069
|
+
// waits on a person, and someone slower than that to pick an
|
|
2070
|
+
// app would be told the share failed while it was still on
|
|
2071
|
+
// screen, with the real answer then dropped.
|
|
2072
|
+
_share: function(payload) {
|
|
2073
|
+
var self = this;
|
|
2074
|
+
var id = 'cb_' + (++this._callbackId);
|
|
2075
|
+
window.webkit.messageHandlers.craft.postMessage(Object.assign({}, payload, {action: 'share', callbackId: id}));
|
|
2076
|
+
return new Promise(function(resolve, reject) {
|
|
2077
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2078
|
+
});
|
|
1994
2079
|
},
|
|
1995
2080
|
|
|
1996
2081
|
openCamera: function() {
|
|
@@ -2112,12 +2197,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2112
2197
|
}
|
|
2113
2198
|
});
|
|
2114
2199
|
},
|
|
2200
|
+
// Resolves true once updates have started, before any
|
|
2201
|
+
// authorization answer. A later refusal arrives as a
|
|
2202
|
+
// craftLocationError event.
|
|
2115
2203
|
watchPosition: function(callback) {
|
|
2204
|
+
var self = window.craft;
|
|
2205
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2116
2206
|
window.addEventListener('craftLocationUpdate', function(e) { callback(e.detail); });
|
|
2117
|
-
window.webkit.messageHandlers.craft.postMessage({action: 'watchPosition'});
|
|
2207
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'watchPosition', callbackId: id});
|
|
2208
|
+
return new Promise(function(resolve, reject) {
|
|
2209
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2210
|
+
});
|
|
2118
2211
|
},
|
|
2119
2212
|
clearWatch: function() {
|
|
2120
|
-
window.
|
|
2213
|
+
var self = window.craft;
|
|
2214
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2215
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'clearWatch', callbackId: id});
|
|
2216
|
+
return new Promise(function(resolve, reject) {
|
|
2217
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2218
|
+
});
|
|
2121
2219
|
}
|
|
2122
2220
|
},
|
|
2123
2221
|
|
|
@@ -2217,7 +2315,12 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2217
2315
|
|
|
2218
2316
|
// Vibrate
|
|
2219
2317
|
vibrate: function(pattern) {
|
|
2220
|
-
window.
|
|
2318
|
+
var self = window.craft;
|
|
2319
|
+
var id = 'cb_' + (++self._callbackId);
|
|
2320
|
+
window.webkit.messageHandlers.craft.postMessage({action: 'vibrate', pattern: pattern, callbackId: id});
|
|
2321
|
+
return new Promise(function(resolve, reject) {
|
|
2322
|
+
self._callbacks[id] = {resolve: resolve, reject: reject};
|
|
2323
|
+
});
|
|
2221
2324
|
},
|
|
2222
2325
|
|
|
2223
2326
|
// Open URL
|
|
@@ -2394,7 +2497,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2394
2497
|
|
|
2395
2498
|
// Deep Links
|
|
2396
2499
|
onDeepLink: function(callback) {
|
|
2397
|
-
window.
|
|
2500
|
+
return window.craft._subscribeDeepLinks(callback);
|
|
2398
2501
|
},
|
|
2399
2502
|
|
|
2400
2503
|
// Background Tasks
|
|
@@ -2722,6 +2825,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2722
2825
|
deepLinks: {
|
|
2723
2826
|
getInitialURL: function() {
|
|
2724
2827
|
var self = window.craft;
|
|
2828
|
+
self._claimInitialDeepLink();
|
|
2725
2829
|
var id = 'cb_' + (++self._callbackId);
|
|
2726
2830
|
window.webkit.messageHandlers.craft.postMessage({action: 'getInitialURL', callbackId: id});
|
|
2727
2831
|
return new Promise(function(resolve, reject) {
|
|
@@ -2729,7 +2833,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2729
2833
|
});
|
|
2730
2834
|
},
|
|
2731
2835
|
onLink: function(callback) {
|
|
2732
|
-
window.
|
|
2836
|
+
return window.craft._subscribeDeepLinks(callback);
|
|
2733
2837
|
}
|
|
2734
2838
|
},
|
|
2735
2839
|
|
|
@@ -2792,6 +2896,61 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2792
2896
|
// Stable, versioned mobile contract consumed by craft-native/mobile.
|
|
2793
2897
|
// Legacy flat methods remain available while every public SDK method
|
|
2794
2898
|
// is routed through this nested contract.
|
|
2899
|
+
// Links that arrive before anything is listening (#198).
|
|
2900
|
+
//
|
|
2901
|
+
// The native side dispatches a link the moment this script has
|
|
2902
|
+
// run, and on a cold start that is the link the app was opened
|
|
2903
|
+
// with. No page code can have called onLink by then: onLink is
|
|
2904
|
+
// defined by this very script. So every such link was dispatched
|
|
2905
|
+
// to nobody, and a page that subscribes, rather than asking
|
|
2906
|
+
// getInitialURL, never learned how it was opened.
|
|
2907
|
+
//
|
|
2908
|
+
// Held here instead, and handed to the first subscriber on the
|
|
2909
|
+
// next turn, so an unsubscribe returned in the same tick still
|
|
2910
|
+
// applies. The launch link belongs to getInitialURL once the page
|
|
2911
|
+
// has called it, whether native has dispatched it yet or not, so a
|
|
2912
|
+
// page that does both in the same tick, in either order, gets it
|
|
2913
|
+
// once. That includes a craftReady handler, which runs before
|
|
2914
|
+
// native dispatches anything. A page that asks getInitialURL only
|
|
2915
|
+
// later, after an await, should skip `initial` in onLink.
|
|
2916
|
+
//
|
|
2917
|
+
// The same block runs on Android (#215), where this script can run
|
|
2918
|
+
// twice in one document, so its state lives on window.
|
|
2919
|
+
(function installDeepLinkReplay(craft) {
|
|
2920
|
+
var replay = window.__craftDeepLinkReplay;
|
|
2921
|
+
if (!replay) {
|
|
2922
|
+
replay = window.__craftDeepLinkReplay = {undelivered: [], subscribed: false, initialClaimed: false};
|
|
2923
|
+
window.addEventListener('craftDeepLink', function(e) {
|
|
2924
|
+
if (!replay.subscribed && !claimed(e.detail)) replay.undelivered.push(e.detail);
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
function claimed(detail) {
|
|
2928
|
+
return replay.initialClaimed && detail && detail.initial;
|
|
2929
|
+
}
|
|
2930
|
+
craft._subscribeDeepLinks = function(callback) {
|
|
2931
|
+
var active = true;
|
|
2932
|
+
var listener = function(e) { if (!claimed(e.detail)) callback(e.detail); };
|
|
2933
|
+
window.addEventListener('craftDeepLink', listener);
|
|
2934
|
+
if (!replay.subscribed) {
|
|
2935
|
+
replay.subscribed = true;
|
|
2936
|
+
setTimeout(function() {
|
|
2937
|
+
var pending = replay.undelivered;
|
|
2938
|
+
replay.undelivered = [];
|
|
2939
|
+
if (!active) return;
|
|
2940
|
+
pending.forEach(function(detail) { callback(detail); });
|
|
2941
|
+
}, 0);
|
|
2942
|
+
}
|
|
2943
|
+
return function() {
|
|
2944
|
+
active = false;
|
|
2945
|
+
window.removeEventListener('craftDeepLink', listener);
|
|
2946
|
+
};
|
|
2947
|
+
};
|
|
2948
|
+
craft._claimInitialDeepLink = function() {
|
|
2949
|
+
replay.initialClaimed = true;
|
|
2950
|
+
replay.undelivered = replay.undelivered.filter(function(detail) { return !(detail && detail.initial); });
|
|
2951
|
+
};
|
|
2952
|
+
})(window.craft);
|
|
2953
|
+
|
|
2795
2954
|
(function installCraftMobileContract(craft) {
|
|
2796
2955
|
var legacyShare = craft.share.bind(craft);
|
|
2797
2956
|
var legacyOpenCamera = craft.openCamera.bind(craft);
|
|
@@ -2823,14 +2982,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2823
2982
|
getInfo: function() { return craft.getDeviceInfo(); },
|
|
2824
2983
|
getCapabilities: function() { return Promise.resolve(Object.assign({}, craft.capabilities)); }
|
|
2825
2984
|
};
|
|
2985
|
+
// Feedback, as on Android and the web, where there is no
|
|
2986
|
+
// motor to fire: an app that left enableHaptics off gets
|
|
2987
|
+
// nothing played and a settled promise, so `await
|
|
2988
|
+
// haptics.selection()` in the middle of a flow does not stop
|
|
2989
|
+
// the flow. A native failure still rejects, and
|
|
2990
|
+
// craft.haptic() itself reports the refusal.
|
|
2991
|
+
function hapticFeedback(answer) {
|
|
2992
|
+
return answer.then(function() {}, function(error) {
|
|
2993
|
+
if (error && error.code === 'CAPABILITY_DISABLED') return;
|
|
2994
|
+
throw error;
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2826
2997
|
craft.haptics = {
|
|
2827
|
-
impact: function(style) { craft.haptic(style || 'medium')
|
|
2998
|
+
impact: function(style) { return hapticFeedback(craft.haptic(style || 'medium')); },
|
|
2828
2999
|
notification: function(type) {
|
|
2829
|
-
craft.haptic(type === 'error' ? 'heavy' : type === 'warning' ? 'medium' : 'light');
|
|
2830
|
-
return Promise.resolve();
|
|
3000
|
+
return hapticFeedback(craft.haptic(type === 'error' ? 'heavy' : type === 'warning' ? 'medium' : 'light'));
|
|
2831
3001
|
},
|
|
2832
|
-
selection: function() { craft.haptic('soft')
|
|
2833
|
-
vibrate: function(pattern) { craft.vibrate(pattern || [])
|
|
3002
|
+
selection: function() { return hapticFeedback(craft.haptic('soft')); },
|
|
3003
|
+
vibrate: function(pattern) { return hapticFeedback(craft.vibrate(pattern || [])); }
|
|
2834
3004
|
};
|
|
2835
3005
|
craft.permissions = {
|
|
2836
3006
|
check: function(permission) { return craft._invoke('checkPermission', {permission: permission}); },
|
|
@@ -2870,9 +3040,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2870
3040
|
},
|
|
2871
3041
|
clearWatch: function(id) {
|
|
2872
3042
|
locationWatchCallbacks.delete(id);
|
|
2873
|
-
if (locationWatchCallbacks.size === 0)
|
|
2874
|
-
window.webkit.messageHandlers.craft.postMessage({action: 'clearWatch'});
|
|
2875
|
-
}
|
|
3043
|
+
if (locationWatchCallbacks.size === 0) void craft._invoke('clearWatch');
|
|
2876
3044
|
},
|
|
2877
3045
|
startRecording: function() { return craft._invoke('startLocationRecording'); },
|
|
2878
3046
|
pauseRecording: function() { return craft._invoke('pauseLocationRecording'); },
|
|
@@ -2901,7 +3069,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2901
3069
|
}
|
|
2902
3070
|
};
|
|
2903
3071
|
var shareApi = function(text) { return legacyShare(text); };
|
|
2904
|
-
shareApi.share = function(options) { return craft.
|
|
3072
|
+
shareApi.share = function(options) { return craft._share({options: options || {}}); };
|
|
2905
3073
|
craft.share = shareApi;
|
|
2906
3074
|
craft.lifecycle = {
|
|
2907
3075
|
getState: function() { return document.visibilityState === 'visible' ? 'active' : 'background'; },
|
|
@@ -5452,9 +5620,42 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5452
5620
|
resolveCallback(callbackId, result: ["registered": true, "action": action, "phrase": phrase])
|
|
5453
5621
|
}
|
|
5454
5622
|
|
|
5623
|
+
// Removals waiting on their completion, each under a token of its own:
|
|
5624
|
+
// callbackId can be nil, and restarts when the page reloads.
|
|
5625
|
+
private var pendingSiriRemovals: [UUID: DispatchWorkItem] = [:]
|
|
5626
|
+
// The only thing that settles craft.siri.remove, which arms no timeout
|
|
5627
|
+
// of its own; the same as Zig's.
|
|
5628
|
+
private static let siriRemovalDeadline: TimeInterval = 15
|
|
5629
|
+
|
|
5455
5630
|
private func removeSiriShortcut(action: String, callbackId: String?) {
|
|
5631
|
+
// The completion comes from a system daemon, and sometimes it never
|
|
5632
|
+
// comes (#211). The deadline settles the call instead, and never
|
|
5633
|
+
// with removed: true, because the deletion may still have happened.
|
|
5634
|
+
// Both run on the main queue and remove the same entry, so only one
|
|
5635
|
+
// of them answers.
|
|
5636
|
+
let token = UUID()
|
|
5637
|
+
let deadline = DispatchWorkItem { [weak self] in
|
|
5638
|
+
guard let self, self.pendingSiriRemovals.removeValue(forKey: token) != nil else { return }
|
|
5639
|
+
self.rejectCallback(
|
|
5640
|
+
callbackId,
|
|
5641
|
+
error: "NSUserActivity.deleteSavedUserActivities did not call its completion handler within \(Int(Coordinator.siriRemovalDeadline))s",
|
|
5642
|
+
code: "TIMEOUT"
|
|
5643
|
+
)
|
|
5644
|
+
}
|
|
5645
|
+
pendingSiriRemovals[token] = deadline
|
|
5646
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Coordinator.siriRemovalDeadline, execute: deadline)
|
|
5647
|
+
|
|
5456
5648
|
NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: [action]) {
|
|
5457
|
-
|
|
5649
|
+
// The queue this runs on is not documented, and a zig: hand-off
|
|
5650
|
+
// reply goes to evaluateJavaScript without a hop of its own.
|
|
5651
|
+
DispatchQueue.main.async { [weak self] in
|
|
5652
|
+
guard let self, let pending = self.pendingSiriRemovals.removeValue(forKey: token) else {
|
|
5653
|
+
print("removeSiriShortcut: the completion for \(action) arrived after its deadline; ignored")
|
|
5654
|
+
return
|
|
5655
|
+
}
|
|
5656
|
+
pending.cancel()
|
|
5657
|
+
self.resolveCallback(callbackId, result: ["removed": true, "action": action])
|
|
5658
|
+
}
|
|
5458
5659
|
}
|
|
5459
5660
|
}
|
|
5460
5661
|
|
package/dist/mobile.js
CHANGED
|
@@ -457,11 +457,18 @@ var share = {
|
|
|
457
457
|
return craft.share.share(options);
|
|
458
458
|
}
|
|
459
459
|
if (navigator.share) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
try {
|
|
461
|
+
await navigator.share({
|
|
462
|
+
title: options.title,
|
|
463
|
+
text: options.text,
|
|
464
|
+
url: options.url
|
|
465
|
+
});
|
|
466
|
+
return true;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
469
|
+
return false;
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
465
472
|
} else {
|
|
466
473
|
throw new Error("Share API not available");
|
|
467
474
|
}
|
|
@@ -555,15 +562,16 @@ var deepLinks = {
|
|
|
555
562
|
return craft.deepLinks.onLink((value) => {
|
|
556
563
|
const url = normalizeDeepLinkURL(value);
|
|
557
564
|
if (url)
|
|
558
|
-
callback(url);
|
|
565
|
+
callback(url, { initial: value?.initial === true });
|
|
559
566
|
}) ?? (() => {});
|
|
560
567
|
}
|
|
561
568
|
if (typeof globalThis === "undefined")
|
|
562
569
|
return () => {};
|
|
563
570
|
const listener = (event) => {
|
|
564
|
-
const
|
|
571
|
+
const detail = event.detail;
|
|
572
|
+
const url = normalizeDeepLinkURL(detail);
|
|
565
573
|
if (url)
|
|
566
|
-
callback(url);
|
|
574
|
+
callback(url, { initial: detail?.initial === true });
|
|
567
575
|
};
|
|
568
576
|
globalThis.addEventListener("craftDeepLink", listener);
|
|
569
577
|
return () => globalThis.removeEventListener("craftDeepLink", listener);
|