@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.
Files changed (100) hide show
  1. package/README.md +99 -11
  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 +65 -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 +55 -0
  14. package/dist/hostRequests.d.ts.map +1 -0
  15. package/dist/hostRequests.js +196 -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/notifications.d.ts +108 -0
  26. package/dist/notifications.d.ts.map +1 -0
  27. package/dist/notifications.js +251 -0
  28. package/dist/notifications.js.map +1 -0
  29. package/dist/run.d.ts.map +1 -1
  30. package/dist/run.js +23 -2
  31. package/dist/run.js.map +1 -1
  32. package/dist/runtime.d.ts +2 -0
  33. package/dist/runtime.d.ts.map +1 -1
  34. package/dist/runtime.js +69 -0
  35. package/dist/runtime.js.map +1 -1
  36. package/dist/secureStorage.d.ts +20 -0
  37. package/dist/secureStorage.d.ts.map +1 -0
  38. package/dist/secureStorage.js +28 -0
  39. package/dist/secureStorage.js.map +1 -0
  40. package/dist/site.d.ts +3 -3
  41. package/dist/site.d.ts.map +1 -1
  42. package/dist/site.js +1 -1
  43. package/dist/site.js.map +1 -1
  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/undo.d.ts +23 -0
  49. package/dist/undo.d.ts.map +1 -0
  50. package/dist/undo.js +44 -0
  51. package/dist/undo.js.map +1 -0
  52. package/dist/webSocket.d.ts +73 -0
  53. package/dist/webSocket.d.ts.map +1 -0
  54. package/dist/webSocket.js +145 -0
  55. package/dist/webSocket.js.map +1 -0
  56. package/package.json +3 -3
  57. package/src/blob.ts +186 -0
  58. package/src/bridge.ts +74 -1
  59. package/src/fetch.ts +29 -3
  60. package/src/hostRequests.ts +216 -0
  61. package/src/index.ts +8 -0
  62. package/src/matchMedia.ts +79 -0
  63. package/src/notifications.ts +317 -0
  64. package/src/run.ts +21 -2
  65. package/src/runtime.ts +71 -0
  66. package/src/secureStorage.ts +39 -0
  67. package/src/site.ts +3 -3
  68. package/src/subtle.ts +87 -0
  69. package/src/undo.ts +47 -0
  70. package/src/webSocket.ts +175 -0
  71. package/swift/Sources/SwiftBrowserNative/Host/AudioRecorderBridge.swift +199 -0
  72. package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +154 -0
  73. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +211 -0
  74. package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +451 -0
  75. package/swift/Sources/SwiftBrowserNative/Host/NotificationBridge.swift +304 -0
  76. package/swift/Sources/SwiftBrowserNative/Host/SecureStorage.swift +60 -0
  77. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +1 -0
  78. package/swift/Sources/SwiftBrowserNative/Host/TextMeasureBridge.swift +139 -0
  79. package/swift/Sources/SwiftBrowserNative/Host/UndoBridge.swift +170 -0
  80. package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +118 -0
  81. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +56 -0
  82. package/swift/Sources/SwiftBrowserNative/Runtime/NativeAppDelegate.swift +36 -0
  83. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +362 -16
  84. package/swift/Sources/SwiftBrowserNative/Views/Kinds/DeleteBackward.swift +147 -0
  85. package/swift/Sources/SwiftBrowserNative/Views/Kinds/KeyboardBar.swift +84 -0
  86. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +116 -11
  87. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +12 -1
  88. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +30 -9
  89. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +163 -10
  90. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +17 -4
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Video.swift +8 -3
  92. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +8 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +41 -0
  94. package/swift/Sources/SwiftBrowserProtocol/Op.swift +27 -4
  95. package/swift/Sources/SwiftBrowserProtocol/Props.swift +110 -3
  96. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +47 -0
  97. package/swift/Sources/SwiftBrowserProtocol/SVGPath.swift +260 -0
  98. package/swift/Sources/SwiftBrowserProtocol/Style.swift +58 -0
  99. package/swift/Sources/SwiftBrowserTree/OpTree.swift +22 -2
  100. 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