craft-native 0.0.118 → 0.0.120
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.js +5 -1
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +3 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +455 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +27 -5
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +43 -0
- package/dist/android/templates/MainActivityNative.kt.template +229 -47
- package/dist/api/mobile.d.ts +24 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +26 -1
- package/dist/index.js +26 -1
- package/dist/ios/src/index.d.ts +18 -0
- package/dist/ios/src/index.js +29 -1
- package/dist/ios/templates/CraftApp.swift +39 -1
- package/dist/ios/templates/CraftNativeActions.swift +472 -31
- package/dist/ios/templates/CraftNativeScreen.swift +93 -22
- package/dist/ios/templates/Info.plist.template +1 -0
- package/dist/mobile.js +25 -0
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ import JavaScriptCore
|
|
|
3
3
|
import SwiftUI
|
|
4
4
|
import UIKit
|
|
5
5
|
|
|
6
|
+
private let craftNativeCapabilityTimeoutMilliseconds = 30_000
|
|
7
|
+
|
|
6
8
|
/// A WebView-free host for the first stx-native vertical slice. The bundled
|
|
7
9
|
/// JavaScript sends whole, compiled view trees to UIKit and receives control
|
|
8
10
|
/// events and Craft API replies through JavaScriptCore.
|
|
@@ -83,6 +85,11 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
83
85
|
private var flatListRows: [ObjectIdentifier: [String: RenderedNode]] = [:]
|
|
84
86
|
private var flatListOwners: [String: String] = [:]
|
|
85
87
|
private let mutationDocument = CraftNativeMutationDocument()
|
|
88
|
+
private let capabilityScope = UUID().uuidString
|
|
89
|
+
private var pendingCapabilityRequests = Set<String>()
|
|
90
|
+
private var pendingCapabilityDeadlines: [String: DispatchWorkItem] = [:]
|
|
91
|
+
private var lifecycleObservers: [NSObjectProtocol] = []
|
|
92
|
+
private var deepLinkListener: UUID?
|
|
86
93
|
|
|
87
94
|
init(config: CraftConfig, routeName: String? = nil, routeParams: [String: Any] = [:]) {
|
|
88
95
|
self.config = config
|
|
@@ -97,6 +104,10 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
97
104
|
|
|
98
105
|
deinit {
|
|
99
106
|
imageTasks.values.forEach { $0.cancel() }
|
|
107
|
+
pendingCapabilityDeadlines.values.forEach { $0.cancel() }
|
|
108
|
+
pendingCapabilityRequests.forEach { CraftNativeActions.cancel(requestToken: $0) }
|
|
109
|
+
lifecycleObservers.forEach(NotificationCenter.default.removeObserver)
|
|
110
|
+
if let deepLinkListener = deepLinkListener { DeepLinkManager.shared.removeNativeListener(deepLinkListener) }
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
override func viewDidLoad() {
|
|
@@ -116,6 +127,7 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
116
127
|
])
|
|
117
128
|
setupJavaScript()
|
|
118
129
|
loadBundle()
|
|
130
|
+
observeNativeEvents()
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
private func setupJavaScript() {
|
|
@@ -130,10 +142,18 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
130
142
|
self?.receive(json)
|
|
131
143
|
}
|
|
132
144
|
jsContext.setObject(postMessage, forKeyedSubscript: "craftNativePostMessage" as NSString)
|
|
145
|
+
let capabilities = CraftNativeActions.capabilities(config: config)
|
|
146
|
+
let capabilityData = try? JSONSerialization.data(withJSONObject: capabilities)
|
|
147
|
+
let capabilityJSON = capabilityData.flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
|
133
148
|
jsContext.evaluateScript("""
|
|
134
149
|
globalThis.__stxNativeCallback = null;
|
|
135
150
|
globalThis.__stxNativeBridge = {
|
|
151
|
+
platform: "ios",
|
|
136
152
|
mutationProtocolVersion: 1,
|
|
153
|
+
capabilityProtocolVersion: \(craftNativeCapabilityProtocolVersion),
|
|
154
|
+
capabilityTimeoutMs: \(craftNativeCapabilityTimeoutMilliseconds),
|
|
155
|
+
capabilities: \(capabilityJSON),
|
|
156
|
+
initialAppState: "\(CraftNativeActions.currentAppState())",
|
|
137
157
|
postMessage: function(message) { craftNativePostMessage(message); },
|
|
138
158
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
139
159
|
};
|
|
@@ -156,6 +176,31 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
156
176
|
}
|
|
157
177
|
}
|
|
158
178
|
|
|
179
|
+
private func observeNativeEvents() {
|
|
180
|
+
func observe(_ name: Notification.Name, state: String) {
|
|
181
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
182
|
+
forName: name,
|
|
183
|
+
object: nil,
|
|
184
|
+
queue: .main
|
|
185
|
+
) { [weak self] _ in self?.sendAppState(state) })
|
|
186
|
+
}
|
|
187
|
+
observe(UIApplication.didBecomeActiveNotification, state: "active")
|
|
188
|
+
observe(UIApplication.willResignActiveNotification, state: "inactive")
|
|
189
|
+
observe(UIApplication.didEnterBackgroundNotification, state: "background")
|
|
190
|
+
sendAppState(CraftNativeActions.currentAppState())
|
|
191
|
+
deepLinkListener = DeepLinkManager.shared.addNativeListener { [weak self] url, initial in
|
|
192
|
+
guard let self = self, self.navigationController?.topViewController === self else { return }
|
|
193
|
+
var payload = CraftNativeActions.deepLinkData(url)
|
|
194
|
+
payload["initial"] = initial
|
|
195
|
+
self.send(type: "DEEP_LINK", payload: payload)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private func sendAppState(_ state: String) {
|
|
200
|
+
guard navigationController?.topViewController === self else { return }
|
|
201
|
+
send(type: "APP_STATE", payload: ["state": state])
|
|
202
|
+
}
|
|
203
|
+
|
|
159
204
|
private func showError(_ message: String) {
|
|
160
205
|
let label = UILabel()
|
|
161
206
|
label.numberOfLines = 0
|
|
@@ -219,30 +264,56 @@ final class CraftNativeScreenController: UIViewController {
|
|
|
219
264
|
case "API_REQUEST":
|
|
220
265
|
guard let id = message["id"] as? String else { return }
|
|
221
266
|
let args = payload["args"] as? [Any] ?? []
|
|
222
|
-
let
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
guard let route = route,
|
|
236
|
-
let answer = CraftNativeActions.perform(action: route.action, body: route.body, config: config) else {
|
|
237
|
-
send(type: "API_ERROR", payload: ["requestId": id, "code": "UNKNOWN_ACTION", "message": "Unsupported native API"], correlationId: id)
|
|
238
|
-
return
|
|
267
|
+
let requestToken = "\(capabilityScope)/\(id)"
|
|
268
|
+
pendingCapabilityRequests.insert(requestToken)
|
|
269
|
+
let deadline = DispatchWorkItem { [weak self] in
|
|
270
|
+
guard let self = self, self.pendingCapabilityRequests.remove(requestToken) != nil else { return }
|
|
271
|
+
self.pendingCapabilityDeadlines.removeValue(forKey: requestToken)
|
|
272
|
+
CraftNativeActions.cancel(requestToken: requestToken)
|
|
273
|
+
self.send(type: "API_ERROR", payload: [
|
|
274
|
+
"version": craftNativeCapabilityProtocolVersion,
|
|
275
|
+
"requestId": id,
|
|
276
|
+
"code": "TIMEOUT",
|
|
277
|
+
"message": "Native API request timed out",
|
|
278
|
+
], correlationId: id)
|
|
239
279
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
280
|
+
pendingCapabilityDeadlines[requestToken] = deadline
|
|
281
|
+
DispatchQueue.main.asyncAfter(
|
|
282
|
+
deadline: .now() + .milliseconds(craftNativeCapabilityTimeoutMilliseconds),
|
|
283
|
+
execute: deadline
|
|
284
|
+
)
|
|
285
|
+
CraftNativeActions.perform(
|
|
286
|
+
requestToken: requestToken,
|
|
287
|
+
version: payload["version"] as? Int ?? craftNativeCapabilityProtocolVersion,
|
|
288
|
+
module: payload["module"] as? String ?? "",
|
|
289
|
+
method: payload["method"] as? String ?? "",
|
|
290
|
+
args: args,
|
|
291
|
+
config: config
|
|
292
|
+
) { [weak self] answer in
|
|
293
|
+
guard let self = self, self.pendingCapabilityRequests.remove(requestToken) != nil else { return }
|
|
294
|
+
self.pendingCapabilityDeadlines.removeValue(forKey: requestToken)?.cancel()
|
|
295
|
+
switch answer {
|
|
296
|
+
case .success(let data):
|
|
297
|
+
self.send(type: "API_RESPONSE", payload: [
|
|
298
|
+
"version": craftNativeCapabilityProtocolVersion,
|
|
299
|
+
"requestId": id,
|
|
300
|
+
"data": data,
|
|
301
|
+
], correlationId: id)
|
|
302
|
+
case .failure(let error):
|
|
303
|
+
self.send(type: "API_ERROR", payload: [
|
|
304
|
+
"version": craftNativeCapabilityProtocolVersion,
|
|
305
|
+
"requestId": id,
|
|
306
|
+
"code": error.code,
|
|
307
|
+
"message": error.message,
|
|
308
|
+
], correlationId: id)
|
|
309
|
+
}
|
|
245
310
|
}
|
|
311
|
+
case "API_CANCEL":
|
|
312
|
+
guard let id = payload["requestId"] as? String else { return }
|
|
313
|
+
let requestToken = "\(capabilityScope)/\(id)"
|
|
314
|
+
pendingCapabilityRequests.remove(requestToken)
|
|
315
|
+
pendingCapabilityDeadlines.removeValue(forKey: requestToken)?.cancel()
|
|
316
|
+
CraftNativeActions.cancel(requestToken: requestToken)
|
|
246
317
|
default:
|
|
247
318
|
break
|
|
248
319
|
}
|
package/dist/mobile.js
CHANGED
|
@@ -326,6 +326,17 @@ var secureStorage = {
|
|
|
326
326
|
throw new Error("Craft secure storage bridge is unavailable");
|
|
327
327
|
localStorage.removeItem(`secure_${key}`);
|
|
328
328
|
},
|
|
329
|
+
async remove(key) {
|
|
330
|
+
const craft = getCraftMobile();
|
|
331
|
+
if (typeof window !== "undefined" && craft?.secureStorage) {
|
|
332
|
+
if (typeof craft.secureStorage.remove === "function")
|
|
333
|
+
return craft.secureStorage.remove(key);
|
|
334
|
+
return craft.secureStorage.delete(key);
|
|
335
|
+
}
|
|
336
|
+
if (typeof window !== "undefined" && window.craft)
|
|
337
|
+
throw new Error("Craft secure storage bridge is unavailable");
|
|
338
|
+
localStorage.removeItem(`secure_${key}`);
|
|
339
|
+
},
|
|
329
340
|
async clear() {
|
|
330
341
|
const craft = getCraftMobile();
|
|
331
342
|
if (typeof window !== "undefined" && craft?.secureStorage) {
|
|
@@ -525,6 +536,20 @@ var notifications = {
|
|
|
525
536
|
}
|
|
526
537
|
throw new Error("Scheduled notifications not available in web");
|
|
527
538
|
},
|
|
539
|
+
async cancel(id) {
|
|
540
|
+
const craft = getCraftMobile();
|
|
541
|
+
if (typeof window !== "undefined" && craft?.notifications) {
|
|
542
|
+
return craft.notifications.cancel(id);
|
|
543
|
+
}
|
|
544
|
+
throw new Error("Scheduled notifications not available in web");
|
|
545
|
+
},
|
|
546
|
+
async pending() {
|
|
547
|
+
const craft = getCraftMobile();
|
|
548
|
+
if (typeof window !== "undefined" && craft?.notifications) {
|
|
549
|
+
return craft.notifications.pending();
|
|
550
|
+
}
|
|
551
|
+
return [];
|
|
552
|
+
},
|
|
528
553
|
async cancelAll() {
|
|
529
554
|
const craft = getCraftMobile();
|
|
530
555
|
if (typeof window !== "undefined" && craft?.notifications) {
|