@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,682 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NebulaNativeModule.swift
|
|
3
|
+
// SuperApp - Nebula Mini-App Container
|
|
4
|
+
//
|
|
5
|
+
// Native module for main app to open mini-apps
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import React
|
|
10
|
+
|
|
11
|
+
@objc(NebulaNativeModule)
|
|
12
|
+
class NebulaNativeModule: RCTEventEmitter {
|
|
13
|
+
|
|
14
|
+
private enum BridgeMessageDirection: String {
|
|
15
|
+
case toHost
|
|
16
|
+
case toMiniApp
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private static let messageNotificationName = Notification.Name("NebulaBridgeMessage")
|
|
20
|
+
private static let hostMessageEvent = "NebulaHostMessage"
|
|
21
|
+
private static let miniAppMessageEvent = "NebulaMiniAppMessage"
|
|
22
|
+
private static let pageLifecycleEvent = "NebulaPageLifecycle"
|
|
23
|
+
|
|
24
|
+
private let emitterId = UUID().uuidString
|
|
25
|
+
private var hasListeners = false
|
|
26
|
+
private var currentRuntimeAppId: String?
|
|
27
|
+
private var messageObserver: NSObjectProtocol?
|
|
28
|
+
private var pageLifecycleObservers: [NSObjectProtocol] = []
|
|
29
|
+
private weak var presentedHostModalViewController: UIViewController?
|
|
30
|
+
|
|
31
|
+
override init() {
|
|
32
|
+
super.init()
|
|
33
|
+
messageObserver = NotificationCenter.default.addObserver(
|
|
34
|
+
forName: Self.messageNotificationName,
|
|
35
|
+
object: nil,
|
|
36
|
+
queue: .main
|
|
37
|
+
) { [weak self] notification in
|
|
38
|
+
self?.handleBridgeMessage(notification)
|
|
39
|
+
}
|
|
40
|
+
pageLifecycleObservers = [
|
|
41
|
+
NotificationCenter.default.addObserver(
|
|
42
|
+
forName: NSNotification.Name("NebulaContainerWillAppear"),
|
|
43
|
+
object: nil,
|
|
44
|
+
queue: .main
|
|
45
|
+
) { [weak self] notification in
|
|
46
|
+
self?.handlePageLifecycle(notification, type: "show")
|
|
47
|
+
},
|
|
48
|
+
NotificationCenter.default.addObserver(
|
|
49
|
+
forName: NSNotification.Name("NebulaContainerDidDisappear"),
|
|
50
|
+
object: nil,
|
|
51
|
+
queue: .main
|
|
52
|
+
) { [weak self] notification in
|
|
53
|
+
self?.handlePageLifecycle(notification, type: "hide")
|
|
54
|
+
},
|
|
55
|
+
NotificationCenter.default.addObserver(
|
|
56
|
+
forName: NSNotification.Name("NebulaContainerDidUnload"),
|
|
57
|
+
object: nil,
|
|
58
|
+
queue: .main
|
|
59
|
+
) { [weak self] notification in
|
|
60
|
+
self?.handlePageLifecycle(notification, type: "unload")
|
|
61
|
+
},
|
|
62
|
+
NotificationCenter.default.addObserver(
|
|
63
|
+
forName: NSNotification.Name("NebulaContainerDidBecomeReady"),
|
|
64
|
+
object: nil,
|
|
65
|
+
queue: .main
|
|
66
|
+
) { [weak self] notification in
|
|
67
|
+
self?.handlePageLifecycle(notification, type: "ready")
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deinit {
|
|
73
|
+
if let observer = messageObserver {
|
|
74
|
+
NotificationCenter.default.removeObserver(observer)
|
|
75
|
+
}
|
|
76
|
+
for observer in pageLifecycleObservers {
|
|
77
|
+
NotificationCenter.default.removeObserver(observer)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private func resolveTopViewController() -> UIViewController? {
|
|
82
|
+
guard let navigationController = NebulaHost.shared.currentNavigationController() else {
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var topViewController: UIViewController = navigationController
|
|
87
|
+
while let presented = topViewController.presentedViewController {
|
|
88
|
+
topViewController = presented
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if let navigation = topViewController as? UINavigationController {
|
|
92
|
+
return navigation.topViewController ?? navigation
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return topViewController
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@objc override class func moduleName() -> String {
|
|
99
|
+
return "NebulaNativeModule"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@objc override class func requiresMainQueueSetup() -> Bool {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override func supportedEvents() -> [String]! {
|
|
107
|
+
return [Self.hostMessageEvent, Self.miniAppMessageEvent, Self.pageLifecycleEvent]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override func startObserving() {
|
|
111
|
+
hasListeners = true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override func stopObserving() {
|
|
115
|
+
hasListeners = false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Open a mini-app from the main React Native app
|
|
119
|
+
@objc func openMiniApp(_ appId: String,
|
|
120
|
+
initialProps: NSDictionary?,
|
|
121
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
122
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
123
|
+
DispatchQueue.main.async {
|
|
124
|
+
guard let window = UIApplication.shared.keyWindow,
|
|
125
|
+
let rootVC = window.rootViewController else {
|
|
126
|
+
rejecter("NO_ROOT_VC", "Cannot find root view controller", nil)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Find the topmost view controller
|
|
131
|
+
var topVC = rootVC
|
|
132
|
+
while let presented = topVC.presentedViewController {
|
|
133
|
+
topVC = presented
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If it's a navigation controller, use it
|
|
137
|
+
let sourceVC = (topVC as? UINavigationController)?.topViewController ?? topVC
|
|
138
|
+
|
|
139
|
+
// Open mini-app
|
|
140
|
+
let props = initialProps as? [String: Any]
|
|
141
|
+
NebulaHost.shared.openApp(appId, from: sourceVC, initialProps: props)
|
|
142
|
+
|
|
143
|
+
resolver(["success": true, "appId": appId])
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Preload a mini-app
|
|
148
|
+
@objc func preloadMiniApp(_ appId: String,
|
|
149
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
150
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
151
|
+
NebulaHost.shared.preloadApp(appId)
|
|
152
|
+
resolver(["success": true, "appId": appId])
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@objc func openMiniAppWithBundleURL(_ appId: String,
|
|
156
|
+
bundleURL: String,
|
|
157
|
+
connectToURLMetroServer: Bool,
|
|
158
|
+
initialProps: NSDictionary?,
|
|
159
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
160
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
161
|
+
NebulaHost.shared.installApp(
|
|
162
|
+
appId,
|
|
163
|
+
bundleURL: bundleURL,
|
|
164
|
+
connectToURLMetroServer: connectToURLMetroServer
|
|
165
|
+
) { success, error in
|
|
166
|
+
if success {
|
|
167
|
+
DispatchQueue.main.async {
|
|
168
|
+
guard let window = UIApplication.shared.keyWindow,
|
|
169
|
+
let rootVC = window.rootViewController else {
|
|
170
|
+
rejecter("NO_ROOT_VC", "Cannot find root view controller", nil)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var topVC = rootVC
|
|
175
|
+
while let presented = topVC.presentedViewController {
|
|
176
|
+
topVC = presented
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let sourceVC = (topVC as? UINavigationController)?.topViewController ?? topVC
|
|
180
|
+
let props = initialProps as? [String: Any]
|
|
181
|
+
NebulaHost.shared.openApp(appId, from: sourceVC, initialProps: props)
|
|
182
|
+
resolver(["success": true, "appId": appId])
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
rejecter("OPEN_ERROR", error?.localizedDescription ?? "Failed to open", error)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Install and preload a mini-app from a bundle URL
|
|
191
|
+
@objc func preloadMiniAppWithBundleURL(_ appId: String,
|
|
192
|
+
bundleURL: String,
|
|
193
|
+
connectToURLMetroServer: Bool,
|
|
194
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
195
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
196
|
+
NebulaHost.shared.installApp(
|
|
197
|
+
appId,
|
|
198
|
+
bundleURL: bundleURL,
|
|
199
|
+
connectToURLMetroServer: connectToURLMetroServer
|
|
200
|
+
) { success, error in
|
|
201
|
+
if success {
|
|
202
|
+
NebulaHost.shared.preloadApp(appId)
|
|
203
|
+
resolver(["success": true, "appId": appId])
|
|
204
|
+
} else {
|
|
205
|
+
rejecter("PRELOAD_ERROR", error?.localizedDescription ?? "Failed to preload", error)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Install a mini-app from URL
|
|
211
|
+
@objc func installMiniApp(_ appId: String,
|
|
212
|
+
bundleURL: String,
|
|
213
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
214
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
215
|
+
NebulaHost.shared.installApp(appId, bundleURL: bundleURL) { success, error in
|
|
216
|
+
if success {
|
|
217
|
+
resolver(["success": true, "appId": appId])
|
|
218
|
+
} else {
|
|
219
|
+
rejecter("INSTALL_ERROR", error?.localizedDescription ?? "Failed to install", error)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// Install a mini-app from URL with explicit Metro connection preference
|
|
225
|
+
@objc func installMiniAppWithBundleURL(_ appId: String,
|
|
226
|
+
bundleURL: String,
|
|
227
|
+
connectToURLMetroServer: Bool,
|
|
228
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
229
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
230
|
+
NebulaHost.shared.installApp(
|
|
231
|
+
appId,
|
|
232
|
+
bundleURL: bundleURL,
|
|
233
|
+
connectToURLMetroServer: connectToURLMetroServer
|
|
234
|
+
) { success, error in
|
|
235
|
+
if success {
|
|
236
|
+
resolver(["success": true, "appId": appId])
|
|
237
|
+
} else {
|
|
238
|
+
rejecter("INSTALL_ERROR", error?.localizedDescription ?? "Failed to install", error)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@objc func installMiniAppFromURLs(_ appId: String,
|
|
244
|
+
bundleURL: String,
|
|
245
|
+
manifestURL: String,
|
|
246
|
+
assetsURL: String,
|
|
247
|
+
connectToURLMetroServer: Bool,
|
|
248
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
249
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
250
|
+
NebulaConfig.shared.downloadBundle(
|
|
251
|
+
for: appId,
|
|
252
|
+
from: bundleURL,
|
|
253
|
+
manifestURL: manifestURL,
|
|
254
|
+
assetsURL: assetsURL,
|
|
255
|
+
connectToURLMetroServer: connectToURLMetroServer,
|
|
256
|
+
) { success, error in
|
|
257
|
+
if success {
|
|
258
|
+
NebulaAppManager.shared.invalidate(appId: appId)
|
|
259
|
+
resolver(["success": true, "appId": appId])
|
|
260
|
+
} else {
|
|
261
|
+
rejecter("INSTALL_ERROR", error?.localizedDescription ?? "Failed to install", error)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@objc func closeMiniApp(_ appId: String,
|
|
267
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
268
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
269
|
+
DispatchQueue.main.async {
|
|
270
|
+
NebulaHost.shared.closeApp(appId)
|
|
271
|
+
resolver([
|
|
272
|
+
"success": true,
|
|
273
|
+
"appId": appId
|
|
274
|
+
])
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@objc func uninstallMiniApp(_ appId: String,
|
|
279
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
280
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
281
|
+
do {
|
|
282
|
+
try NebulaHost.shared.uninstallApp(appId)
|
|
283
|
+
resolver(["success": true, "appId": appId])
|
|
284
|
+
} catch {
|
|
285
|
+
rejecter("UNINSTALL_ERROR", error.localizedDescription, error)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Register route table from mini-app JS at startup
|
|
290
|
+
/// Called by the mini-app itself so native routing knows which component handles each path
|
|
291
|
+
@objc func registerRoutes(_ appId: String,
|
|
292
|
+
routes: NSDictionary,
|
|
293
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
294
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
295
|
+
guard let pages = routes as? [String: String] else {
|
|
296
|
+
rejecter("INVALID_ROUTES", "routes must be a flat object mapping path -> componentName", nil)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
currentRuntimeAppId = appId
|
|
300
|
+
NebulaManifestManager.shared.registerManifest(forAppId: appId, pages: pages)
|
|
301
|
+
resolver(["success": true, "appId": appId, "count": pages.count])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@objc func registerManifest(_ appId: String,
|
|
305
|
+
manifest: NSDictionary,
|
|
306
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
307
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
308
|
+
do {
|
|
309
|
+
let data = try JSONSerialization.data(withJSONObject: manifest, options: [])
|
|
310
|
+
let decodedManifest = try JSONDecoder().decode(NebulaManifest.self, from: data)
|
|
311
|
+
currentRuntimeAppId = appId
|
|
312
|
+
NebulaManifestManager.shared.registerManifest(forAppId: appId, manifest: decodedManifest)
|
|
313
|
+
let entryPath = decodedManifest.entryPagePath ?? "/"
|
|
314
|
+
if let pageConfig = NebulaManifestManager.shared.getPageConfig(forAppId: appId, path: entryPath) as? [String: Any] {
|
|
315
|
+
NebulaRouter.shared.updatePageStyle(forAppId: appId, style: pageConfig as NSDictionary) { _, _ in }
|
|
316
|
+
}
|
|
317
|
+
resolver(["success": true, "appId": appId, "count": decodedManifest.pages.count])
|
|
318
|
+
} catch {
|
|
319
|
+
rejecter("INVALID_MANIFEST", "manifest must be a valid Nebula manifest object", error)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// Mini-app -> Host communication
|
|
324
|
+
@objc func postMessageToHost(_ appId: String,
|
|
325
|
+
message: NSDictionary,
|
|
326
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
327
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
328
|
+
currentRuntimeAppId = appId
|
|
329
|
+
publishBridgeMessage(direction: .toHost, appId: appId, message: message)
|
|
330
|
+
resolver(["errMsg": "postMessageToHost:ok"])
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Host -> Mini-app communication
|
|
334
|
+
@objc func postMessageToMiniApp(_ appId: String,
|
|
335
|
+
message: NSDictionary,
|
|
336
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
337
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
338
|
+
publishBridgeMessage(direction: .toMiniApp, appId: appId, message: message)
|
|
339
|
+
resolver(["errMsg": "postMessageToMiniApp:ok"])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// Get list of installed mini-apps
|
|
343
|
+
@objc func getInstalledMiniApps(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
344
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
345
|
+
let apps = NebulaHost.shared.installedApps()
|
|
346
|
+
resolver(["apps": apps])
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@objc func getInstalledMiniAppInfo(_ appId: String,
|
|
350
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
351
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
352
|
+
guard let info = NebulaHost.shared.installedAppInfo(appId) else {
|
|
353
|
+
resolver([
|
|
354
|
+
"installed": false,
|
|
355
|
+
"app": NSNull()
|
|
356
|
+
])
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
resolver([
|
|
361
|
+
"installed": true,
|
|
362
|
+
"app": [
|
|
363
|
+
"appId": info.appId,
|
|
364
|
+
"mode": info.mode,
|
|
365
|
+
"bundlePath": info.bundlePath,
|
|
366
|
+
"sourceUrl": info.sourceUrl,
|
|
367
|
+
"version": info.version as Any,
|
|
368
|
+
"updateStrategy": info.updateStrategy
|
|
369
|
+
]
|
|
370
|
+
])
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@objc func getServerBaseURL(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
374
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
375
|
+
resolver([
|
|
376
|
+
"serverBaseURL": NebulaConfig.shared.serverBaseURL
|
|
377
|
+
])
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@objc func setServerBaseURL(_ serverBaseURL: String?,
|
|
381
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
382
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
383
|
+
let trimmedValue = serverBaseURL?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
384
|
+
if let trimmedValue,
|
|
385
|
+
!trimmedValue.isEmpty,
|
|
386
|
+
NebulaConfig.normalizeServerBaseURL(trimmedValue) == nil {
|
|
387
|
+
rejecter("INVALID_SERVER_URL", "serverBaseURL must be a valid http(s) URL", nil)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
NebulaConfig.shared.serverBaseURL = trimmedValue
|
|
392
|
+
resolver([
|
|
393
|
+
"serverBaseURL": NebulaConfig.shared.serverBaseURL
|
|
394
|
+
])
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@objc func setMiniappLoadingDelay(_ delayMs: NSNumber?,
|
|
398
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
399
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
400
|
+
let normalizedDelayMs = max(0, delayMs?.doubleValue ?? 0)
|
|
401
|
+
miniappLoadingDelay = normalizedDelayMs / 1000.0
|
|
402
|
+
resolver([
|
|
403
|
+
"delayMs": normalizedDelayMs
|
|
404
|
+
])
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@objc func setMiniappLoadingEnterContentDelay(_ delayMs: NSNumber?,
|
|
408
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
409
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
410
|
+
let normalizedDelayMs = max(0, delayMs?.doubleValue ?? 0)
|
|
411
|
+
miniappLoadingEnterContentDelay = normalizedDelayMs / 1000.0
|
|
412
|
+
resolver([
|
|
413
|
+
"delayMs": normalizedDelayMs
|
|
414
|
+
])
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@objc func bringHostToFront(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
418
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
419
|
+
DispatchQueue.main.async {
|
|
420
|
+
let token = NebulaHost.shared.bringHostToFront()
|
|
421
|
+
resolver([
|
|
422
|
+
"success": token != nil,
|
|
423
|
+
"token": token
|
|
424
|
+
])
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@objc func restoreMiniApp(_ token: String?,
|
|
429
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
430
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
431
|
+
DispatchQueue.main.async {
|
|
432
|
+
let success = NebulaHost.shared.restoreMiniApp(with: token)
|
|
433
|
+
resolver([
|
|
434
|
+
"success": success,
|
|
435
|
+
"token": token as Any
|
|
436
|
+
])
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@objc func presentHostModal(_ moduleName: String,
|
|
441
|
+
props: NSDictionary,
|
|
442
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
443
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
444
|
+
DispatchQueue.main.async {
|
|
445
|
+
guard let presenter = self.resolveTopViewController() else {
|
|
446
|
+
resolver(["errMsg": "presentHostModal:fail no_presenter"])
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let initialProps = props as? [AnyHashable: Any]
|
|
451
|
+
guard let modalViewController = NebulaHost.shared.makeHostViewController(
|
|
452
|
+
moduleName: moduleName,
|
|
453
|
+
initialProperties: initialProps,
|
|
454
|
+
title: moduleName
|
|
455
|
+
) else {
|
|
456
|
+
resolver(["errMsg": "presentHostModal:fail no_host_view_controller"])
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
modalViewController.modalPresentationStyle = .overFullScreen
|
|
460
|
+
modalViewController.modalTransitionStyle = .crossDissolve
|
|
461
|
+
modalViewController.view.backgroundColor = .clear
|
|
462
|
+
self.presentedHostModalViewController = modalViewController
|
|
463
|
+
|
|
464
|
+
presenter.present(modalViewController, animated: true) {
|
|
465
|
+
resolver(["errMsg": "presentHostModal:ok"])
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@objc func dismissHostModal(_ resolver: @escaping RCTPromiseResolveBlock,
|
|
471
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
472
|
+
DispatchQueue.main.async {
|
|
473
|
+
let modalViewController =
|
|
474
|
+
self.presentedHostModalViewController
|
|
475
|
+
?? self.resolveTopViewController()?.presentedViewController
|
|
476
|
+
?? self.resolveTopViewController()
|
|
477
|
+
|
|
478
|
+
guard let modalViewController else {
|
|
479
|
+
resolver(["errMsg": "dismissHostModal:ok"])
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
modalViewController.dismiss(animated: true) {
|
|
484
|
+
self.presentedHostModalViewController = nil
|
|
485
|
+
resolver(["errMsg": "dismissHostModal:ok"])
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// MARK: - Mini-App Navigation APIs
|
|
491
|
+
|
|
492
|
+
@objc func navigateTo(_ appId: String,
|
|
493
|
+
url: String,
|
|
494
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
495
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
496
|
+
DispatchQueue.main.async {
|
|
497
|
+
NebulaRouter.shared.navigateToURL(url, fromAppId: appId) { success, error in
|
|
498
|
+
if success {
|
|
499
|
+
resolver(["errMsg": "navigateTo:ok"])
|
|
500
|
+
} else {
|
|
501
|
+
rejecter("navigateTo", error?.localizedDescription ?? "unknown", nil)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
@objc func redirectTo(_ appId: String,
|
|
508
|
+
url: String,
|
|
509
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
510
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
511
|
+
DispatchQueue.main.async {
|
|
512
|
+
NebulaRouter.shared.redirectToURL(url, fromAppId: appId) { success, error in
|
|
513
|
+
if success {
|
|
514
|
+
resolver(["errMsg": "redirectTo:ok"])
|
|
515
|
+
} else {
|
|
516
|
+
rejecter("redirectTo", error?.localizedDescription ?? "unknown", nil)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@objc func reLaunch(_ appId: String,
|
|
523
|
+
url: String,
|
|
524
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
525
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
526
|
+
DispatchQueue.main.async {
|
|
527
|
+
NebulaRouter.shared.reLaunchURL(url, fromAppId: appId) { success, error in
|
|
528
|
+
if success {
|
|
529
|
+
resolver(["errMsg": "reLaunch:ok"])
|
|
530
|
+
} else {
|
|
531
|
+
rejecter("reLaunch", error?.localizedDescription ?? "unknown", nil)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
@objc func navigateBack(_ appId: String,
|
|
538
|
+
delta: Int,
|
|
539
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
540
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
541
|
+
DispatchQueue.main.async {
|
|
542
|
+
NebulaRouter.shared.navigateBack(fromAppId: appId, delta: delta) { success, error in
|
|
543
|
+
if success {
|
|
544
|
+
resolver(["errMsg": "navigateBack:ok"])
|
|
545
|
+
} else {
|
|
546
|
+
rejecter("navigateBack", error?.localizedDescription ?? "unknown", nil)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@objc func setPageStyle(_ appId: String,
|
|
553
|
+
style: NSDictionary,
|
|
554
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
555
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
556
|
+
DispatchQueue.main.async {
|
|
557
|
+
NebulaRouter.shared.updatePageStyle(forAppId: appId, style: style) { success, error in
|
|
558
|
+
if success {
|
|
559
|
+
resolver(["errMsg": "setPageStyle:ok"])
|
|
560
|
+
} else {
|
|
561
|
+
rejecter("setPageStyle", error?.localizedDescription ?? "unknown", nil)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@objc func showToast(_ title: String,
|
|
568
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
569
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
570
|
+
DispatchQueue.main.async {
|
|
571
|
+
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }),
|
|
572
|
+
let rootVC = window.rootViewController else {
|
|
573
|
+
resolver(["errMsg": "showToast:fail no_active_window"])
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
let alert = UIAlertController(title: nil, message: title, preferredStyle: .alert)
|
|
578
|
+
rootVC.presentedViewController?.present(alert, animated: true)
|
|
579
|
+
?? rootVC.present(alert, animated: true)
|
|
580
|
+
|
|
581
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
582
|
+
alert.dismiss(animated: true)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
resolver(["errMsg": "showToast:ok"])
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@objc func getDeviceInfo() -> NSDictionary {
|
|
590
|
+
let device = UIDevice.current
|
|
591
|
+
return [
|
|
592
|
+
"model": device.model,
|
|
593
|
+
"systemVersion": device.systemVersion,
|
|
594
|
+
"platform": "iOS"
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private func publishBridgeMessage(direction: BridgeMessageDirection,
|
|
599
|
+
appId: String,
|
|
600
|
+
message: NSDictionary) {
|
|
601
|
+
let payload = message as? [String: Any] ?? [:]
|
|
602
|
+
NotificationCenter.default.post(
|
|
603
|
+
name: Self.messageNotificationName,
|
|
604
|
+
object: nil,
|
|
605
|
+
userInfo: [
|
|
606
|
+
"direction": direction.rawValue,
|
|
607
|
+
"appId": appId,
|
|
608
|
+
"message": payload,
|
|
609
|
+
"sourceEmitterId": emitterId,
|
|
610
|
+
"timestamp": Date().timeIntervalSince1970
|
|
611
|
+
]
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private func handleBridgeMessage(_ notification: Notification) {
|
|
616
|
+
guard hasListeners,
|
|
617
|
+
let userInfo = notification.userInfo,
|
|
618
|
+
let directionRaw = userInfo["direction"] as? String,
|
|
619
|
+
let direction = BridgeMessageDirection(rawValue: directionRaw),
|
|
620
|
+
let appId = userInfo["appId"] as? String,
|
|
621
|
+
let message = userInfo["message"] as? [String: Any] else {
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let sourceEmitterId = userInfo["sourceEmitterId"] as? String
|
|
626
|
+
if sourceEmitterId == emitterId {
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
switch direction {
|
|
631
|
+
case .toHost:
|
|
632
|
+
guard !isMiniAppRuntime() else {
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
sendEvent(withName: Self.hostMessageEvent, body: [
|
|
636
|
+
"appId": appId,
|
|
637
|
+
"message": message,
|
|
638
|
+
"timestamp": userInfo["timestamp"] ?? Date().timeIntervalSince1970
|
|
639
|
+
])
|
|
640
|
+
case .toMiniApp:
|
|
641
|
+
let miniAppRuntime = isMiniAppRuntime()
|
|
642
|
+
guard miniAppRuntime, currentRuntimeAppId == appId else {
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
sendEvent(withName: Self.miniAppMessageEvent, body: [
|
|
646
|
+
"appId": appId,
|
|
647
|
+
"message": message,
|
|
648
|
+
"timestamp": userInfo["timestamp"] ?? Date().timeIntervalSince1970
|
|
649
|
+
])
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private func handlePageLifecycle(_ notification: Notification, type: String) {
|
|
654
|
+
guard hasListeners,
|
|
655
|
+
let userInfo = notification.userInfo,
|
|
656
|
+
let appId = userInfo["appId"] as? String,
|
|
657
|
+
(!isMiniAppRuntime() || currentRuntimeAppId == appId),
|
|
658
|
+
let instanceId = userInfo["instanceId"] as? String else {
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
sendEvent(withName: Self.pageLifecycleEvent, body: [
|
|
663
|
+
"appId": appId,
|
|
664
|
+
"instanceId": instanceId,
|
|
665
|
+
"routePath": userInfo["routePath"] as? String ?? "/",
|
|
666
|
+
"type": type
|
|
667
|
+
])
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private func isMiniAppRuntime() -> Bool {
|
|
671
|
+
if let currentRuntimeAppId, !currentRuntimeAppId.isEmpty {
|
|
672
|
+
return true
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
guard let bundleURLString = bridge?.bundleURL?.absoluteString.lowercased() else {
|
|
676
|
+
return false
|
|
677
|
+
}
|
|
678
|
+
return bundleURLString.contains("/miniapps/")
|
|
679
|
+
|| bundleURLString.contains("entryfile=miniapps/")
|
|
680
|
+
|| bundleURLString.contains("entryfile=miniapps%2f")
|
|
681
|
+
}
|
|
682
|
+
}
|