@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.
- package/README.md +101 -2
- package/dist/blob.d.ts +60 -0
- package/dist/blob.d.ts.map +1 -0
- package/dist/blob.js +153 -0
- package/dist/blob.js.map +1 -0
- package/dist/bridge.d.ts +47 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/fetch.d.ts +11 -3
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +26 -2
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +49 -0
- package/dist/hostRequests.d.ts.map +1 -0
- package/dist/hostRequests.js +134 -0
- package/dist/hostRequests.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/matchMedia.d.ts +43 -0
- package/dist/matchMedia.d.ts.map +1 -0
- package/dist/matchMedia.js +63 -0
- package/dist/matchMedia.js.map +1 -0
- package/dist/mediaSession.d.ts +59 -0
- package/dist/mediaSession.d.ts.map +1 -0
- package/dist/mediaSession.js +119 -0
- package/dist/mediaSession.js.map +1 -0
- package/dist/notifications.d.ts +108 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +251 -0
- package/dist/notifications.js.map +1 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +9 -1
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +72 -3
- package/dist/runtime.js.map +1 -1
- package/dist/secureStorage.d.ts +20 -0
- package/dist/secureStorage.d.ts.map +1 -0
- package/dist/secureStorage.js +28 -0
- package/dist/secureStorage.js.map +1 -0
- package/dist/subtle.d.ts +38 -0
- package/dist/subtle.d.ts.map +1 -0
- package/dist/subtle.js +72 -0
- package/dist/subtle.js.map +1 -0
- package/dist/webSocket.d.ts +73 -0
- package/dist/webSocket.d.ts.map +1 -0
- package/dist/webSocket.js +145 -0
- package/dist/webSocket.js.map +1 -0
- package/package.json +3 -3
- package/src/blob.ts +186 -0
- package/src/bridge.ts +57 -1
- package/src/fetch.ts +29 -3
- package/src/hostRequests.ts +148 -0
- package/src/index.ts +8 -0
- package/src/matchMedia.ts +79 -0
- package/src/mediaSession.ts +163 -0
- package/src/notifications.ts +317 -0
- package/src/run.ts +10 -1
- package/src/runtime.ts +73 -3
- package/src/secureStorage.ts +39 -0
- package/src/subtle.ts +87 -0
- package/src/webSocket.ts +175 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +180 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +437 -0
- package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
- package/swift/Sources/SwiftBrowserNative/Host/NowPlayingBridge.swift +190 -0
- package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +3 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +26 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +2 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +22 -9
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +8 -5
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +304 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +43 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +12 -2
- package/wasm/SwiftBrowserNative.wasm +2 -2
package/src/webSocket.ts
ADDED
|
@@ -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 {
|