@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.
Files changed (37) hide show
  1. package/NebulaHost.podspec +23 -0
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build.gradle +27 -0
  11. package/android/consumer-rules.pro +1 -0
  12. package/android/src/main/AndroidManifest.xml +1 -0
  13. package/android/src/main/java/com/hectorzhuang/nebula/NebulaActivity.kt +290 -0
  14. package/android/src/main/java/com/hectorzhuang/nebula/NebulaAppManager.kt +134 -0
  15. package/android/src/main/java/com/hectorzhuang/nebula/NebulaConfig.kt +324 -0
  16. package/android/src/main/java/com/hectorzhuang/nebula/NebulaEventHub.kt +49 -0
  17. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHost.kt +145 -0
  18. package/android/src/main/java/com/hectorzhuang/nebula/NebulaHostModalActivity.kt +178 -0
  19. package/android/src/main/java/com/hectorzhuang/nebula/NebulaManifestManager.kt +130 -0
  20. package/android/src/main/java/com/hectorzhuang/nebula/NebulaNativeModule.kt +604 -0
  21. package/android/src/main/java/com/hectorzhuang/nebula/NebulaPackage.kt +16 -0
  22. package/android/src/main/java/com/hectorzhuang/nebula/NebulaRouter.kt +300 -0
  23. package/ios/Nebula/NebulaAppManager.swift +355 -0
  24. package/ios/Nebula/NebulaConfig.swift +549 -0
  25. package/ios/Nebula/NebulaContainerController.swift +580 -0
  26. package/ios/Nebula/NebulaDevLoading.swift +333 -0
  27. package/ios/Nebula/NebulaHost.swift +611 -0
  28. package/ios/Nebula/NebulaManifest.swift +214 -0
  29. package/ios/Nebula/NebulaNativeModule.swift +682 -0
  30. package/ios/Nebula/NebulaNativeModuleBridge.m +364 -0
  31. package/ios/Nebula/NebulaPerformanceMonitor.swift +46 -0
  32. package/ios/Nebula/NebulaRouter.swift +594 -0
  33. package/ios/Nebula/NebulaRouterBridge.m +19 -0
  34. package/ios/Nebula/RNInstanceViewController.swift +52 -0
  35. package/package.json +41 -0
  36. package/react-native.config.js +14 -0
  37. 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
+ }