@swiftbrowser/native 0.17.7 → 0.18.0
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 +99 -11
- 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 +65 -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 +55 -0
- package/dist/hostRequests.d.ts.map +1 -0
- package/dist/hostRequests.js +196 -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/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 +23 -2
- package/dist/run.js.map +1 -1
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +69 -0
- 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/site.d.ts +3 -3
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +1 -1
- package/dist/site.js.map +1 -1
- 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/undo.d.ts +23 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +44 -0
- package/dist/undo.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 +74 -1
- package/src/fetch.ts +29 -3
- package/src/hostRequests.ts +216 -0
- package/src/index.ts +8 -0
- package/src/matchMedia.ts +79 -0
- package/src/notifications.ts +317 -0
- package/src/run.ts +21 -2
- package/src/runtime.ts +71 -0
- package/src/secureStorage.ts +39 -0
- package/src/site.ts +3 -3
- package/src/subtle.ts +87 -0
- package/src/undo.ts +47 -0
- package/src/webSocket.ts +175 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +199 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +154 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +211 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +451 -0
- package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
- package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +1 -0
- package/swift/Sources/SwiftBrowserNative/Host/TextMeasureBridge.swift +139 -0
- package/swift/Sources/SwiftBrowserNative/Host/UndoBridge.swift +170 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +56 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +362 -16
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/DeleteBackward.swift +147 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/KeyboardBar.swift +84 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +116 -11
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +12 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +30 -9
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +163 -10
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +17 -4
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +8 -3
- package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +8 -0
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +41 -0
- package/swift/Sources/SwiftBrowserProtocol/Op.swift +27 -4
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +110 -3
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +47 -0
- package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +260 -0
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +58 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +22 -2
- package/wasm/SwiftBrowserNative.wasm +2 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
#if canImport(FoundationBridge)
|
|
5
|
+
import FoundationBridge
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
/// The app's `WebSocket`s (packages/native/src/webSocket.ts) on
|
|
9
|
+
/// `URLSessionWebSocketTask`: `wsOpen` starts one, which is open once its
|
|
10
|
+
/// first ping is answered (the handshake is done by then), `wsSend` sends
|
|
11
|
+
/// text or bytes, `wsClose` closes it. What a socket receives, and how it
|
|
12
|
+
/// ends, goes to the app as `__sbWebSocketEvent(id, json)`: `open`,
|
|
13
|
+
/// `message` (`text` or base64 `binary`), `error` and `close` (`code`,
|
|
14
|
+
/// `reason`, `wasClean`). Against the shim (the round trip) the task is the
|
|
15
|
+
/// shim's, over the page's WebSocket.
|
|
16
|
+
@MainActor
|
|
17
|
+
final class WebSocketBridge {
|
|
18
|
+
private let session: URLSession
|
|
19
|
+
private let send: (Int, JSONValue) -> Void
|
|
20
|
+
private var tasks: [Int: URLSessionWebSocketTask] = [:]
|
|
21
|
+
|
|
22
|
+
/// `send` hands an event for socket `id` to the app.
|
|
23
|
+
init(session: URLSession, send: @escaping (Int, JSONValue) -> Void) {
|
|
24
|
+
self.session = session
|
|
25
|
+
self.send = send
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func open(id: Int, url string: String, protocols: [String]) {
|
|
29
|
+
guard let url = URL(string: string) else {
|
|
30
|
+
emit(id, ["type": .string("error"), "message": .string("invalid URL")])
|
|
31
|
+
emit(id, ["type": .string("close"), "code": .number(1006), "reason": .string(""), "wasClean": .bool(false)])
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
let task = protocols.isEmpty ? session.webSocketTask(with: url) : session.webSocketTask(with: url, protocols: protocols)
|
|
35
|
+
tasks[id] = task
|
|
36
|
+
task.resume()
|
|
37
|
+
task.sendPing { [weak self] error in
|
|
38
|
+
Task { @MainActor [weak self] in
|
|
39
|
+
guard let self, self.tasks[id] === task else { return }
|
|
40
|
+
if let error {
|
|
41
|
+
self.fail(id: id, task: task, error: error)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
self.emit(id, ["type": .string("open"), "protocol": .string("")])
|
|
45
|
+
self.receive(id: id, task: task)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// `{ text }` or `{ binary }` (base64).
|
|
51
|
+
func send(id: Int, json: String) {
|
|
52
|
+
guard let task = tasks[id], let fields = (try? JSONValue.parse(json))?.object else { return }
|
|
53
|
+
let message: URLSessionWebSocketTask.Message
|
|
54
|
+
if let text = fields["text"]?.string {
|
|
55
|
+
message = .string(text)
|
|
56
|
+
} else if let encoded = fields["binary"]?.string, let data = Data(base64Encoded: encoded) {
|
|
57
|
+
message = .data(data)
|
|
58
|
+
} else {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
task.send(message) { [weak self] error in
|
|
62
|
+
guard let error else { return }
|
|
63
|
+
Task { @MainActor [weak self] in
|
|
64
|
+
guard let self, self.tasks[id] === task else { return }
|
|
65
|
+
self.fail(id: id, task: task, error: error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func close(id: Int, code: Int, reason: String) {
|
|
71
|
+
guard let task = tasks.removeValue(forKey: id) else { return }
|
|
72
|
+
task.cancel(with: URLSessionWebSocketTask.CloseCode(rawValue: code) ?? .normalClosure, reason: reason.isEmpty ? nil : Data(reason.utf8))
|
|
73
|
+
emit(id, ["type": .string("close"), "code": .number(Double(code)), "reason": .string(reason), "wasClean": .bool(true)])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Ends every socket; the app that owned the bridge is going away.
|
|
77
|
+
func invalidate() {
|
|
78
|
+
for task in tasks.values { task.cancel(with: .goingAway, reason: nil) }
|
|
79
|
+
tasks.removeAll()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func receive(id: Int, task: URLSessionWebSocketTask) {
|
|
83
|
+
task.receive { [weak self] result in
|
|
84
|
+
Task { @MainActor [weak self] in
|
|
85
|
+
guard let self, self.tasks[id] === task else { return }
|
|
86
|
+
switch result {
|
|
87
|
+
case .success(let message):
|
|
88
|
+
switch message {
|
|
89
|
+
case .string(let text): self.emit(id, ["type": .string("message"), "text": .string(text)])
|
|
90
|
+
case .data(let data): self.emit(id, ["type": .string("message"), "binary": .string(data.base64EncodedString())])
|
|
91
|
+
@unknown default: break
|
|
92
|
+
}
|
|
93
|
+
self.receive(id: id, task: task)
|
|
94
|
+
case .failure(let error):
|
|
95
|
+
self.fail(id: id, task: task, error: error)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// The socket ended: a close the server sent (its code and reason), else an error and an unclean close.
|
|
102
|
+
private func fail(id: Int, task: URLSessionWebSocketTask, error: any Error) {
|
|
103
|
+
tasks[id] = nil
|
|
104
|
+
let code = task.closeCode
|
|
105
|
+
if code != .invalid {
|
|
106
|
+
let reason = task.closeReason.map { String(decoding: $0, as: UTF8.self) } ?? ""
|
|
107
|
+
emit(id, ["type": .string("close"), "code": .number(Double(code.rawValue)), "reason": .string(reason), "wasClean": .bool(true)])
|
|
108
|
+
} else {
|
|
109
|
+
emit(id, ["type": .string("error"), "message": .string(error.localizedDescription)])
|
|
110
|
+
emit(id, ["type": .string("close"), "code": .number(1006), "reason": .string(""), "wasClean": .bool(false)])
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func emit(_ id: Int, _ fields: JSONObject) {
|
|
115
|
+
send(id, .object(fields))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
#endif
|
|
@@ -40,6 +40,10 @@ public final class NativeApp {
|
|
|
40
40
|
/// URLs the app was opened with before its script could take them (a
|
|
41
41
|
/// universal link that launched it): delivered once it loads.
|
|
42
42
|
private var pendingURLs: [URL] = []
|
|
43
|
+
#if !canImport(FoundationBridge)
|
|
44
|
+
/// The window's undo manager, kept across reloads.
|
|
45
|
+
private weak var undoManager: UndoManager?
|
|
46
|
+
#endif
|
|
43
47
|
|
|
44
48
|
#if canImport(JavaScriptCore)
|
|
45
49
|
public convenience init() {
|
|
@@ -57,13 +61,52 @@ public final class NativeApp {
|
|
|
57
61
|
body()
|
|
58
62
|
}
|
|
59
63
|
}
|
|
64
|
+
// `withAnimation(_:completionCriteria:_:completion:)`: SwiftUI calls the
|
|
65
|
+
// completion when the pass's animation ends; with no animation, right
|
|
66
|
+
// after the pass (the tree sends the event a turn later, off the
|
|
67
|
+
// pass's stack, as the app answers with a pass of its own).
|
|
68
|
+
tree.animateWithCompletion = { spec, completion, body, done in
|
|
69
|
+
guard let animation = spec.map(Animation.init(spec:)) else {
|
|
70
|
+
body()
|
|
71
|
+
done()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
let criteria: AnimationCompletionCriteria = completion.criteria == "removed" ? .removed : .logicallyComplete
|
|
75
|
+
withAnimation(animation, completionCriteria: criteria, body, completion: done)
|
|
76
|
+
}
|
|
60
77
|
tree.onEvent = { [weak self] event in self?.dispatch(event) }
|
|
61
78
|
#if canImport(JavaScriptCore)
|
|
62
79
|
(host as? JavaScriptCoreHost)?.onError = { [weak self] error in self?.error = error.description }
|
|
63
80
|
#endif
|
|
81
|
+
#if canImport(UserNotifications)
|
|
82
|
+
NotificationRouter.shared.installIfApp()
|
|
83
|
+
#endif
|
|
64
84
|
bridge = HostBridge(host: host, tree: tree)
|
|
65
85
|
}
|
|
66
86
|
|
|
87
|
+
#if canImport(UserNotifications)
|
|
88
|
+
// MARK: Remote notifications
|
|
89
|
+
//
|
|
90
|
+
// What the app delegate is handed (NativeAppDelegate.swift, or the app's
|
|
91
|
+
// own delegate calling these), for `registerForPush` and `onNotification`
|
|
92
|
+
// in `@swiftbrowser/native` (NotificationBridge.swift).
|
|
93
|
+
|
|
94
|
+
/// The device token: `registerForPush` resolves with it, as hex.
|
|
95
|
+
public static func didRegisterForRemoteNotifications(deviceToken: Data) {
|
|
96
|
+
NotificationRouter.shared.registered(token: deviceToken)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// No token: `registerForPush` rejects with the error.
|
|
100
|
+
public static func didFailToRegisterForRemoteNotifications(error: any Error) {
|
|
101
|
+
NotificationRouter.shared.registrationFailed("\(error)")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// A push delivered to the app delegate (a silent one): a `received` event.
|
|
105
|
+
public static func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) {
|
|
106
|
+
NotificationRouter.shared.received(remote: userInfo)
|
|
107
|
+
}
|
|
108
|
+
#endif
|
|
109
|
+
|
|
67
110
|
/// Evaluates the app's bundle (what `@swiftbrowser/native` built: it calls
|
|
68
111
|
/// `run(<App/>)` at the end, which renders the first pass into the tree
|
|
69
112
|
/// before this returns). The environment, if known, is sent right after.
|
|
@@ -111,8 +154,21 @@ public final class NativeApp {
|
|
|
111
154
|
try? tree.apply(tree.root.children.map { Op.remove(id: $0.id) } + [.commit(animation: nil)])
|
|
112
155
|
host.reset()
|
|
113
156
|
bridge = HostBridge(host: host, tree: tree)
|
|
157
|
+
#if !canImport(FoundationBridge)
|
|
158
|
+
bridge?.setUndoManager(undoManager)
|
|
159
|
+
#endif
|
|
114
160
|
}
|
|
115
161
|
|
|
162
|
+
#if !canImport(FoundationBridge)
|
|
163
|
+
/// The window's `UndoManager` (`NativeRootView` reads it from the
|
|
164
|
+
/// environment): it mirrors the app's undo manager (UndoBridge.swift),
|
|
165
|
+
/// so the system's undo gestures, keys and menu reach the app.
|
|
166
|
+
public func setUndoManager(_ manager: UndoManager?) {
|
|
167
|
+
undoManager = manager
|
|
168
|
+
bridge?.setUndoManager(manager)
|
|
169
|
+
}
|
|
170
|
+
#endif
|
|
171
|
+
|
|
116
172
|
/// Loads a bundled resource, `app.js` by default.
|
|
117
173
|
public func load(resource: String = "app", extension ext: String = "js", bundle: Bundle = .main) {
|
|
118
174
|
guard let url = bundle.url(forResource: resource, withExtension: ext) else {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && canImport(UIKit) && canImport(UserNotifications)
|
|
2
|
+
import Foundation
|
|
3
|
+
import UIKit
|
|
4
|
+
import UserNotifications
|
|
5
|
+
#if canImport(FoundationBridge)
|
|
6
|
+
import FoundationBridge
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
/// The app delegate a native app adopts for remote notifications, through
|
|
10
|
+
/// `@UIApplicationDelegateAdaptor(NativeAppDelegate.self)` in its App file
|
|
11
|
+
/// (the project `native init` writes does): it hands the device token, a
|
|
12
|
+
/// failure to get one and the pushes delivered to the app to `NativeApp`,
|
|
13
|
+
/// for `registerForPush` and `onNotification` in `@swiftbrowser/native`
|
|
14
|
+
/// (NotificationBridge.swift). An app with a delegate of its own calls the
|
|
15
|
+
/// same `NativeApp` entry points from it.
|
|
16
|
+
open class NativeAppDelegate: UIResponder, UIApplicationDelegate {
|
|
17
|
+
open func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
18
|
+
// Before launching finishes, so a tap that launched the app is reported.
|
|
19
|
+
NotificationRouter.shared.install()
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
24
|
+
NativeApp.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
open func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
|
|
28
|
+
NativeApp.didFailToRegisterForRemoteNotifications(error: error)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
|
32
|
+
NativeApp.didReceiveRemoteNotification(userInfo: userInfo)
|
|
33
|
+
return .newData
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
#endif
|