@nebula-rn/sdk 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.
@@ -0,0 +1,153 @@
1
+ import { AppRegistry } from 'react-native';
2
+ import { NEBULA_INTERNAL_MINIAPP_LOADING_MODULE } from './nebulaNative';
3
+ import type {
4
+ InstalledMiniAppInfo,
5
+ MiniappLoadingProps,
6
+ MiniappLoadingResolveContext,
7
+ MiniappLoadingResolvedProps,
8
+ MiniappLoadingStatus,
9
+ } from './nebulaTypes';
10
+ import { createElement, useEffect, useState } from 'react';
11
+
12
+ type MiniappLoadingState = {
13
+ appId: string | null;
14
+ mode: 'development' | 'production';
15
+ status: MiniappLoadingStatus;
16
+ title?: string | null;
17
+ icon?: MiniappLoadingProps['icon'];
18
+ extraData?: MiniappLoadingProps['extraData'];
19
+ installedInfo?: InstalledMiniAppInfo | null;
20
+ errorMessage?: string | null;
21
+ visible: boolean;
22
+ };
23
+
24
+ let miniappLoadingDelayMs = 0;
25
+
26
+ let internalMiniappLoadingRegistered = false;
27
+ let miniappLoadingComponent: React.ComponentType<MiniappLoadingProps> | null =
28
+ null;
29
+ let miniappLoadingResolveProps:
30
+ | ((context: MiniappLoadingResolveContext) => MiniappLoadingResolvedProps)
31
+ | null = null;
32
+
33
+ let miniappLoadingState: MiniappLoadingState = {
34
+ appId: null,
35
+ mode: 'production',
36
+ status: 'loading',
37
+ title: null,
38
+ icon: null,
39
+ extraData: null,
40
+ installedInfo: null,
41
+ errorMessage: null,
42
+ visible: false,
43
+ };
44
+
45
+ const miniappLoadingListeners = new Set<() => void>();
46
+
47
+ function emitMiniappLoadingState(): void {
48
+ miniappLoadingListeners.forEach(listener => listener());
49
+ }
50
+
51
+ export function configureMiniappLoadingComponent(
52
+ component: React.ComponentType<MiniappLoadingProps> | null,
53
+ resolveProps?:
54
+ | ((context: MiniappLoadingResolveContext) => MiniappLoadingResolvedProps)
55
+ | null,
56
+ delayMs?: number | null,
57
+ ): void {
58
+ miniappLoadingComponent = component;
59
+ miniappLoadingResolveProps = resolveProps ?? null;
60
+ miniappLoadingDelayMs =
61
+ typeof delayMs === 'number' && Number.isFinite(delayMs)
62
+ ? Math.max(0, Math.round(delayMs))
63
+ : 0;
64
+ }
65
+
66
+ export function setMiniappLoadingState(
67
+ nextState: Partial<MiniappLoadingState>,
68
+ ): void {
69
+ miniappLoadingState = {
70
+ ...miniappLoadingState,
71
+ ...nextState,
72
+ };
73
+ emitMiniappLoadingState();
74
+ }
75
+
76
+ export function hideMiniappLoading(appId?: string | null): void {
77
+ if (
78
+ appId &&
79
+ miniappLoadingState.appId &&
80
+ miniappLoadingState.appId !== appId
81
+ ) {
82
+ return;
83
+ }
84
+ setMiniappLoadingState({
85
+ visible: false,
86
+ status: 'ready',
87
+ title: null,
88
+ icon: null,
89
+ extraData: null,
90
+ installedInfo: null,
91
+ errorMessage: null,
92
+ });
93
+ }
94
+
95
+ export function getMiniappLoadingDelayMs(): number {
96
+ return miniappLoadingDelayMs;
97
+ }
98
+
99
+ export function ensureInternalMiniappLoadingComponentRegistered(): void {
100
+ if (internalMiniappLoadingRegistered) {
101
+ return;
102
+ }
103
+
104
+ AppRegistry.registerComponent(NEBULA_INTERNAL_MINIAPP_LOADING_MODULE, () => {
105
+ const NebulaInternalMiniappLoading = () => {
106
+ const [, forceUpdate] = useState(0);
107
+
108
+ useEffect(() => {
109
+ const listener = () => forceUpdate(value => value + 1);
110
+ miniappLoadingListeners.add(listener);
111
+ return () => {
112
+ miniappLoadingListeners.delete(listener);
113
+ };
114
+ }, []);
115
+
116
+ if (!miniappLoadingState.visible || !miniappLoadingState.appId) {
117
+ return null;
118
+ }
119
+
120
+ const Component = miniappLoadingComponent;
121
+ if (!Component) {
122
+ return null;
123
+ }
124
+
125
+ const resolvedProps = miniappLoadingResolveProps
126
+ ? miniappLoadingResolveProps({
127
+ appId: miniappLoadingState.appId,
128
+ mode: miniappLoadingState.mode,
129
+ status: miniappLoadingState.status,
130
+ title: miniappLoadingState.title,
131
+ icon: miniappLoadingState.icon,
132
+ extraData: miniappLoadingState.extraData,
133
+ errorMessage: miniappLoadingState.errorMessage,
134
+ installedInfo: miniappLoadingState.installedInfo,
135
+ })
136
+ : null;
137
+
138
+ return createElement(Component, {
139
+ appId: miniappLoadingState.appId,
140
+ mode: miniappLoadingState.mode,
141
+ status: miniappLoadingState.status,
142
+ title: resolvedProps?.title ?? miniappLoadingState.title,
143
+ icon: resolvedProps?.icon ?? miniappLoadingState.icon,
144
+ extraData: resolvedProps?.extraData ?? miniappLoadingState.extraData,
145
+ errorMessage: miniappLoadingState.errorMessage,
146
+ });
147
+ };
148
+
149
+ return NebulaInternalMiniappLoading;
150
+ });
151
+
152
+ internalMiniappLoadingRegistered = true;
153
+ }
@@ -0,0 +1,140 @@
1
+ import React, { useContext, useEffect, useRef } from 'react';
2
+ import { PAGE_RESERVED_PROP_KEYS } from './nebulaNative';
3
+ import { Miniapp } from './miniappRuntime';
4
+ import type {
5
+ MiniAppPageContextValue,
6
+ PageLifecycleEvent,
7
+ } from './nebulaTypes';
8
+
9
+ const MiniAppPageContext = React.createContext<MiniAppPageContextValue | null>(
10
+ null,
11
+ );
12
+
13
+ function extractPageParams(
14
+ props: Record<string, unknown>,
15
+ ): Record<string, unknown> {
16
+ const params: Record<string, unknown> = {};
17
+
18
+ for (const key in props) {
19
+ if (
20
+ Object.prototype.hasOwnProperty.call(props, key) &&
21
+ !PAGE_RESERVED_PROP_KEYS.has(key)
22
+ ) {
23
+ params[key] = props[key];
24
+ }
25
+ }
26
+
27
+ return params;
28
+ }
29
+
30
+ function usePageLifecycleSubscription(
31
+ eventType: PageLifecycleEvent['type'],
32
+ callback: () => void,
33
+ ): void {
34
+ const pageContext = useContext(MiniAppPageContext);
35
+ const callbackRef = useRef(callback);
36
+ callbackRef.current = callback;
37
+
38
+ useEffect(() => {
39
+ if (!pageContext?.instanceId) {
40
+ return;
41
+ }
42
+ return Miniapp.onPageLifecycle(event => {
43
+ if (
44
+ event.type === eventType &&
45
+ event.instanceId === pageContext.instanceId
46
+ ) {
47
+ callbackRef.current();
48
+ }
49
+ });
50
+ }, [eventType, pageContext?.instanceId]);
51
+ }
52
+
53
+ export function createMiniAppPage<P extends Record<string, unknown>>(
54
+ Component: React.ComponentType<P>,
55
+ ): React.ComponentType<P> {
56
+ return function NebulaMiniAppPage(props: P) {
57
+ const appId =
58
+ typeof props.appId === 'string' && props.appId.length > 0
59
+ ? props.appId
60
+ : null;
61
+
62
+ useEffect(() => {
63
+ Miniapp.bootstrap(appId);
64
+ }, [appId]);
65
+
66
+ const pageContext: MiniAppPageContextValue = {
67
+ appId,
68
+ instanceId:
69
+ typeof props.instanceId === 'string' && props.instanceId.length > 0
70
+ ? props.instanceId
71
+ : null,
72
+ params: extractPageParams(props),
73
+ routePath:
74
+ typeof props.__routePath === 'string' ? props.__routePath : '/',
75
+ routeUrl: typeof props.__routeUrl === 'string' ? props.__routeUrl : '',
76
+ pageStyle:
77
+ props.__pageConfig &&
78
+ typeof props.__pageConfig === 'object' &&
79
+ !Array.isArray(props.__pageConfig)
80
+ ? (props.__pageConfig as Record<string, unknown>)
81
+ : {},
82
+ };
83
+
84
+ return React.createElement(
85
+ MiniAppPageContext.Provider,
86
+ { value: pageContext },
87
+ React.createElement(Component, props),
88
+ );
89
+ };
90
+ }
91
+
92
+ export function usePageOnLoad(
93
+ callback: (params: Record<string, unknown>) => void,
94
+ ): void {
95
+ const pageContext = useContext(MiniAppPageContext);
96
+ const callbackRef = useRef(callback);
97
+ callbackRef.current = callback;
98
+
99
+ useEffect(() => {
100
+ callbackRef.current(pageContext?.params ?? {});
101
+ }, [pageContext?.instanceId]);
102
+ }
103
+
104
+ export function usePageOnShow(callback: () => void): void {
105
+ const callbackRef = useRef(callback);
106
+ callbackRef.current = callback;
107
+
108
+ useEffect(() => {
109
+ callbackRef.current();
110
+ }, []);
111
+
112
+ usePageLifecycleSubscription('show', callback);
113
+ }
114
+
115
+ export function usePageOnReady(callback: () => void): void {
116
+ const callbackRef = useRef(callback);
117
+ callbackRef.current = callback;
118
+
119
+ useEffect(() => {
120
+ const frame = requestAnimationFrame(() => {
121
+ callbackRef.current();
122
+ });
123
+ return () => cancelAnimationFrame(frame);
124
+ }, []);
125
+ }
126
+
127
+ export function usePageOnHide(callback: () => void): void {
128
+ usePageLifecycleSubscription('hide', callback);
129
+ }
130
+
131
+ export function usePageOnUnload(callback: () => void): void {
132
+ const callbackRef = useRef(callback);
133
+ callbackRef.current = callback;
134
+
135
+ useEffect(() => {
136
+ return () => {
137
+ callbackRef.current();
138
+ };
139
+ }, []);
140
+ }
@@ -0,0 +1,344 @@
1
+ import {
2
+ addNebulaEventListener,
3
+ getNebulaNativeModule,
4
+ isProtocolResponse,
5
+ NEBULA_MINI_APP_MESSAGE_EVENT,
6
+ NEBULA_PAGE_LIFECYCLE_EVENT,
7
+ } from './nebulaNative';
8
+ import type {
9
+ BridgeMessage,
10
+ HostVisibilityResult,
11
+ NavigationResult,
12
+ NebulaApiInvokeResult,
13
+ NebulaHostApiDescriptionMap,
14
+ NebulaHostCapabilityMap,
15
+ NebulaPageStyle,
16
+ PageLifecycleEvent,
17
+ PendingProtocolRequest,
18
+ } from './nebulaTypes';
19
+
20
+ let currentMiniAppId: string | null = null;
21
+ let miniAppProtocolListening = false;
22
+ const pendingProtocolRequests = new Map<string, PendingProtocolRequest>();
23
+
24
+ export function compareCapabilityVersions(left: string, right: string): number {
25
+ const leftParts = left.split('.').map(part => Number(part) || 0);
26
+ const rightParts = right.split('.').map(part => Number(part) || 0);
27
+ const maxLength = Math.max(leftParts.length, rightParts.length);
28
+
29
+ for (let index = 0; index < maxLength; index += 1) {
30
+ const leftPart = leftParts[index] ?? 0;
31
+ const rightPart = rightParts[index] ?? 0;
32
+ if (leftPart > rightPart) {
33
+ return 1;
34
+ }
35
+ if (leftPart < rightPart) {
36
+ return -1;
37
+ }
38
+ }
39
+
40
+ return 0;
41
+ }
42
+
43
+ function createProtocolRequestId(): string {
44
+ return `nebula-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
45
+ }
46
+
47
+ function ensureMiniAppProtocolListener(): void {
48
+ if (miniAppProtocolListening) {
49
+ return;
50
+ }
51
+
52
+ const unsubscribe = addNebulaEventListener(
53
+ NEBULA_MINI_APP_MESSAGE_EVENT,
54
+ (event: BridgeMessage) => {
55
+ const message = event.message;
56
+ if (!isProtocolResponse(message)) {
57
+ return;
58
+ }
59
+
60
+ const pending = pendingProtocolRequests.get(message.requestId);
61
+ if (!pending) {
62
+ console.warn('[NebulaMiniAppAPI] missing pending request', {
63
+ kind: message.kind,
64
+ requestId: message.requestId,
65
+ pendingRequestIds: Array.from(pendingProtocolRequests.keys()),
66
+ });
67
+ return;
68
+ }
69
+
70
+ clearTimeout(pending.timeoutId);
71
+ pendingProtocolRequests.delete(message.requestId);
72
+
73
+ if (message.kind === 'capabilities') {
74
+ pending.resolve(message.data);
75
+ return;
76
+ }
77
+
78
+ if (message.kind === 'apiDescriptions') {
79
+ pending.resolve(message.data);
80
+ return;
81
+ }
82
+
83
+ if (message.kind === 'invokeResult') {
84
+ pending.resolve(message.result);
85
+ }
86
+ },
87
+ );
88
+
89
+ if (!unsubscribe) {
90
+ return;
91
+ }
92
+
93
+ miniAppProtocolListening = true;
94
+ }
95
+
96
+ export class Miniapp {
97
+ static bootstrap(appId?: string | null): void {
98
+ if (typeof appId === 'string' && appId.length > 0) {
99
+ currentMiniAppId = appId;
100
+ (globalThis as any).__nebulaCurrentAppId = appId;
101
+ }
102
+ }
103
+
104
+ static async navigateTo(url: string): Promise<NavigationResult> {
105
+ return getNebulaNativeModule().navigateTo(currentMiniAppId || '', url);
106
+ }
107
+
108
+ static async redirectTo(url: string): Promise<NavigationResult> {
109
+ return getNebulaNativeModule().redirectTo(currentMiniAppId || '', url);
110
+ }
111
+
112
+ static async reLaunch(url: string): Promise<NavigationResult> {
113
+ return getNebulaNativeModule().reLaunch(currentMiniAppId || '', url);
114
+ }
115
+
116
+ static async navigateBack(delta = 1): Promise<NavigationResult> {
117
+ return getNebulaNativeModule().navigateBack(currentMiniAppId || '', delta);
118
+ }
119
+
120
+ static async setPageStyle(style: NebulaPageStyle): Promise<NavigationResult> {
121
+ return getNebulaNativeModule().setPageStyle(currentMiniAppId || '', style);
122
+ }
123
+
124
+ static async setNavigationBarTitle(title: string): Promise<NavigationResult> {
125
+ return this.setPageStyle({ navigationBarTitleText: title });
126
+ }
127
+
128
+ static async setNavigationBarColor(options: {
129
+ backgroundColor?: string;
130
+ frontColor?: string;
131
+ }): Promise<NavigationResult> {
132
+ return this.setPageStyle({
133
+ navigationBarBackgroundColor: options.backgroundColor,
134
+ navigationBarTextColor: options.frontColor,
135
+ });
136
+ }
137
+
138
+ static getDeviceInfo(): unknown {
139
+ return getNebulaNativeModule().getDeviceInfo();
140
+ }
141
+
142
+ static getAppId(): string | null {
143
+ return currentMiniAppId;
144
+ }
145
+
146
+ static getSandboxPath(): string {
147
+ return `/Documents/MiniApps/${currentMiniAppId || ''}`;
148
+ }
149
+
150
+ static async showToast(title: string): Promise<NavigationResult> {
151
+ return getNebulaNativeModule().showToast(title);
152
+ }
153
+
154
+ static async postMessageToHost(
155
+ message: Record<string, unknown>,
156
+ ): Promise<NavigationResult> {
157
+ return getNebulaNativeModule().postMessageToHost(
158
+ currentMiniAppId || '',
159
+ message,
160
+ );
161
+ }
162
+
163
+ static async bringHostToFront(): Promise<HostVisibilityResult> {
164
+ return getNebulaNativeModule().bringHostToFront();
165
+ }
166
+
167
+ static async restoreMiniApp(
168
+ token?: string | null,
169
+ ): Promise<HostVisibilityResult> {
170
+ return getNebulaNativeModule().restoreMiniApp(token);
171
+ }
172
+
173
+ static async presentHostModal(
174
+ moduleName: string,
175
+ props: Record<string, unknown> = {},
176
+ ): Promise<NavigationResult> {
177
+ return getNebulaNativeModule().presentHostModal(moduleName, props);
178
+ }
179
+
180
+ static async dismissHostModal(): Promise<NavigationResult> {
181
+ return getNebulaNativeModule().dismissHostModal();
182
+ }
183
+
184
+ static async invokeHostApi<TData = unknown>(
185
+ apiName: string,
186
+ payload: Record<string, unknown> = {},
187
+ version = '1.0.0',
188
+ timeoutMs = 15000,
189
+ ): Promise<NebulaApiInvokeResult<TData>> {
190
+ ensureMiniAppProtocolListener();
191
+
192
+ return new Promise<NebulaApiInvokeResult<TData>>((resolve, reject) => {
193
+ const requestId = createProtocolRequestId();
194
+ const timeoutId =
195
+ timeoutMs > 0
196
+ ? setTimeout(() => {
197
+ console.warn('[NebulaMiniAppAPI] invokeHostApi timeout', {
198
+ apiName,
199
+ requestId,
200
+ pendingRequestIds: Array.from(pendingProtocolRequests.keys()),
201
+ });
202
+ pendingProtocolRequests.delete(requestId);
203
+ reject(new Error(`Timed out waiting for host API: ${apiName}`));
204
+ }, timeoutMs)
205
+ : setTimeout(() => {}, 2147483647);
206
+
207
+ pendingProtocolRequests.set(requestId, {
208
+ resolve,
209
+ reject,
210
+ timeoutId,
211
+ });
212
+
213
+ this.postMessageToHost({
214
+ __nebulaProtocol: 'api.v1',
215
+ kind: 'invoke',
216
+ requestId,
217
+ api: apiName,
218
+ version,
219
+ payload,
220
+ }).catch(error => {
221
+ clearTimeout(timeoutId);
222
+ pendingProtocolRequests.delete(requestId);
223
+ console.error(
224
+ '[NebulaMiniAppAPI] invokeHostApi postMessageToHost failed',
225
+ {
226
+ apiName,
227
+ requestId,
228
+ error,
229
+ },
230
+ );
231
+ reject(error);
232
+ });
233
+ });
234
+ }
235
+
236
+ static async getCapabilities(): Promise<{
237
+ bridgeVersion: string;
238
+ capabilities: NebulaHostCapabilityMap;
239
+ }> {
240
+ ensureMiniAppProtocolListener();
241
+
242
+ return new Promise((resolve, reject) => {
243
+ const requestId = createProtocolRequestId();
244
+ const timeoutId = setTimeout(() => {
245
+ pendingProtocolRequests.delete(requestId);
246
+ reject(new Error('Timed out waiting for host capabilities'));
247
+ }, 15000);
248
+
249
+ pendingProtocolRequests.set(requestId, {
250
+ resolve,
251
+ reject,
252
+ timeoutId,
253
+ });
254
+
255
+ this.postMessageToHost({
256
+ __nebulaProtocol: 'api.v1',
257
+ kind: 'getCapabilities',
258
+ requestId,
259
+ }).catch(error => {
260
+ clearTimeout(timeoutId);
261
+ pendingProtocolRequests.delete(requestId);
262
+ reject(error);
263
+ });
264
+ });
265
+ }
266
+
267
+ static async isSupported(
268
+ apiName: string,
269
+ minimumVersion?: string,
270
+ ): Promise<boolean> {
271
+ const { capabilities } = await this.getCapabilities();
272
+ const capability = capabilities[apiName];
273
+
274
+ if (!capability?.supported) {
275
+ return false;
276
+ }
277
+
278
+ if (!minimumVersion) {
279
+ return true;
280
+ }
281
+
282
+ return compareCapabilityVersions(capability.version, minimumVersion) >= 0;
283
+ }
284
+
285
+ static async getHostApiDescriptions(
286
+ timeoutMs = 15000,
287
+ ): Promise<NebulaHostApiDescriptionMap> {
288
+ ensureMiniAppProtocolListener();
289
+
290
+ return new Promise((resolve, reject) => {
291
+ const requestId = createProtocolRequestId();
292
+ const timeoutId = setTimeout(() => {
293
+ pendingProtocolRequests.delete(requestId);
294
+ reject(new Error('Timed out waiting for host API descriptions'));
295
+ }, timeoutMs);
296
+
297
+ pendingProtocolRequests.set(requestId, {
298
+ resolve,
299
+ reject,
300
+ timeoutId,
301
+ });
302
+
303
+ this.postMessageToHost({
304
+ __nebulaProtocol: 'api.v1',
305
+ kind: 'getApiDescriptions',
306
+ requestId,
307
+ }).catch(error => {
308
+ clearTimeout(timeoutId);
309
+ pendingProtocolRequests.delete(requestId);
310
+ reject(error);
311
+ });
312
+ });
313
+ }
314
+
315
+ static onHostMessage(listener: (event: BridgeMessage) => void): () => void {
316
+ const unsubscribe = addNebulaEventListener(
317
+ NEBULA_MINI_APP_MESSAGE_EVENT,
318
+ listener,
319
+ );
320
+ if (!unsubscribe) {
321
+ console.warn(
322
+ '[Nebula] Native event emitter unavailable for host messages',
323
+ );
324
+ return () => {};
325
+ }
326
+ return unsubscribe;
327
+ }
328
+
329
+ static onPageLifecycle(
330
+ listener: (event: PageLifecycleEvent) => void,
331
+ ): () => void {
332
+ const unsubscribe = addNebulaEventListener(
333
+ NEBULA_PAGE_LIFECYCLE_EVENT,
334
+ listener,
335
+ );
336
+ if (!unsubscribe) {
337
+ console.warn(
338
+ '[Nebula] Native event emitter unavailable for page lifecycle events',
339
+ );
340
+ return () => {};
341
+ }
342
+ return unsubscribe;
343
+ }
344
+ }