@nebula-rn/host 0.0.1
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/NebulaHost.podspec +23 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +27 -0
- package/android/consumer-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaActivity.kt +290 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaAppManager.kt +134 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaConfig.kt +324 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaEventHub.kt +49 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaHost.kt +145 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaHostModalActivity.kt +178 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaManifestManager.kt +130 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaNativeModule.kt +604 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaPackage.kt +16 -0
- package/android/src/main/java/com/hectorzhuang/nebula/NebulaRouter.kt +300 -0
- package/ios/Nebula/NebulaAppManager.swift +355 -0
- package/ios/Nebula/NebulaConfig.swift +549 -0
- package/ios/Nebula/NebulaContainerController.swift +580 -0
- package/ios/Nebula/NebulaDevLoading.swift +333 -0
- package/ios/Nebula/NebulaHost.swift +611 -0
- package/ios/Nebula/NebulaManifest.swift +214 -0
- package/ios/Nebula/NebulaNativeModule.swift +682 -0
- package/ios/Nebula/NebulaNativeModuleBridge.m +364 -0
- package/ios/Nebula/NebulaPerformanceMonitor.swift +46 -0
- package/ios/Nebula/NebulaRouter.swift +594 -0
- package/ios/Nebula/NebulaRouterBridge.m +19 -0
- package/ios/Nebula/RNInstanceViewController.swift +52 -0
- package/package.json +41 -0
- package/react-native.config.js +14 -0
- package/src/index.ts +9 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NebulaRouter.swift
|
|
3
|
+
// SuperApp - Nebula Mini-App Container
|
|
4
|
+
//
|
|
5
|
+
// Router for handling navigation between mini-apps and native pages
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import React
|
|
10
|
+
|
|
11
|
+
@objc public final class NebulaRouter: NSObject {
|
|
12
|
+
|
|
13
|
+
// MARK: - Singleton
|
|
14
|
+
|
|
15
|
+
@objc public static let shared = NebulaRouter()
|
|
16
|
+
|
|
17
|
+
// MARK: - Properties
|
|
18
|
+
|
|
19
|
+
// Primary storage: instanceId -> controller
|
|
20
|
+
private var containerRegistry: [String: WeakContainer] = [:]
|
|
21
|
+
// Secondary index: appId -> [instanceIds]
|
|
22
|
+
private var appIdToInstances: [String: Set<String>] = [:]
|
|
23
|
+
private let queue = DispatchQueue(label: "com.nebula.router", attributes: .concurrent)
|
|
24
|
+
|
|
25
|
+
// MARK: - Types
|
|
26
|
+
|
|
27
|
+
private struct WeakContainer {
|
|
28
|
+
weak var controller: NebulaContainerController?
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private enum NavigationAction {
|
|
32
|
+
case push
|
|
33
|
+
case replaceTop
|
|
34
|
+
case relaunch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// MARK: - Container Registry
|
|
38
|
+
|
|
39
|
+
func registerContainer(_ controller: NebulaContainerController, instanceId: String, appId: String) {
|
|
40
|
+
queue.async(flags: .barrier) { [weak self] in
|
|
41
|
+
guard let self = self else { return }
|
|
42
|
+
self.containerRegistry[instanceId] = WeakContainer(controller: controller)
|
|
43
|
+
|
|
44
|
+
// Update appId mapping
|
|
45
|
+
if self.appIdToInstances[appId] == nil {
|
|
46
|
+
self.appIdToInstances[appId] = Set<String>()
|
|
47
|
+
}
|
|
48
|
+
self.appIdToInstances[appId]?.insert(instanceId)
|
|
49
|
+
|
|
50
|
+
print("[Nebula] Registered container: appId=\(appId), instanceId=\(instanceId)")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func unregisterContainer(for instanceId: String) {
|
|
55
|
+
queue.async(flags: .barrier) { [weak self] in
|
|
56
|
+
guard let self = self else { return }
|
|
57
|
+
|
|
58
|
+
// Find and remove from appId mapping
|
|
59
|
+
for (appId, instanceIds) in self.appIdToInstances {
|
|
60
|
+
if instanceIds.contains(instanceId) {
|
|
61
|
+
self.appIdToInstances[appId]?.remove(instanceId)
|
|
62
|
+
if self.appIdToInstances[appId]?.isEmpty == true {
|
|
63
|
+
self.appIdToInstances.removeValue(forKey: appId)
|
|
64
|
+
}
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
self.containerRegistry.removeValue(forKey: instanceId)
|
|
70
|
+
print("[Nebula] Unregistered container: instanceId=\(instanceId)")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Find container by instanceId or appId (finds topmost visible instance)
|
|
75
|
+
private func findContainer(byInstanceId instanceId: String? = nil, orAppId appId: String? = nil) -> NebulaContainerController? {
|
|
76
|
+
var result: NebulaContainerController?
|
|
77
|
+
|
|
78
|
+
queue.sync {
|
|
79
|
+
// First try instanceId (most specific)
|
|
80
|
+
if let instanceId = instanceId {
|
|
81
|
+
result = containerRegistry[instanceId]?.controller
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback to appId - find the topmost visible instance
|
|
86
|
+
if let appId = appId, let instanceIds = appIdToInstances[appId] {
|
|
87
|
+
var topmost: NebulaContainerController?
|
|
88
|
+
var topmostLevel = -1
|
|
89
|
+
|
|
90
|
+
for instanceId in instanceIds {
|
|
91
|
+
guard let controller = containerRegistry[instanceId]?.controller else { continue }
|
|
92
|
+
|
|
93
|
+
// Find the navigation level (how deep in the stack)
|
|
94
|
+
if let navController = controller.navigationController,
|
|
95
|
+
let index = navController.viewControllers.firstIndex(of: controller) {
|
|
96
|
+
if index > topmostLevel {
|
|
97
|
+
topmost = controller
|
|
98
|
+
topmostLevel = index
|
|
99
|
+
}
|
|
100
|
+
} else if topmost == nil {
|
|
101
|
+
// No nav controller, just use first found
|
|
102
|
+
topmost = controller
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
result = topmost
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - Navigation
|
|
114
|
+
|
|
115
|
+
/// Navigate to a URL (mini-app or native page)
|
|
116
|
+
@objc public func navigateToURL(_ url: String,
|
|
117
|
+
fromAppId: String,
|
|
118
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
119
|
+
guard let parsedURL = NebulaURL.parse(url) else {
|
|
120
|
+
let error = NSError(
|
|
121
|
+
domain: "com.nebula.router",
|
|
122
|
+
code: -1,
|
|
123
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(url)"]
|
|
124
|
+
)
|
|
125
|
+
completion(false, error)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
DispatchQueue.main.async { [weak self] in
|
|
130
|
+
self?.handleNavigation(parsedURL, fromAppId: fromAppId, action: .push, completion: completion)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Replace current page with target page
|
|
135
|
+
@objc public func redirectToURL(_ url: String,
|
|
136
|
+
fromAppId: String,
|
|
137
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
138
|
+
guard let parsedURL = NebulaURL.parse(url) else {
|
|
139
|
+
let error = NSError(
|
|
140
|
+
domain: "com.nebula.router",
|
|
141
|
+
code: -1,
|
|
142
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(url)"]
|
|
143
|
+
)
|
|
144
|
+
completion(false, error)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
DispatchQueue.main.async { [weak self] in
|
|
149
|
+
self?.handleNavigation(parsedURL, fromAppId: fromAppId, action: .replaceTop, completion: completion)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Close all mini-app pages and open target page
|
|
154
|
+
@objc public func reLaunchURL(_ url: String,
|
|
155
|
+
fromAppId: String,
|
|
156
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
157
|
+
guard let parsedURL = NebulaURL.parse(url) else {
|
|
158
|
+
let error = NSError(
|
|
159
|
+
domain: "com.nebula.router",
|
|
160
|
+
code: -1,
|
|
161
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(url)"]
|
|
162
|
+
)
|
|
163
|
+
completion(false, error)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
DispatchQueue.main.async { [weak self] in
|
|
168
|
+
self?.handleNavigation(parsedURL, fromAppId: fromAppId, action: .relaunch, completion: completion)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Navigate back
|
|
173
|
+
@objc public func navigateBack(fromAppId: String,
|
|
174
|
+
delta: Int = 1,
|
|
175
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
176
|
+
DispatchQueue.main.async { [weak self] in
|
|
177
|
+
guard let self = self else {
|
|
178
|
+
completion(false, nil)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Find the container by appId (will find topmost instance)
|
|
183
|
+
guard let container = self.findContainer(orAppId: fromAppId) else {
|
|
184
|
+
completion(false, NSError(
|
|
185
|
+
domain: "com.nebula.router",
|
|
186
|
+
code: -2,
|
|
187
|
+
userInfo: [NSLocalizedDescriptionKey: "Container not found for appId: \(fromAppId)"]
|
|
188
|
+
))
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
guard let navigationController = container.navigationController else {
|
|
193
|
+
completion(false, NSError(
|
|
194
|
+
domain: "com.nebula.router",
|
|
195
|
+
code: -2,
|
|
196
|
+
userInfo: [NSLocalizedDescriptionKey: "Navigation controller not found"]
|
|
197
|
+
))
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let normalizedDelta = max(1, delta)
|
|
202
|
+
let stack = navigationController.viewControllers
|
|
203
|
+
guard stack.count > 1 else {
|
|
204
|
+
completion(false, NSError(
|
|
205
|
+
domain: "com.nebula.router",
|
|
206
|
+
code: -6,
|
|
207
|
+
userInfo: [NSLocalizedDescriptionKey: "Cannot navigateBack: already at root"]
|
|
208
|
+
))
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let targetIndex = max(0, stack.count - 1 - normalizedDelta)
|
|
213
|
+
let targetVC = stack[targetIndex]
|
|
214
|
+
navigationController.popToViewController(targetVC, animated: true)
|
|
215
|
+
completion(true, nil)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@objc public func closeApp(
|
|
220
|
+
_ appId: String,
|
|
221
|
+
completion: @escaping (Bool, Error?) -> Void
|
|
222
|
+
) {
|
|
223
|
+
DispatchQueue.main.async { [weak self] in
|
|
224
|
+
guard let self = self else {
|
|
225
|
+
completion(false, nil)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
guard let container = self.findContainer(orAppId: appId) else {
|
|
230
|
+
completion(false, NSError(
|
|
231
|
+
domain: "com.nebula.router",
|
|
232
|
+
code: -7,
|
|
233
|
+
userInfo: [NSLocalizedDescriptionKey: "Container not found for appId: \(appId)"]
|
|
234
|
+
))
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
container.dismissLoading()
|
|
239
|
+
|
|
240
|
+
if let navigationController = container.navigationController,
|
|
241
|
+
navigationController.topViewController === container {
|
|
242
|
+
navigationController.popViewController(animated: true)
|
|
243
|
+
completion(true, nil)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if container.presentingViewController != nil {
|
|
248
|
+
container.dismiss(animated: true) {
|
|
249
|
+
completion(true, nil)
|
|
250
|
+
}
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
completion(false, NSError(
|
|
255
|
+
domain: "com.nebula.router",
|
|
256
|
+
code: -8,
|
|
257
|
+
userInfo: [NSLocalizedDescriptionKey: "Miniapp container is not currently closable"]
|
|
258
|
+
))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@objc public func updatePageStyle(forAppId appId: String,
|
|
263
|
+
style: NSDictionary,
|
|
264
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
265
|
+
DispatchQueue.main.async { [weak self] in
|
|
266
|
+
guard let self = self,
|
|
267
|
+
let container = self.findContainer(orAppId: appId) else {
|
|
268
|
+
completion(false, NSError(
|
|
269
|
+
domain: "com.nebula.router",
|
|
270
|
+
code: -8,
|
|
271
|
+
userInfo: [NSLocalizedDescriptionKey: "Container not found for appId: \(appId)"]
|
|
272
|
+
))
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
container.updatePageStyle(style as? [String: Any] ?? [:])
|
|
277
|
+
completion(true, nil)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// MARK: - Private Methods
|
|
282
|
+
|
|
283
|
+
private func handleNavigation(_ url: NebulaURL,
|
|
284
|
+
fromAppId: String,
|
|
285
|
+
action: NavigationAction,
|
|
286
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
287
|
+
// Find source container by appId (will find topmost instance)
|
|
288
|
+
guard let sourceContainer = findContainer(orAppId: fromAppId),
|
|
289
|
+
let navigationController = sourceContainer.navigationController else {
|
|
290
|
+
completion(false, NSError(
|
|
291
|
+
domain: "com.nebula.router",
|
|
292
|
+
code: -2,
|
|
293
|
+
userInfo: [NSLocalizedDescriptionKey: "Navigation controller not found for appId: \(fromAppId)"]
|
|
294
|
+
))
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
switch url.scheme {
|
|
299
|
+
case .miniApp:
|
|
300
|
+
// Navigate to another mini-app
|
|
301
|
+
let targetAppId = url.appId ?? fromAppId
|
|
302
|
+
let targetPath = url.path ?? NebulaManifestManager.shared.getEntryPagePath(forAppId: targetAppId) ?? "/"
|
|
303
|
+
guard NebulaManifestManager.shared.getComponentName(forAppId: targetAppId, path: targetPath) != nil else {
|
|
304
|
+
completion(false, NSError(
|
|
305
|
+
domain: "com.nebula.router",
|
|
306
|
+
code: -4,
|
|
307
|
+
userInfo: [NSLocalizedDescriptionKey: "No component registered for route '\(targetPath)' in app '\(targetAppId)'"]
|
|
308
|
+
))
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
let routeProps = buildRouteInitialProps(url: url, appId: targetAppId)
|
|
312
|
+
let targetVC = NebulaContainerController(
|
|
313
|
+
appId: targetAppId,
|
|
314
|
+
initialProps: routeProps,
|
|
315
|
+
delayContentEntrance: false
|
|
316
|
+
)
|
|
317
|
+
do {
|
|
318
|
+
try applyNavigationAction(action, targetVC: targetVC, navigationController: navigationController)
|
|
319
|
+
completion(true, nil)
|
|
320
|
+
} catch {
|
|
321
|
+
completion(false, error)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case .native:
|
|
325
|
+
// Navigate to native page (custom implementation)
|
|
326
|
+
handleNativeNavigation(url,
|
|
327
|
+
action: action,
|
|
328
|
+
navigationController: navigationController,
|
|
329
|
+
completion: completion)
|
|
330
|
+
|
|
331
|
+
case .external:
|
|
332
|
+
// Open external URL
|
|
333
|
+
if let externalURL = URL(string: url.originalURL) {
|
|
334
|
+
UIApplication.shared.open(externalURL, options: [:]) { success in
|
|
335
|
+
completion(success, nil)
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
completion(false, NSError(
|
|
339
|
+
domain: "com.nebula.router",
|
|
340
|
+
code: -3,
|
|
341
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid external URL"]
|
|
342
|
+
))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func buildRouteInitialProps(url: NebulaURL, appId: String) -> [String: Any]? {
|
|
348
|
+
var props = url.params ?? [:]
|
|
349
|
+
if let path = url.path {
|
|
350
|
+
props["__routePath"] = path
|
|
351
|
+
if let pageConfig = NebulaManifestManager.shared.getPageConfig(forAppId: appId, path: path) as? [String: Any] {
|
|
352
|
+
props["__pageConfig"] = pageConfig
|
|
353
|
+
}
|
|
354
|
+
print("[Nebula] Routing \(appId):\(path)")
|
|
355
|
+
}
|
|
356
|
+
props["__routeUrl"] = url.originalURL
|
|
357
|
+
return props.isEmpty ? nil : props
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private func applyNavigationAction(_ action: NavigationAction,
|
|
361
|
+
targetVC: UIViewController,
|
|
362
|
+
navigationController: UINavigationController) throws {
|
|
363
|
+
switch action {
|
|
364
|
+
case .push:
|
|
365
|
+
let maxDepth = NebulaConfig.shared.maxNavigationStackDepth
|
|
366
|
+
let stack = navigationController.viewControllers
|
|
367
|
+
|
|
368
|
+
// Get appId from target container (if it's a mini-app page)
|
|
369
|
+
var targetAppId: String?
|
|
370
|
+
if let targetContainer = targetVC as? NebulaContainerController {
|
|
371
|
+
targetAppId = targetContainer.appId
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Count only mini-app pages with the same appId (don't count host app pages)
|
|
375
|
+
let miniAppPageCount: Int
|
|
376
|
+
if let appId = targetAppId {
|
|
377
|
+
miniAppPageCount = stack.filter { vc in
|
|
378
|
+
guard let container = vc as? NebulaContainerController else { return false }
|
|
379
|
+
return container.appId == appId
|
|
380
|
+
}.count
|
|
381
|
+
} else {
|
|
382
|
+
// For non-mini-app pages, count all pages
|
|
383
|
+
miniAppPageCount = stack.count
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Reject navigation if this mini-app's stack is already at max depth
|
|
387
|
+
if miniAppPageCount >= maxDepth {
|
|
388
|
+
let appIdLabel = targetAppId ?? "unknown"
|
|
389
|
+
print("[Nebula] Navigation rejected: mini-app \(appIdLabel) stack at max depth (\(miniAppPageCount)/\(maxDepth))")
|
|
390
|
+
throw NSError(
|
|
391
|
+
domain: "com.nebula.router",
|
|
392
|
+
code: -7,
|
|
393
|
+
userInfo: [NSLocalizedDescriptionKey: "Navigation stack limit reached for this mini-app (max: \(maxDepth)). Cannot push more pages."]
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Add the new page
|
|
398
|
+
navigationController.pushViewController(targetVC, animated: true)
|
|
399
|
+
|
|
400
|
+
case .replaceTop:
|
|
401
|
+
var stack = navigationController.viewControllers
|
|
402
|
+
if !stack.isEmpty {
|
|
403
|
+
stack.removeLast()
|
|
404
|
+
}
|
|
405
|
+
stack.append(targetVC)
|
|
406
|
+
navigationController.setViewControllers(stack, animated: true)
|
|
407
|
+
case .relaunch:
|
|
408
|
+
if let root = navigationController.viewControllers.first {
|
|
409
|
+
navigationController.setViewControllers([root, targetVC], animated: true)
|
|
410
|
+
} else {
|
|
411
|
+
navigationController.setViewControllers([targetVC], animated: true)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private func handleNativeNavigation(_ url: NebulaURL,
|
|
417
|
+
action: NavigationAction,
|
|
418
|
+
navigationController: UINavigationController,
|
|
419
|
+
completion: @escaping (Bool, Error?) -> Void) {
|
|
420
|
+
// Example: Handle native://settings, native://profile, etc.
|
|
421
|
+
guard let path = url.path else {
|
|
422
|
+
completion(false, NSError(
|
|
423
|
+
domain: "com.nebula.router",
|
|
424
|
+
code: -4,
|
|
425
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing path in native URL"]
|
|
426
|
+
))
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
switch path {
|
|
431
|
+
case "settings":
|
|
432
|
+
// Navigate to native settings page
|
|
433
|
+
let settingsVC = UIViewController()
|
|
434
|
+
settingsVC.title = "Settings"
|
|
435
|
+
settingsVC.view.backgroundColor = .systemBackground
|
|
436
|
+
do {
|
|
437
|
+
try applyNavigationAction(action, targetVC: settingsVC, navigationController: navigationController)
|
|
438
|
+
completion(true, nil)
|
|
439
|
+
} catch {
|
|
440
|
+
completion(false, error)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
default:
|
|
444
|
+
completion(false, NSError(
|
|
445
|
+
domain: "com.nebula.router",
|
|
446
|
+
code: -5,
|
|
447
|
+
userInfo: [NSLocalizedDescriptionKey: "Unknown native path: \(path)"]
|
|
448
|
+
))
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private override init() {
|
|
453
|
+
super.init()
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// MARK: - URL Parser
|
|
458
|
+
|
|
459
|
+
private struct NebulaURL {
|
|
460
|
+
enum Scheme {
|
|
461
|
+
case miniApp // nebula://appId/path
|
|
462
|
+
case native // native://path
|
|
463
|
+
case external // http://, https://
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let scheme: Scheme
|
|
467
|
+
let appId: String?
|
|
468
|
+
let path: String?
|
|
469
|
+
let params: [String: Any]?
|
|
470
|
+
let originalURL: String
|
|
471
|
+
|
|
472
|
+
static func parse(_ urlString: String) -> NebulaURL? {
|
|
473
|
+
guard let url = URL(string: urlString) else {
|
|
474
|
+
return nil
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
var scheme: Scheme
|
|
478
|
+
var appId: String?
|
|
479
|
+
var path: String?
|
|
480
|
+
var params: [String: Any] = [:]
|
|
481
|
+
|
|
482
|
+
// Parse scheme
|
|
483
|
+
if urlString.hasPrefix("nebula://") {
|
|
484
|
+
scheme = .miniApp
|
|
485
|
+
appId = url.host
|
|
486
|
+
path = url.path.isEmpty ? nil : url.path
|
|
487
|
+
} else if urlString.hasPrefix("native://") {
|
|
488
|
+
scheme = .native
|
|
489
|
+
path = url.host
|
|
490
|
+
} else if urlString.hasPrefix("http://") || urlString.hasPrefix("https://") {
|
|
491
|
+
scheme = .external
|
|
492
|
+
} else {
|
|
493
|
+
// Default to mini-app with relative path
|
|
494
|
+
scheme = .miniApp
|
|
495
|
+
path = urlString
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Parse query parameters
|
|
499
|
+
if let components = URLComponents(string: urlString),
|
|
500
|
+
let queryItems = components.queryItems {
|
|
501
|
+
for item in queryItems {
|
|
502
|
+
params[item.name] = item.value
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return NebulaURL(
|
|
507
|
+
scheme: scheme,
|
|
508
|
+
appId: appId,
|
|
509
|
+
path: path,
|
|
510
|
+
params: params.isEmpty ? nil : params,
|
|
511
|
+
originalURL: urlString
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// MARK: - Router Module (RCTBridgeModule)
|
|
517
|
+
|
|
518
|
+
@objc(NebulaRouterModule)
|
|
519
|
+
final class NebulaRouterModule: NSObject {
|
|
520
|
+
|
|
521
|
+
private let appId: String
|
|
522
|
+
|
|
523
|
+
@objc static func moduleName() -> String! {
|
|
524
|
+
return "NebulaRouterModule"
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@objc static func requiresMainQueueSetup() -> Bool {
|
|
528
|
+
return true
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
init(appId: String) {
|
|
532
|
+
self.appId = appId
|
|
533
|
+
super.init()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@objc func navigateTo(_ url: String,
|
|
537
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
538
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
539
|
+
NebulaRouter.shared.navigateToURL(url, fromAppId: appId) { success, error in
|
|
540
|
+
if success {
|
|
541
|
+
resolver(["success": true])
|
|
542
|
+
} else {
|
|
543
|
+
rejecter("NAVIGATION_ERROR", error?.localizedDescription ?? "Unknown error", error)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@objc func redirectTo(_ url: String,
|
|
549
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
550
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
551
|
+
NebulaRouter.shared.redirectToURL(url, fromAppId: appId) { success, error in
|
|
552
|
+
if success {
|
|
553
|
+
resolver(["success": true])
|
|
554
|
+
} else {
|
|
555
|
+
rejecter("NAVIGATION_ERROR", error?.localizedDescription ?? "Unknown error", error)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
@objc func reLaunch(_ url: String,
|
|
561
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
562
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
563
|
+
NebulaRouter.shared.reLaunchURL(url, fromAppId: appId) { success, error in
|
|
564
|
+
if success {
|
|
565
|
+
resolver(["success": true])
|
|
566
|
+
} else {
|
|
567
|
+
rejecter("NAVIGATION_ERROR", error?.localizedDescription ?? "Unknown error", error)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@objc func navigateBack(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
573
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
574
|
+
NebulaRouter.shared.navigateBack(fromAppId: appId, delta: 1) { success, error in
|
|
575
|
+
if success {
|
|
576
|
+
resolver(["success": true])
|
|
577
|
+
} else {
|
|
578
|
+
rejecter("NAVIGATION_ERROR", error?.localizedDescription ?? "Unknown error", error)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
@objc func navigateBackWithDelta(_ delta: NSNumber,
|
|
584
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
585
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
586
|
+
NebulaRouter.shared.navigateBack(fromAppId: appId, delta: delta.intValue) { success, error in
|
|
587
|
+
if success {
|
|
588
|
+
resolver(["success": true])
|
|
589
|
+
} else {
|
|
590
|
+
rejecter("NAVIGATION_ERROR", error?.localizedDescription ?? "Unknown error", error)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NebulaRouterBridge.m
|
|
3
|
+
// SuperApp - Nebula Mini-App Container
|
|
4
|
+
//
|
|
5
|
+
// Objective-C bridge for NebulaRouterModule
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <React/RCTBridgeModule.h>
|
|
9
|
+
|
|
10
|
+
@interface RCT_EXTERN_MODULE(NebulaRouterModule, NSObject)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(navigateTo:(NSString *)url
|
|
13
|
+
resolver:(RCTPromiseResolveBlock)resolver
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(navigateBack:(RCTPromiseResolveBlock)resolver
|
|
17
|
+
rejecter:(RCTPromiseRejectBlock)rejecter)
|
|
18
|
+
|
|
19
|
+
@end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import React
|
|
3
|
+
import React_RCTAppDelegate
|
|
4
|
+
|
|
5
|
+
public final class RNInstanceViewController: UIViewController {
|
|
6
|
+
private let moduleName: String
|
|
7
|
+
private let initialProperties: [AnyHashable: Any]?
|
|
8
|
+
private let prefersNavigationBarHidden: Bool
|
|
9
|
+
private weak var rootViewFactory: RCTRootViewFactory?
|
|
10
|
+
|
|
11
|
+
public init(
|
|
12
|
+
rootViewFactory: RCTRootViewFactory?,
|
|
13
|
+
moduleName: String,
|
|
14
|
+
initialProperties: [AnyHashable: Any]? = nil,
|
|
15
|
+
title: String? = nil,
|
|
16
|
+
prefersNavigationBarHidden: Bool = false
|
|
17
|
+
) {
|
|
18
|
+
self.rootViewFactory = rootViewFactory
|
|
19
|
+
self.moduleName = moduleName
|
|
20
|
+
self.initialProperties = initialProperties
|
|
21
|
+
self.prefersNavigationBarHidden = prefersNavigationBarHidden
|
|
22
|
+
super.init(nibName: nil, bundle: nil)
|
|
23
|
+
self.title = title
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@available(*, unavailable)
|
|
27
|
+
required init?(coder: NSCoder) {
|
|
28
|
+
fatalError("init(coder:) has not been implemented")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public override func loadView() {
|
|
32
|
+
guard let rootViewFactory else {
|
|
33
|
+
view = UIView()
|
|
34
|
+
view.backgroundColor = .systemBackground
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let rootView = rootViewFactory.view(
|
|
39
|
+
withModuleName: moduleName,
|
|
40
|
+
initialProperties: initialProperties
|
|
41
|
+
)
|
|
42
|
+
let wantsTransparentBackground =
|
|
43
|
+
(initialProperties?["__transparentBackground"] as? Bool) ?? false
|
|
44
|
+
rootView.backgroundColor = wantsTransparentBackground ? .clear : .systemBackground
|
|
45
|
+
view = rootView
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public override func viewWillAppear(_ animated: Bool) {
|
|
49
|
+
super.viewWillAppear(animated)
|
|
50
|
+
navigationController?.setNavigationBarHidden(prefersNavigationBarHidden, animated: animated)
|
|
51
|
+
}
|
|
52
|
+
}
|