@nebula-rn/host-apis 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,23 @@
1
+ import { createHostApiFailure, createHostApiSuccess } from '@nebula-rn/sdk';
2
+ import type { NebulaApiInvokeResult } from '@nebula-rn/sdk';
3
+ export type HostApiEventMessage<TPayload = unknown> = {
4
+ __nebulaApiEvent: 'v1';
5
+ apiName: string;
6
+ channel: string;
7
+ subscriptionId?: string;
8
+ taskId?: string;
9
+ payload?: TPayload;
10
+ };
11
+ export declare function createHostBridgeId(prefix: string): string;
12
+ export declare function emitHostApiEvent<TPayload>(appId: string, message: HostApiEventMessage<TPayload>): Promise<void>;
13
+ export declare function createSubscriptionStartResult(subscriptionId: string): NebulaApiInvokeResult<{
14
+ subscriptionId: string;
15
+ }>;
16
+ export declare function createTaskStartResult(taskId: string): NebulaApiInvokeResult<{
17
+ taskId: string;
18
+ }>;
19
+ export declare function emitSubscriptionEvent<TPayload>(appId: string, apiName: string, subscriptionId: string, payload: TPayload): Promise<void>;
20
+ export declare function emitTaskProgress<TPayload>(appId: string, apiName: string, taskId: string, payload: TPayload): Promise<void>;
21
+ export declare function emitTaskHeaders<TPayload>(appId: string, apiName: string, taskId: string, payload: TPayload): Promise<void>;
22
+ export declare function emitTaskResult<TPayload>(appId: string, apiName: string, taskId: string, result: NebulaApiInvokeResult<TPayload>): Promise<void>;
23
+ export { createHostApiFailure, createHostApiSuccess };
@@ -0,0 +1,54 @@
1
+ import { NebulaAPI, createHostApiFailure, createHostApiSuccess, } from '@nebula-rn/sdk';
2
+ export function createHostBridgeId(prefix) {
3
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
4
+ }
5
+ export async function emitHostApiEvent(appId, message) {
6
+ await NebulaAPI.postMessageToMiniApp(appId, message);
7
+ }
8
+ export function createSubscriptionStartResult(subscriptionId) {
9
+ return createHostApiSuccess({
10
+ subscriptionId,
11
+ });
12
+ }
13
+ export function createTaskStartResult(taskId) {
14
+ return createHostApiSuccess({
15
+ taskId,
16
+ });
17
+ }
18
+ export function emitSubscriptionEvent(appId, apiName, subscriptionId, payload) {
19
+ return emitHostApiEvent(appId, {
20
+ __nebulaApiEvent: 'v1',
21
+ apiName,
22
+ channel: 'subscription',
23
+ subscriptionId,
24
+ payload,
25
+ });
26
+ }
27
+ export function emitTaskProgress(appId, apiName, taskId, payload) {
28
+ return emitHostApiEvent(appId, {
29
+ __nebulaApiEvent: 'v1',
30
+ apiName,
31
+ channel: 'task.progress',
32
+ taskId,
33
+ payload,
34
+ });
35
+ }
36
+ export function emitTaskHeaders(appId, apiName, taskId, payload) {
37
+ return emitHostApiEvent(appId, {
38
+ __nebulaApiEvent: 'v1',
39
+ apiName,
40
+ channel: 'task.headers',
41
+ taskId,
42
+ payload,
43
+ });
44
+ }
45
+ export function emitTaskResult(appId, apiName, taskId, result) {
46
+ return emitHostApiEvent(appId, {
47
+ __nebulaApiEvent: 'v1',
48
+ apiName,
49
+ channel: 'task.result',
50
+ taskId,
51
+ payload: result,
52
+ });
53
+ }
54
+ export { createHostApiFailure, createHostApiSuccess };
@@ -0,0 +1,7 @@
1
+ import { previewImageHostApi } from './PreviewImageHostBridge';
2
+ import { scanCodeHostApi } from './ScanCodeHostBridge';
3
+ import { coreHostApis, saveMediaHost } from './coreHostApis';
4
+ import { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost } from './taskHosts';
5
+ export { previewImageHostApi, scanCodeHostApi, coreHostApis, saveMediaHost };
6
+ export { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, };
7
+ export declare const defaultHostApis: import("@nebula-rn/sdk").NebulaHostFeature[];
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { previewImageHostApi } from './PreviewImageHostBridge';
2
+ import { scanCodeHostApi } from './ScanCodeHostBridge';
3
+ import { coreHostApis, saveMediaHost } from './coreHostApis';
4
+ import { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, } from './taskHosts';
5
+ export { previewImageHostApi, scanCodeHostApi, coreHostApis, saveMediaHost };
6
+ export { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, };
7
+ export const defaultHostApis = [
8
+ scanCodeHostApi,
9
+ previewImageHostApi,
10
+ ...coreHostApis,
11
+ ];
@@ -0,0 +1,8 @@
1
+ export type PreviewImageRequest = {
2
+ urls: string[];
3
+ current?: string;
4
+ showMenu?: boolean;
5
+ saveMediaText?: string;
6
+ cancelText?: string;
7
+ };
8
+ export declare const previewImageChannel: import("@nebula-rn/sdk").HostModalChannel<PreviewImageRequest>;
@@ -0,0 +1,2 @@
1
+ import { createHostModalChannel } from '@nebula-rn/sdk';
2
+ export const previewImageChannel = createHostModalChannel();
@@ -0,0 +1,5 @@
1
+ export type ScanCodeRequest = {
2
+ onlyFromCamera?: boolean;
3
+ scanTypes: string[];
4
+ };
5
+ export declare const scanCodeChannel: import("@nebula-rn/sdk").HostModalChannel<ScanCodeRequest>;
@@ -0,0 +1,2 @@
1
+ import { createHostModalChannel } from '@nebula-rn/sdk';
2
+ export const scanCodeChannel = createHostModalChannel();
@@ -0,0 +1,27 @@
1
+ type DownloadFileHostOption = {
2
+ url: string;
3
+ header?: Record<string, string>;
4
+ timeout?: number;
5
+ filePath?: string;
6
+ };
7
+ type DownloadFileHostResult = {
8
+ tempFilePath: string;
9
+ statusCode: number;
10
+ };
11
+ type UploadFileHostOption = {
12
+ url: string;
13
+ filePath: string;
14
+ name: string;
15
+ header?: Record<string, string>;
16
+ formData?: Record<string, string>;
17
+ timeout?: number;
18
+ };
19
+ type UploadFileHostResult = {
20
+ data: string;
21
+ statusCode: number;
22
+ };
23
+ export declare const downloadFileHost: (options: DownloadFileHostOption) => Promise<DownloadFileHostResult>;
24
+ export declare const uploadFileHost: (options: UploadFileHostOption) => Promise<UploadFileHostResult>;
25
+ export declare const downloadFileHostApi: import("@nebula-rn/sdk").NebulaHostFeature[];
26
+ export declare const uploadFileHostApi: import("@nebula-rn/sdk").NebulaHostFeature[];
27
+ export {};
@@ -0,0 +1,204 @@
1
+ import * as RNFS from '@dr.pogodin/react-native-fs';
2
+ import { createHostApiFeature } from '@nebula-rn/sdk';
3
+ import { createTaskStartResult, emitTaskHeaders, emitTaskProgress, emitTaskResult, } from './hostApiBridge';
4
+ const activeDownloads = new Map();
5
+ const activeUploads = new Map();
6
+ const TASK_API_DESCRIPTIONS = {
7
+ 'downloadFile.start': {
8
+ summary: 'Start a host-managed file download task with progress events.',
9
+ tags: ['file', 'task', 'network'],
10
+ },
11
+ 'downloadFile.abort': {
12
+ summary: 'Abort an in-flight host download task by task id.',
13
+ tags: ['file', 'task', 'network'],
14
+ },
15
+ 'uploadFile.start': {
16
+ summary: 'Start a host-managed file upload task with progress events.',
17
+ tags: ['file', 'task', 'network'],
18
+ },
19
+ 'uploadFile.abort': {
20
+ summary: 'Abort an in-flight host upload task by task id.',
21
+ tags: ['file', 'task', 'network'],
22
+ },
23
+ };
24
+ export const downloadFileHost = async (options) => {
25
+ const { url, header = {}, timeout = 60000, filePath } = options;
26
+ const destPath = filePath || `${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
27
+ const result = await RNFS.downloadFile({
28
+ fromUrl: url,
29
+ toFile: destPath,
30
+ headers: header,
31
+ connectionTimeout: timeout,
32
+ readTimeout: timeout,
33
+ }).promise;
34
+ return {
35
+ tempFilePath: destPath,
36
+ statusCode: result.statusCode,
37
+ };
38
+ };
39
+ export const uploadFileHost = async (options) => {
40
+ const nativeTask = RNFS.uploadFiles({
41
+ toUrl: options.url,
42
+ files: [
43
+ {
44
+ name: options.name,
45
+ filename: options.filePath.split('/').pop() || 'file',
46
+ filepath: options.filePath,
47
+ },
48
+ ],
49
+ headers: options.header ?? {},
50
+ fields: options.formData ?? {},
51
+ method: 'POST',
52
+ });
53
+ const result = await nativeTask.promise;
54
+ return {
55
+ data: result.body,
56
+ statusCode: result.statusCode,
57
+ };
58
+ };
59
+ export const downloadFileHostApi = [
60
+ createHostApiFeature({
61
+ apiName: 'downloadFile.start',
62
+ description: TASK_API_DESCRIPTIONS['downloadFile.start'],
63
+ async handle(payload, context) {
64
+ const taskId = `download-${Date.now()}-${Math.random()
65
+ .toString(36)
66
+ .slice(2, 10)}`;
67
+ const options = payload;
68
+ const destPath = options.filePath ||
69
+ `${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
70
+ const nativeTask = RNFS.downloadFile({
71
+ fromUrl: options.url,
72
+ toFile: destPath,
73
+ headers: options.header ?? {},
74
+ connectionTimeout: options.timeout ?? 60000,
75
+ readTimeout: options.timeout ?? 60000,
76
+ begin: res => {
77
+ emitTaskHeaders(context.appId, 'downloadFile', taskId, {
78
+ header: res.headers,
79
+ }).catch(() => { });
80
+ },
81
+ progress: res => {
82
+ emitTaskProgress(context.appId, 'downloadFile', taskId, {
83
+ progress: Math.floor((res.bytesWritten / res.contentLength) * 100),
84
+ totalBytesWritten: res.bytesWritten,
85
+ totalBytesExpectedToWrite: res.contentLength,
86
+ }).catch(() => { });
87
+ },
88
+ });
89
+ activeDownloads.set(taskId, nativeTask.jobId);
90
+ nativeTask.promise
91
+ .then(res => emitTaskResult(context.appId, 'downloadFile', taskId, {
92
+ ok: true,
93
+ data: {
94
+ tempFilePath: destPath,
95
+ statusCode: res.statusCode,
96
+ },
97
+ }))
98
+ .catch(error => emitTaskResult(context.appId, 'downloadFile', taskId, {
99
+ ok: false,
100
+ error: {
101
+ code: 'DOWNLOAD_FAILED',
102
+ message: error instanceof Error
103
+ ? error.message
104
+ : 'downloadFile:fail host download failed',
105
+ },
106
+ }))
107
+ .finally(() => {
108
+ activeDownloads.delete(taskId);
109
+ });
110
+ return createTaskStartResult(taskId);
111
+ },
112
+ }),
113
+ createHostApiFeature({
114
+ apiName: 'downloadFile.abort',
115
+ description: TASK_API_DESCRIPTIONS['downloadFile.abort'],
116
+ async handle(payload) {
117
+ const taskId = String(payload.taskId ?? '');
118
+ const jobId = activeDownloads.get(taskId);
119
+ if (jobId) {
120
+ RNFS.stopDownload(jobId);
121
+ activeDownloads.delete(taskId);
122
+ }
123
+ return {
124
+ ok: true,
125
+ data: null,
126
+ };
127
+ },
128
+ }),
129
+ ];
130
+ export const uploadFileHostApi = [
131
+ createHostApiFeature({
132
+ apiName: 'uploadFile.start',
133
+ description: TASK_API_DESCRIPTIONS['uploadFile.start'],
134
+ async handle(payload, context) {
135
+ const taskId = `upload-${Date.now()}-${Math.random()
136
+ .toString(36)
137
+ .slice(2, 10)}`;
138
+ const options = payload;
139
+ const nativeTask = RNFS.uploadFiles({
140
+ toUrl: options.url,
141
+ files: [
142
+ {
143
+ name: options.name,
144
+ filename: options.filePath.split('/').pop() || 'file',
145
+ filepath: options.filePath,
146
+ },
147
+ ],
148
+ headers: options.header ?? {},
149
+ fields: options.formData ?? {},
150
+ method: 'POST',
151
+ progress: res => {
152
+ emitTaskProgress(context.appId, 'uploadFile', taskId, {
153
+ progress: Math.floor((res.totalBytesSent / res.totalBytesExpectedToSend) * 100),
154
+ totalBytesSent: res.totalBytesSent,
155
+ totalBytesExpectedToSend: res.totalBytesExpectedToSend,
156
+ }).catch(() => { });
157
+ },
158
+ });
159
+ activeUploads.set(taskId, nativeTask.jobId);
160
+ nativeTask.promise
161
+ .then(res => Promise.all([
162
+ emitTaskHeaders(context.appId, 'uploadFile', taskId, {
163
+ header: res.headers,
164
+ }),
165
+ emitTaskResult(context.appId, 'uploadFile', taskId, {
166
+ ok: true,
167
+ data: {
168
+ data: res.body,
169
+ statusCode: res.statusCode,
170
+ },
171
+ }),
172
+ ]))
173
+ .catch(error => emitTaskResult(context.appId, 'uploadFile', taskId, {
174
+ ok: false,
175
+ error: {
176
+ code: 'UPLOAD_FAILED',
177
+ message: error instanceof Error
178
+ ? error.message
179
+ : 'uploadFile:fail host upload failed',
180
+ },
181
+ }))
182
+ .finally(() => {
183
+ activeUploads.delete(taskId);
184
+ });
185
+ return createTaskStartResult(taskId);
186
+ },
187
+ }),
188
+ createHostApiFeature({
189
+ apiName: 'uploadFile.abort',
190
+ description: TASK_API_DESCRIPTIONS['uploadFile.abort'],
191
+ async handle(payload) {
192
+ const taskId = String(payload.taskId ?? '');
193
+ const jobId = activeUploads.get(taskId);
194
+ if (jobId) {
195
+ RNFS.stopUpload(jobId);
196
+ activeUploads.delete(taskId);
197
+ }
198
+ return {
199
+ ok: true,
200
+ data: null,
201
+ };
202
+ },
203
+ }),
204
+ ];
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nebula-rn/host-apis",
3
+ "version": "0.0.1",
4
+ "description": "Official Nebula host API feature bundle",
5
+ "author": "Hector Zhuang",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Hector-Zhuang/nebula.git",
10
+ "directory": "packages/host-apis"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Hector-Zhuang/nebula/issues"
14
+ },
15
+ "homepage": "https://github.com/Hector-Zhuang/nebula/tree/main/packages/host-apis#readme",
16
+ "keywords": ["nebula", "superapp", "miniapp", "react-native"],
17
+ "main": "./dist/index.js",
18
+ "react-native": "./src/index.ts",
19
+ "source": "./src/index.ts",
20
+ "types": "./dist/index.d.ts",
21
+ "files": ["dist", "src", "README.md"],
22
+ "publishConfig": { "access": "public" },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "prepack": "npm run build",
26
+ "lint": "eslint src --ext .ts,.tsx"
27
+ },
28
+ "dependencies": {
29
+ "@bam.tech/react-native-image-resizer": "^3.0.11",
30
+ "@dr.pogodin/react-native-fs": "^2.37.0",
31
+ "@nebula-rn/sdk": "^0.0.1",
32
+ "@react-native-camera-roll/camera-roll": "^7.10.2",
33
+ "@react-native-clipboard/clipboard": "^1.16.3",
34
+ "@react-native-community/geolocation": "^3.4.0",
35
+ "@react-native-community/netinfo": "^12.0.1",
36
+ "react": ">=19",
37
+ "react-native": ">=0.83",
38
+ "react-native-device-info": "^15.0.2",
39
+ "react-native-image-picker": "^8.2.1",
40
+ "react-native-image-zoom-viewer": "^3.0.1",
41
+ "react-native-mmkv": "^4.2.0",
42
+ "react-native-safe-area-context": "^5.5.2",
43
+ "react-native-screenshot-aware": "^2.0.0",
44
+ "react-native-sensors": "^7.3.6",
45
+ "react-native-vision-camera": "^4.7.3"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=19",
49
+ "react-native": ">=0.83"
50
+ }
51
+ }
@@ -0,0 +1,119 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { ActivityIndicator, StyleSheet, View } from 'react-native';
3
+ import ImageViewer from 'react-native-image-zoom-viewer';
4
+ import { NebulaAPI } from '@nebula-rn/sdk';
5
+ import { createHostApiFailure, createHostApiSuccess } from './hostApiBridge';
6
+ import { previewImageChannel } from './previewImageRuntime';
7
+ import { saveMediaHost } from './coreHostApis';
8
+ import { downloadFileHost } from './taskHosts';
9
+
10
+ function PreviewImageLoading(): React.ReactElement {
11
+ return (
12
+ <View style={styles.loadingContainer}>
13
+ <ActivityIndicator size="large" color="#999" />
14
+ </View>
15
+ );
16
+ }
17
+
18
+ function renderPreviewImageLoading() {
19
+ return <PreviewImageLoading />;
20
+ }
21
+
22
+ export default function NebulaHostPreviewImageModal(): React.ReactElement | null {
23
+ const [request, setRequest] = useState(previewImageChannel.getCurrent());
24
+ const didCloseRef = useRef(false);
25
+
26
+ useEffect(() => {
27
+ return previewImageChannel.subscribe(nextRequest => {
28
+ didCloseRef.current = false;
29
+ setRequest(nextRequest);
30
+ });
31
+ }, []);
32
+
33
+ const imageUrls = useMemo(
34
+ () => request?.urls.map(url => ({ url })) ?? [],
35
+ [request],
36
+ );
37
+
38
+ if (!request) {
39
+ return null;
40
+ }
41
+
42
+ const initialIndex = request.current
43
+ ? request.urls.indexOf(request.current)
44
+ : 0;
45
+ const index = initialIndex === -1 ? 0 : initialIndex;
46
+
47
+ const close = (
48
+ result:
49
+ | ReturnType<typeof createHostApiSuccess<{ dismissed: boolean }>>
50
+ | ReturnType<typeof createHostApiFailure> = createHostApiSuccess({
51
+ dismissed: true,
52
+ }),
53
+ ) => {
54
+ if (didCloseRef.current) {
55
+ return;
56
+ }
57
+ didCloseRef.current = true;
58
+ NebulaAPI.dismissHostModal()
59
+ .catch(() => {})
60
+ .finally(() => {
61
+ previewImageChannel.settle(result);
62
+ });
63
+ };
64
+
65
+ return (
66
+ <View style={styles.container}>
67
+ <ImageViewer
68
+ imageUrls={imageUrls}
69
+ index={index}
70
+ onCancel={() => close()}
71
+ onClick={() => close()}
72
+ onSwipeDown={() => close()}
73
+ enableSwipeDown
74
+ useNativeDriver
75
+ loadingRender={renderPreviewImageLoading}
76
+ onSave={async uri => {
77
+ try {
78
+ const download = await downloadFileHost({
79
+ url: uri,
80
+ });
81
+ await saveMediaHost({
82
+ url: download.tempFilePath,
83
+ type: 'photo',
84
+ });
85
+ } catch (error) {
86
+ close(
87
+ createHostApiFailure(
88
+ 'PREVIEW_IMAGE_SAVE_FAILED',
89
+ error instanceof Error
90
+ ? error.message
91
+ : 'previewImage:fail unable to save image',
92
+ ),
93
+ );
94
+ }
95
+ }}
96
+ menuContext={
97
+ request.showMenu === false
98
+ ? undefined
99
+ : {
100
+ saveToLocal: request.saveMediaText ?? 'Save Image',
101
+ cancel: request.cancelText ?? 'Cancel',
102
+ }
103
+ }
104
+ />
105
+ </View>
106
+ );
107
+ }
108
+
109
+ const styles = StyleSheet.create({
110
+ container: {
111
+ flex: 1,
112
+ backgroundColor: '#000',
113
+ },
114
+ loadingContainer: {
115
+ flex: 1,
116
+ justifyContent: 'center',
117
+ alignItems: 'center',
118
+ },
119
+ });