@swiftbrowser/native 0.17.6 → 0.17.8

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.
Files changed (81) hide show
  1. package/README.md +101 -2
  2. package/dist/blob.d.ts +60 -0
  3. package/dist/blob.d.ts.map +1 -0
  4. package/dist/blob.js +153 -0
  5. package/dist/blob.js.map +1 -0
  6. package/dist/bridge.d.ts +47 -1
  7. package/dist/bridge.d.ts.map +1 -1
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/fetch.d.ts +11 -3
  10. package/dist/fetch.d.ts.map +1 -1
  11. package/dist/fetch.js +26 -2
  12. package/dist/fetch.js.map +1 -1
  13. package/dist/hostRequests.d.ts +49 -0
  14. package/dist/hostRequests.d.ts.map +1 -0
  15. package/dist/hostRequests.js +134 -0
  16. package/dist/hostRequests.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/matchMedia.d.ts +43 -0
  22. package/dist/matchMedia.d.ts.map +1 -0
  23. package/dist/matchMedia.js +63 -0
  24. package/dist/matchMedia.js.map +1 -0
  25. package/dist/mediaSession.d.ts +59 -0
  26. package/dist/mediaSession.d.ts.map +1 -0
  27. package/dist/mediaSession.js +119 -0
  28. package/dist/mediaSession.js.map +1 -0
  29. package/dist/notifications.d.ts +108 -0
  30. package/dist/notifications.d.ts.map +1 -0
  31. package/dist/notifications.js +251 -0
  32. package/dist/notifications.js.map +1 -0
  33. package/dist/run.d.ts.map +1 -1
  34. package/dist/run.js +9 -1
  35. package/dist/run.js.map +1 -1
  36. package/dist/runtime.d.ts +3 -1
  37. package/dist/runtime.d.ts.map +1 -1
  38. package/dist/runtime.js +72 -3
  39. package/dist/runtime.js.map +1 -1
  40. package/dist/secureStorage.d.ts +20 -0
  41. package/dist/secureStorage.d.ts.map +1 -0
  42. package/dist/secureStorage.js +28 -0
  43. package/dist/secureStorage.js.map +1 -0
  44. package/dist/subtle.d.ts +38 -0
  45. package/dist/subtle.d.ts.map +1 -0
  46. package/dist/subtle.js +72 -0
  47. package/dist/subtle.js.map +1 -0
  48. package/dist/webSocket.d.ts +73 -0
  49. package/dist/webSocket.d.ts.map +1 -0
  50. package/dist/webSocket.js +145 -0
  51. package/dist/webSocket.js.map +1 -0
  52. package/package.json +3 -3
  53. package/src/blob.ts +186 -0
  54. package/src/bridge.ts +57 -1
  55. package/src/fetch.ts +29 -3
  56. package/src/hostRequests.ts +148 -0
  57. package/src/index.ts +8 -0
  58. package/src/matchMedia.ts +79 -0
  59. package/src/mediaSession.ts +163 -0
  60. package/src/notifications.ts +317 -0
  61. package/src/run.ts +10 -1
  62. package/src/runtime.ts +73 -3
  63. package/src/secureStorage.ts +39 -0
  64. package/src/subtle.ts +87 -0
  65. package/src/webSocket.ts +175 -0
  66. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +180 -0
  67. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +437 -0
  68. package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
  69. package/swift/Sources/SwiftBrowserNative/Host/NowPlayingBridge.swift +190 -0
  70. package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
  71. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +3 -0
  72. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
  73. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +26 -0
  74. package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
  75. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +2 -0
  76. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +22 -9
  77. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +8 -5
  78. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +304 -0
  79. package/swift/Sources/SwiftBrowserProtocol/Props.swift +43 -0
  80. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +12 -2
  81. package/wasm/SwiftBrowserNative.wasm +2 -2
@@ -0,0 +1,175 @@
1
+ /**
2
+ * `WebSocket` for the native app over the bridge's `wsOpen`, `wsSend` and
3
+ * `wsClose` (`URLSessionWebSocketTask` on the Swift side). What the socket
4
+ * receives arrives at `__sbWebSocketEvent(id, json)`: `{ type: "open",
5
+ * protocol }`, `{ type: "message", text }` or `{ type: "message", binary }`
6
+ * (base64), `{ type: "error", message }` and `{ type: "close", code, reason,
7
+ * wasClean }`. The browser's event model as apps use it: `onopen`,
8
+ * `onmessage`, `onerror`, `onclose`, `addEventListener`, `readyState`,
9
+ * `binaryType` (`blob` by default, or `arraybuffer`).
10
+ */
11
+
12
+ import { SimpleBlob } from './blob';
13
+ import type { NativeBridge } from './bridge';
14
+ import { base64Decode, base64Encode } from './encoding';
15
+
16
+ export interface WebSocketEventLike {
17
+ type: string;
18
+ target: NativeWebSocket;
19
+ data?: unknown;
20
+ code?: number;
21
+ reason?: string;
22
+ wasClean?: boolean;
23
+ message?: string;
24
+ }
25
+
26
+ type Handler = (event: WebSocketEventLike) => void;
27
+
28
+ /** The bridge's event for socket `id`. */
29
+ export interface NativeWebSocketEvent {
30
+ type: 'open' | 'message' | 'error' | 'close';
31
+ protocol?: string;
32
+ text?: string;
33
+ /** Base64. */
34
+ binary?: string;
35
+ message?: string;
36
+ code?: number;
37
+ reason?: string;
38
+ wasClean?: boolean;
39
+ }
40
+
41
+ export type WebSocketBridge = Required<Pick<NativeBridge, 'wsOpen' | 'wsSend' | 'wsClose'>>;
42
+
43
+ const sockets = new Map<number, NativeWebSocket>();
44
+ let nextId = 1;
45
+ let bridge: WebSocketBridge | null = null;
46
+
47
+ export class NativeWebSocket {
48
+ static readonly CONNECTING = 0;
49
+ static readonly OPEN = 1;
50
+ static readonly CLOSING = 2;
51
+ static readonly CLOSED = 3;
52
+ readonly CONNECTING = 0;
53
+ readonly OPEN = 1;
54
+ readonly CLOSING = 2;
55
+ readonly CLOSED = 3;
56
+
57
+ readonly url: string;
58
+ readyState = 0;
59
+ protocol = '';
60
+ extensions = '';
61
+ bufferedAmount = 0;
62
+ binaryType: 'blob' | 'arraybuffer' = 'blob';
63
+ onopen: Handler | null = null;
64
+ onmessage: Handler | null = null;
65
+ onerror: Handler | null = null;
66
+ onclose: Handler | null = null;
67
+
68
+ private readonly id: number;
69
+ private readonly listeners = new Map<string, Set<Handler>>();
70
+
71
+ constructor(url: string | { href: string }, protocols: string | string[] = []) {
72
+ if (!bridge) throw new TypeError('WebSocket: no native bridge');
73
+ const href = typeof url === 'string' ? url : url.href;
74
+ if (!/^wss?:\/\//i.test(href)) throw new SyntaxError(`WebSocket: the URL's scheme must be ws or wss: ${href}`);
75
+ this.url = href;
76
+ this.id = nextId++;
77
+ sockets.set(this.id, this);
78
+ bridge.wsOpen(this.id, href, typeof protocols === 'string' ? [protocols] : [...protocols]);
79
+ }
80
+
81
+ send(data: string | ArrayBuffer | ArrayBufferView | SimpleBlob): void {
82
+ if (this.readyState === NativeWebSocket.CONNECTING) throw new DOMExceptionLike('InvalidStateError', 'WebSocket: still connecting');
83
+ if (this.readyState !== NativeWebSocket.OPEN) return;
84
+ if (typeof data === 'string') {
85
+ bridge?.wsSend(this.id, JSON.stringify({ text: data }));
86
+ return;
87
+ }
88
+ const bytes = data instanceof SimpleBlob ? data.bytes() : data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
89
+ bridge?.wsSend(this.id, JSON.stringify({ binary: base64Encode(bytes) }));
90
+ }
91
+
92
+ close(code?: number, reason?: string): void {
93
+ if (code !== undefined && code !== 1000 && !(code >= 3000 && code <= 4999)) throw new DOMExceptionLike('InvalidAccessError', 'WebSocket: the close code must be 1000 or in 3000–4999');
94
+ if (this.readyState === NativeWebSocket.CLOSING || this.readyState === NativeWebSocket.CLOSED) return;
95
+ this.readyState = NativeWebSocket.CLOSING;
96
+ bridge?.wsClose(this.id, code ?? 1000, reason ?? '');
97
+ }
98
+
99
+ addEventListener(type: string, handler: Handler): void {
100
+ let set = this.listeners.get(type);
101
+ if (!set) {
102
+ set = new Set();
103
+ this.listeners.set(type, set);
104
+ }
105
+ set.add(handler);
106
+ }
107
+
108
+ removeEventListener(type: string, handler: Handler): void {
109
+ this.listeners.get(type)?.delete(handler);
110
+ }
111
+
112
+ dispatchEvent(event: WebSocketEventLike): boolean {
113
+ const own = { open: this.onopen, message: this.onmessage, error: this.onerror, close: this.onclose }[event.type];
114
+ own?.(event);
115
+ for (const handler of this.listeners.get(event.type) ?? []) handler(event);
116
+ return true;
117
+ }
118
+
119
+ /** The bridge's event for this socket. */
120
+ receive(event: NativeWebSocketEvent): void {
121
+ switch (event.type) {
122
+ case 'open':
123
+ if (this.readyState !== NativeWebSocket.CONNECTING) return;
124
+ this.readyState = NativeWebSocket.OPEN;
125
+ this.protocol = event.protocol ?? '';
126
+ this.dispatchEvent({ type: 'open', target: this });
127
+ return;
128
+ case 'message': {
129
+ if (this.readyState !== NativeWebSocket.OPEN) return;
130
+ let data: unknown = event.text ?? '';
131
+ if (event.binary !== undefined) {
132
+ const bytes = base64Decode(event.binary);
133
+ data = this.binaryType === 'arraybuffer' ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : new SimpleBlob([bytes]);
134
+ }
135
+ this.dispatchEvent({ type: 'message', target: this, data });
136
+ return;
137
+ }
138
+ case 'error':
139
+ this.dispatchEvent({ type: 'error', target: this, message: event.message ?? '' });
140
+ return;
141
+ case 'close':
142
+ if (this.readyState === NativeWebSocket.CLOSED) return;
143
+ this.readyState = NativeWebSocket.CLOSED;
144
+ sockets.delete(this.id);
145
+ this.dispatchEvent({ type: 'close', target: this, code: event.code ?? 1006, reason: event.reason ?? '', wasClean: event.wasClean ?? false });
146
+ return;
147
+ }
148
+ }
149
+
150
+ /** The bridge's `__sbWebSocketEvent(id, json)`. */
151
+ static receive(id: number, json: string | NativeWebSocketEvent): void {
152
+ const socket = sockets.get(id);
153
+ if (!socket) return;
154
+ let event: NativeWebSocketEvent;
155
+ try {
156
+ event = typeof json === 'string' ? (JSON.parse(json) as NativeWebSocketEvent) : json;
157
+ } catch {
158
+ return;
159
+ }
160
+ socket.receive(event);
161
+ }
162
+
163
+ /** Points the sockets at the bridge (the runtime, once per install). */
164
+ static install(native: WebSocketBridge): void {
165
+ bridge = native;
166
+ }
167
+ }
168
+
169
+ /** A `DOMException` stand-in: an error with the browser's name. */
170
+ class DOMExceptionLike extends Error {
171
+ constructor(name: string, message: string) {
172
+ super(message);
173
+ this.name = name;
174
+ }
175
+ }
@@ -11,6 +11,9 @@ import AppKit
11
11
  #if canImport(FoundationBridge)
12
12
  import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
13
13
  #endif
14
+ #if canImport(CryptoKit)
15
+ import CryptoKit
16
+ #endif
14
17
 
15
18
  /// The native primitives `@swiftbrowser/native` builds the web globals from
16
19
  /// (packages/native/src/runtime.ts), installed on `__sbNative`:
@@ -28,6 +31,13 @@ import FoundationBridge // the shim's Foundation stand-ins (ShimAliases.swift)
28
31
  /// | `openURL(url)` | the system opener |
29
32
  /// | `locale()` | the viewer's locale identifier |
30
33
  /// | `launchArguments()` | the process's arguments (`-screen list` from a scheme or `simctl launch`) |
34
+ /// | `mediaSession(update)` | `navigator.mediaSession`: the lock screen's now-playing info and the remote commands the app handles, on MediaPlayer (NowPlayingBridge.swift); a command the viewer gives comes to `__sbMediaSessionCommand(json)` |
35
+ /// | `notificationsReady()`, `notificationPermission(id, request)`, `notificationShow(json)`, `notificationClose(json)`, `setBadge(count)`, `pushRegister(id, key?)` | notifications on UserNotifications (NotificationBridge.swift): the permission answers `__sbNotificationPermission(id, state)`, a registration `__sbPushRegistered(id, result)`, and what the viewer opens or what arrives comes to `__sbNotification(json)` |
36
+ /// | `wsOpen(id, url, protocols)`, `wsSend(id, json)`, `wsClose(id, code, reason)` | `WebSocket` on `URLSessionWebSocketTask` (WebSocketBridge.swift); what a socket receives comes to `__sbWebSocketEvent(id, json)` |
37
+ /// | `digest(algorithm, base64)`, `hmac(algorithm, key, base64)` | `crypto.subtle` on CryptoKit, synchronously: the base64 digest or HMAC, or nothing when the algorithm is not one of SHA-1, SHA-256, SHA-384 and SHA-512 |
38
+ /// | `haptic(kind)` | `navigator.vibrate`: a feedback generator's `impact`, `selection`, `success`, `warning` or `error` |
39
+ /// | `hostRequest(id, kind, json)` | the web renderer's `share`, `pickFiles`, `clipboard`, `pickColor` and `webAuth` requests, answered with the system's share sheet, pickers, pasteboard and sign-in session (HostRequestBridge.swift); the answer comes to `__sbHostResponse(id, result)` |
40
+ /// | `secureGet(key)`, `secureSet(key, value)`, `secureRemove(key)` | `secureStorage` over the Keychain (SecureStorage.swift) |
31
41
  /// | `workerStart(id, name, engine)`, `workerPost(id, text)`, `workerTerminate(id)` | workers (JavaScriptCore only): a `JSContext` on a thread (`"context"`, WorkerHost.swift) or a hidden web view with WebKit's JIT (`"webview"`, WebViewWorkerHost.swift); their messages come to `__sbWorkerMessage(id, text)`, their errors to `__sbWorkerError(id, message)` |
32
42
  ///
33
43
  /// Everything JavaScript-facing runs on the main thread; work that is not
@@ -42,6 +52,14 @@ final class HostBridge {
42
52
  #endif
43
53
  private let logger = Logger(subsystem: "swiftbrowser", category: "js")
44
54
  private let session = URLSession(configuration: .default)
55
+ #if canImport(MediaPlayer)
56
+ private var nowPlaying: NowPlayingBridge?
57
+ #endif
58
+ #if canImport(UserNotifications)
59
+ private var notifications: NotificationBridge?
60
+ #endif
61
+ private var webSockets: WebSocketBridge?
62
+ private var hostRequests: HostRequestBridge?
45
63
  /// Keys of `localStorage` live under this prefix in `UserDefaults`.
46
64
  static let storagePrefix = "swiftbrowser.storage."
47
65
 
@@ -55,6 +73,7 @@ final class HostBridge {
55
73
  func invalidate() {
56
74
  for timer in timers.values { timer.invalidate() }
57
75
  timers.removeAll()
76
+ webSockets?.invalidate()
58
77
  #if canImport(JavaScriptCore)
59
78
  for worker in workers.values { worker.terminate() }
60
79
  workers.removeAll()
@@ -192,6 +211,103 @@ final class HostBridge {
192
211
 
193
212
  host.install("launchArguments") { _ in .array(ProcessInfo.processInfo.arguments.map(JSONValue.string)) }
194
213
 
214
+ #if canImport(MediaPlayer)
215
+ host.install("mediaSession") { [weak self] arguments in
216
+ guard let self, let update = arguments.first?.string else { return .string("mediaSession: expected a JSON string") }
217
+ if nowPlaying == nil {
218
+ nowPlaying = NowPlayingBridge { [weak self] command in self?.callIn("__sbMediaSessionCommand", [.string(command.json)]) }
219
+ }
220
+ return nowPlaying?.update(update).map(JSONValue.string)
221
+ }
222
+ #endif
223
+
224
+ #if canImport(UserNotifications)
225
+ host.install("notificationsReady") { [weak self] _ in
226
+ self?.notificationBridge.listen()
227
+ return nil
228
+ }
229
+ host.install("notificationPermission") { [weak self] arguments in
230
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
231
+ notificationBridge.permission(id: id, request: arguments[safe: 1]?.bool ?? false)
232
+ return nil
233
+ }
234
+ host.install("notificationShow") { [weak self] arguments in
235
+ guard let self, let json = arguments.first?.string else { return .string("notification: expected a JSON string") }
236
+ return notificationBridge.show(json).map(JSONValue.string)
237
+ }
238
+ host.install("notificationClose") { [weak self] arguments in
239
+ guard let self, let json = arguments.first?.string else { return nil }
240
+ notificationBridge.close(json)
241
+ return nil
242
+ }
243
+ host.install("setBadge") { [weak self] arguments in
244
+ guard let self, let count = arguments.first?.int else { return nil }
245
+ notificationBridge.setBadge(count)
246
+ return nil
247
+ }
248
+ host.install("pushRegister") { [weak self] arguments in
249
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
250
+ notificationBridge.registerForPush(id: id, applicationServerKey: arguments[safe: 1]?.string)
251
+ return nil
252
+ }
253
+ #endif
254
+
255
+ host.install("wsOpen") { [weak self] arguments in
256
+ guard let self, let id = arguments[safe: 0]?.int, let url = arguments[safe: 1]?.string else { return nil }
257
+ let protocols = arguments[safe: 2]?.array?.compactMap(\.string) ?? []
258
+ webSocketBridge.open(id: id, url: url, protocols: protocols)
259
+ return nil
260
+ }
261
+ host.install("wsSend") { [weak self] arguments in
262
+ guard let self, let id = arguments[safe: 0]?.int, let json = arguments[safe: 1]?.string else { return nil }
263
+ webSocketBridge.send(id: id, json: json)
264
+ return nil
265
+ }
266
+ host.install("wsClose") { [weak self] arguments in
267
+ guard let self, let id = arguments[safe: 0]?.int else { return nil }
268
+ webSocketBridge.close(id: id, code: arguments[safe: 1]?.int ?? 1000, reason: arguments[safe: 2]?.string ?? "")
269
+ return nil
270
+ }
271
+
272
+ host.install("digest") { arguments in
273
+ guard let algorithm = arguments[safe: 0]?.string, let encoded = arguments[safe: 1]?.string, let data = Data(base64Encoded: encoded) else { return nil }
274
+ return Self.digest(algorithm, data).map { .string($0.base64EncodedString()) }
275
+ }
276
+ host.install("hmac") { arguments in
277
+ guard let algorithm = arguments[safe: 0]?.string, let key = arguments[safe: 1]?.string, let encoded = arguments[safe: 2]?.string,
278
+ let keyData = Data(base64Encoded: key), let data = Data(base64Encoded: encoded) else { return nil }
279
+ return Self.hmac(algorithm, key: keyData, data).map { .string($0.base64EncodedString()) }
280
+ }
281
+
282
+ host.install("haptic") { arguments in
283
+ Self.haptic(arguments.first?.string ?? "impact")
284
+ return nil
285
+ }
286
+
287
+ host.install("hostRequest") { [weak self] arguments in
288
+ guard let self, let id = arguments[safe: 0]?.int, let kind = arguments[safe: 1]?.string else { return nil }
289
+ if hostRequests == nil {
290
+ hostRequests = HostRequestBridge { [weak self] id, result in self?.callIn("__sbHostResponse", [.number(Double(id)), .string(result.json)]) }
291
+ }
292
+ hostRequests?.perform(id: id, kind: kind, json: arguments[safe: 2]?.string ?? "{}")
293
+ return nil
294
+ }
295
+
296
+ host.install("secureGet") { arguments in
297
+ guard let key = arguments.first?.string else { return nil }
298
+ return SecureStorage.get(key).map(JSONValue.string) ?? .null
299
+ }
300
+ host.install("secureSet") { arguments in
301
+ guard let key = arguments[safe: 0]?.string, let value = arguments[safe: 1]?.string else { return nil }
302
+ SecureStorage.set(key, value)
303
+ return nil
304
+ }
305
+ host.install("secureRemove") { arguments in
306
+ guard let key = arguments.first?.string else { return nil }
307
+ SecureStorage.remove(key)
308
+ return nil
309
+ }
310
+
195
311
  #if canImport(JavaScriptCore)
196
312
  host.install("workerStart") { [weak self] arguments in
197
313
  guard let self, let id = arguments[safe: 0]?.int, let name = arguments[safe: 1]?.string else {
@@ -241,6 +357,70 @@ final class HostBridge {
241
357
  #endif
242
358
  }
243
359
 
360
+ #if canImport(UserNotifications)
361
+ /// The notification bridge, made when the app first uses notifications.
362
+ private var notificationBridge: NotificationBridge {
363
+ if let notifications { return notifications }
364
+ let bridge = NotificationBridge { [weak self] function, arguments in self?.callIn(function, arguments) }
365
+ notifications = bridge
366
+ return bridge
367
+ }
368
+ #endif
369
+
370
+ /// The WebSocket bridge, made when the app opens its first socket.
371
+ private var webSocketBridge: WebSocketBridge {
372
+ if let webSockets { return webSockets }
373
+ let bridge = WebSocketBridge(session: session) { [weak self] id, event in self?.callIn("__sbWebSocketEvent", [.number(Double(id)), .string(event.json)]) }
374
+ webSockets = bridge
375
+ return bridge
376
+ }
377
+
378
+ // MARK: crypto.subtle and haptics
379
+
380
+ static func digest(_ algorithm: String, _ data: Data) -> Data? {
381
+ #if canImport(CryptoKit)
382
+ switch algorithm {
383
+ case "SHA-256": Data(SHA256.hash(data: data))
384
+ case "SHA-384": Data(SHA384.hash(data: data))
385
+ case "SHA-512": Data(SHA512.hash(data: data))
386
+ case "SHA-1": Data(Insecure.SHA1.hash(data: data))
387
+ default: nil
388
+ }
389
+ #else
390
+ nil
391
+ #endif
392
+ }
393
+
394
+ static func hmac(_ algorithm: String, key: Data, _ data: Data) -> Data? {
395
+ #if canImport(CryptoKit) && !canImport(FoundationBridge)
396
+ let symmetricKey = SymmetricKey(data: key)
397
+ return switch algorithm {
398
+ case "SHA-256": Data(HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey))
399
+ case "SHA-384": Data(HMAC<SHA384>.authenticationCode(for: data, using: symmetricKey))
400
+ case "SHA-512": Data(HMAC<SHA512>.authenticationCode(for: data, using: symmetricKey))
401
+ case "SHA-1": Data(HMAC<Insecure.SHA1>.authenticationCode(for: data, using: symmetricKey))
402
+ default: nil
403
+ }
404
+ #else
405
+ // The shim's CryptoKit has no HMAC.
406
+ nil
407
+ #endif
408
+ }
409
+
410
+ static func haptic(_ kind: String) {
411
+ #if canImport(UIKit) && !os(tvOS)
412
+ switch kind {
413
+ case "selection": UISelectionFeedbackGenerator().selectionChanged()
414
+ case "success": UINotificationFeedbackGenerator().notificationOccurred(.success)
415
+ case "warning": UINotificationFeedbackGenerator().notificationOccurred(.warning)
416
+ case "error": UINotificationFeedbackGenerator().notificationOccurred(.error)
417
+ default: UIImpactFeedbackGenerator(style: .medium).impactOccurred()
418
+ }
419
+ #elseif canImport(AppKit)
420
+ NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .now)
421
+ #endif
422
+ }
423
+
244
424
  /// Calls into JavaScript from a timer or a completion, where a throw has nowhere to go but the log.
245
425
  private func callIn(_ function: String, _ arguments: [JSONValue]) {
246
426
  do {