react-native-custom-ota 0.0.0-stage → 1.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 (48) hide show
  1. package/README.md +214 -2
  2. package/RNCustomOTA.podspec +20 -0
  3. package/android/build.gradle +21 -0
  4. package/android/src/main/java/com/customota/CustomOTABundleResolver.kt +85 -0
  5. package/android/src/main/java/com/customota/OTAUpdaterModule.kt +152 -0
  6. package/android/src/main/java/com/customota/OTAUpdaterPackage.kt +22 -0
  7. package/bin/custom-ota.js +217 -0
  8. package/ios/CustomOTABundleResolver.swift +79 -0
  9. package/ios/OTAUpdater.m +19 -0
  10. package/ios/OTAUpdater.swift +105 -0
  11. package/lib/commonjs/hoc.js +64 -0
  12. package/lib/commonjs/index.js +32 -0
  13. package/lib/commonjs/manager.js +262 -0
  14. package/lib/commonjs/native.js +9 -0
  15. package/lib/commonjs/storage.js +48 -0
  16. package/lib/commonjs/types.js +23 -0
  17. package/lib/module/hoc.js +28 -0
  18. package/lib/module/index.js +16 -0
  19. package/lib/module/manager.js +255 -0
  20. package/lib/module/native.js +6 -0
  21. package/lib/module/storage.js +39 -0
  22. package/lib/module/types.js +20 -0
  23. package/lib/typescript/hoc.d.ts +3 -0
  24. package/lib/typescript/hoc.js +64 -0
  25. package/lib/typescript/index.d.ts +16 -0
  26. package/lib/typescript/index.js +32 -0
  27. package/lib/typescript/manager.d.ts +35 -0
  28. package/lib/typescript/manager.js +262 -0
  29. package/lib/typescript/native.d.ts +8 -0
  30. package/lib/typescript/native.js +9 -0
  31. package/lib/typescript/src/hoc.d.ts +3 -0
  32. package/lib/typescript/src/index.d.ts +15 -0
  33. package/lib/typescript/src/manager.d.ts +33 -0
  34. package/lib/typescript/src/native.d.ts +8 -0
  35. package/lib/typescript/src/storage.d.ts +8 -0
  36. package/lib/typescript/src/types.d.ts +54 -0
  37. package/lib/typescript/storage.d.ts +8 -0
  38. package/lib/typescript/storage.js +48 -0
  39. package/lib/typescript/types.d.ts +60 -0
  40. package/lib/typescript/types.js +23 -0
  41. package/package.json +48 -3
  42. package/react-native.config.js +11 -0
  43. package/src/hoc.tsx +34 -0
  44. package/src/index.ts +19 -0
  45. package/src/manager.ts +328 -0
  46. package/src/native.ts +17 -0
  47. package/src/storage.ts +45 -0
  48. package/src/types.ts +66 -0
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TEMP_DIR = exports.BACKUP_DIR = exports.CURRENT_DIR = exports.OTA_ROOT = void 0;
7
+ exports.ensureOTADirs = ensureOTADirs;
8
+ exports.readState = readState;
9
+ exports.writeState = writeState;
10
+ const react_native_fs_1 = __importDefault(require("react-native-fs"));
11
+ exports.OTA_ROOT = `${react_native_fs_1.default.DocumentDirectoryPath}/ota`;
12
+ exports.CURRENT_DIR = `${exports.OTA_ROOT}/current`;
13
+ exports.BACKUP_DIR = `${exports.OTA_ROOT}/backup`;
14
+ exports.TEMP_DIR = `${exports.OTA_ROOT}/temp`;
15
+ const STATE_FILE = `${exports.OTA_ROOT}/state.json`;
16
+ async function ensureOTADirs() {
17
+ await react_native_fs_1.default.mkdir(exports.OTA_ROOT).catch(() => { });
18
+ await react_native_fs_1.default.mkdir(exports.TEMP_DIR).catch(() => { });
19
+ }
20
+ async function readState() {
21
+ await ensureOTADirs();
22
+ if (!(await react_native_fs_1.default.exists(STATE_FILE))) {
23
+ return {
24
+ currentVersion: "0.0.0",
25
+ previousVersion: null,
26
+ pending: false,
27
+ ready: true,
28
+ failedVersions: []
29
+ };
30
+ }
31
+ try {
32
+ const content = await react_native_fs_1.default.readFile(STATE_FILE, "utf8");
33
+ return JSON.parse(content);
34
+ }
35
+ catch {
36
+ return {
37
+ currentVersion: "0.0.0",
38
+ previousVersion: null,
39
+ pending: false,
40
+ ready: true,
41
+ failedVersions: []
42
+ };
43
+ }
44
+ }
45
+ async function writeState(state) {
46
+ await ensureOTADirs();
47
+ await react_native_fs_1.default.writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
48
+ }
@@ -0,0 +1,60 @@
1
+ export declare enum SyncStatus {
2
+ UP_TO_DATE = 0,
3
+ UPDATE_AVAILABLE = 1,
4
+ DOWNLOADING_PACKAGE = 2,
5
+ INSTALLING_UPDATE = 3,
6
+ UPDATE_INSTALLED = 4,
7
+ ERROR = -1
8
+ }
9
+ export declare enum CheckFrequency {
10
+ ON_APP_START = "ON_APP_START",
11
+ ON_APP_RESUME = "ON_APP_RESUME",
12
+ MANUAL = "MANUAL"
13
+ }
14
+ export declare enum InstallMode {
15
+ IMMEDIATE = "IMMEDIATE",
16
+ ON_NEXT_RESTART = "ON_NEXT_RESTART"
17
+ }
18
+ export interface UpdateDialogOptions {
19
+ title?: string;
20
+ optionalUpdateMessage?: string;
21
+ mandatoryUpdateMessage?: string;
22
+ optionalInstallButtonLabel?: string;
23
+ optionalIgnoreButtonLabel?: string;
24
+ }
25
+ export interface SyncOptions {
26
+ serverUrl: string;
27
+ appId?: string;
28
+ apiKey?: string;
29
+ nativeVersion?: string;
30
+ channel?: string;
31
+ installMode?: InstallMode;
32
+ mandatoryInstallMode?: InstallMode;
33
+ updateDialog?: boolean | UpdateDialogOptions;
34
+ checkFrequency?: CheckFrequency;
35
+ }
36
+ export interface RemotePackage {
37
+ updateAvailable: boolean;
38
+ appId?: string;
39
+ platform?: string;
40
+ otaVersion?: string;
41
+ version?: string;
42
+ bundle?: string;
43
+ sha256?: string;
44
+ channel?: string;
45
+ minNativeVersion?: string;
46
+ mandatory?: boolean;
47
+ isMandatory?: boolean;
48
+ releaseNotes?: string;
49
+ downloadUrl?: string;
50
+ size?: number;
51
+ reason?: string;
52
+ }
53
+ export interface LocalPackage {
54
+ currentVersion: string;
55
+ previousVersion: string | null;
56
+ pending: boolean;
57
+ trial?: boolean;
58
+ ready: boolean;
59
+ failedVersions: string[];
60
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InstallMode = exports.CheckFrequency = exports.SyncStatus = void 0;
4
+ var SyncStatus;
5
+ (function (SyncStatus) {
6
+ SyncStatus[SyncStatus["UP_TO_DATE"] = 0] = "UP_TO_DATE";
7
+ SyncStatus[SyncStatus["UPDATE_AVAILABLE"] = 1] = "UPDATE_AVAILABLE";
8
+ SyncStatus[SyncStatus["DOWNLOADING_PACKAGE"] = 2] = "DOWNLOADING_PACKAGE";
9
+ SyncStatus[SyncStatus["INSTALLING_UPDATE"] = 3] = "INSTALLING_UPDATE";
10
+ SyncStatus[SyncStatus["UPDATE_INSTALLED"] = 4] = "UPDATE_INSTALLED";
11
+ SyncStatus[SyncStatus["ERROR"] = -1] = "ERROR";
12
+ })(SyncStatus || (exports.SyncStatus = SyncStatus = {}));
13
+ var CheckFrequency;
14
+ (function (CheckFrequency) {
15
+ CheckFrequency["ON_APP_START"] = "ON_APP_START";
16
+ CheckFrequency["ON_APP_RESUME"] = "ON_APP_RESUME";
17
+ CheckFrequency["MANUAL"] = "MANUAL";
18
+ })(CheckFrequency || (exports.CheckFrequency = CheckFrequency = {}));
19
+ var InstallMode;
20
+ (function (InstallMode) {
21
+ InstallMode["IMMEDIATE"] = "IMMEDIATE";
22
+ InstallMode["ON_NEXT_RESTART"] = "ON_NEXT_RESTART";
23
+ })(InstallMode || (exports.InstallMode = InstallMode = {}));
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "react-native-custom-ota",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.1",
4
+ "description": "Self-hosted OTA (Over-The-Air) update library for React Native with automatic rollback, similar to react-native-code-push",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "bin": {
11
+ "custom-ota": "bin/custom-ota.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "lib",
16
+ "android/src",
17
+ "android/build.gradle",
18
+ "ios",
19
+ "bin",
20
+ "react-native.config.js",
21
+ "RNCustomOTA.podspec",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "npx tsc --declaration --outDir lib/typescript && npx tsc --module commonjs --outDir lib/commonjs && npx tsc --module esnext --outDir lib/module",
26
+ "prepack": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "react-native",
30
+ "ota",
31
+ "code-push",
32
+ "hot-update",
33
+ "live-update",
34
+ "self-hosted",
35
+ "rollback"
36
+ ],
37
+ "author": "Custom OTA Team",
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "react": "*",
41
+ "react-native": "*"
42
+ },
43
+ "dependencies": {
44
+ "react-native-fs": "^2.20.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/react": "^18.2.0 || ^19.0.0",
48
+ "@types/react-native": "^0.72.0",
49
+ "typescript": "^5.0.0"
50
+ }
6
51
  }
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageImportPath: 'import com.customota.OTAUpdaterPackage;',
6
+ packageInstance: 'new OTAUpdaterPackage()'
7
+ },
8
+ ios: {}
9
+ }
10
+ }
11
+ };
package/src/hoc.tsx ADDED
@@ -0,0 +1,34 @@
1
+ import React, { ComponentType, useEffect } from "react";
2
+ import { AppState, AppStateStatus } from "react-native";
3
+ import { CustomOTAManager } from "./manager";
4
+ import { CheckFrequency, SyncOptions } from "./types";
5
+
6
+ export function withCustomOTA(options: SyncOptions) {
7
+ return function <P extends object>(WrappedComponent: ComponentType<P>): ComponentType<P> {
8
+ return function CustomOTAWrapper(props: P) {
9
+ useEffect(() => {
10
+ // Tự động kiểm tra cập nhật khi app khởi động
11
+ if (options.checkFrequency !== CheckFrequency.MANUAL) {
12
+ CustomOTAManager.sync(options);
13
+ }
14
+
15
+ // Nếu bật kiểm tra khi App Resume từ background
16
+ if (options.checkFrequency === CheckFrequency.ON_APP_RESUME) {
17
+ const subscription = AppState.addEventListener(
18
+ "change",
19
+ (nextAppState: AppStateStatus) => {
20
+ if (nextAppState === "active") {
21
+ CustomOTAManager.sync(options);
22
+ }
23
+ }
24
+ );
25
+ return () => {
26
+ subscription.remove();
27
+ };
28
+ }
29
+ }, []);
30
+
31
+ return <WrappedComponent {...props} />;
32
+ };
33
+ };
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { CustomOTAManager } from "./manager";
2
+ import { withCustomOTA } from "./hoc";
3
+
4
+ export * from "./types";
5
+ export * from "./manager";
6
+ export * from "./hoc";
7
+ export * from "./storage";
8
+
9
+ const CustomOTA = {
10
+ sync: CustomOTAManager.sync.bind(CustomOTAManager),
11
+ checkForUpdate: CustomOTAManager.checkForUpdate.bind(CustomOTAManager),
12
+ downloadAndInstall: CustomOTAManager.downloadAndInstall.bind(CustomOTAManager),
13
+ restartApp: CustomOTAManager.restartApp.bind(CustomOTAManager),
14
+ getUpdateMetadata: CustomOTAManager.getUpdateMetadata.bind(CustomOTAManager),
15
+ markReady: CustomOTAManager.markReady.bind(CustomOTAManager),
16
+ wrap: withCustomOTA
17
+ };
18
+
19
+ export default CustomOTA;
package/src/manager.ts ADDED
@@ -0,0 +1,328 @@
1
+ import { Alert, Platform } from "react-native";
2
+ import RNFS from "react-native-fs";
3
+ import { OTAUpdater } from "./native";
4
+ import { BACKUP_DIR, CURRENT_DIR, TEMP_DIR, ensureOTADirs, readState, writeState } from "./storage";
5
+ import {
6
+ InstallMode,
7
+ LocalPackage,
8
+ RemotePackage,
9
+ SyncOptions,
10
+ SyncStatus,
11
+ UpdateDialogOptions
12
+ } from "./types";
13
+
14
+ export class CustomOTAManager {
15
+ private static defaultDialog: UpdateDialogOptions = {
16
+ title: "Có bản cập nhật mới",
17
+ optionalUpdateMessage: "Đã có bản cập nhật ứng dụng mới. Bạn có muốn cài đặt ngay bây giờ?",
18
+ mandatoryUpdateMessage: "Đã có bản cập nhật bắt buộc. Vui lòng cài đặt để tiếp tục sử dụng.",
19
+ optionalInstallButtonLabel: "Cập nhật",
20
+ optionalIgnoreButtonLabel: "Để sau"
21
+ };
22
+
23
+ /**
24
+ * Đánh dấu bản build OTA hiện tại đã khởi động thành công.
25
+ * Xóa bản backup trước đó và tắt cờ pending.
26
+ */
27
+ static async markReady(): Promise<void> {
28
+ try {
29
+ const state = await readState();
30
+ state.pending = false;
31
+ state.ready = true;
32
+ state.trial = false;
33
+ await writeState(state);
34
+ } catch (e) {
35
+ console.warn("[CustomOTA] Error updating state in markReady:", e);
36
+ }
37
+
38
+ if (OTAUpdater?.markReady) {
39
+ try {
40
+ await OTAUpdater.markReady();
41
+ } catch (e) {
42
+ console.warn("[CustomOTA] Error calling native markReady:", e);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Khởi động lại ứng dụng để nạp ngay bản OTA mới.
49
+ */
50
+ static async restartApp(): Promise<void> {
51
+ if (OTAUpdater?.restart) {
52
+ await OTAUpdater.restart();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Lấy thông tin phiên bản OTA hiện hành trên máy.
58
+ */
59
+ static async getUpdateMetadata(): Promise<LocalPackage> {
60
+ return readState();
61
+ }
62
+
63
+ /**
64
+ * Kiểm tra xem server có bản cập nhật mới hay không.
65
+ */
66
+ static async checkForUpdate(options: {
67
+ serverUrl: string;
68
+ appId?: string;
69
+ apiKey?: string;
70
+ nativeVersion?: string;
71
+ channel?: string;
72
+ }): Promise<RemotePackage | null> {
73
+ await ensureOTADirs();
74
+
75
+ const state = await readState();
76
+ const platform = Platform.OS === "ios" ? "ios" : "android";
77
+ const nativeVersion = options.nativeVersion || "1.0.0";
78
+ const channel = options.channel || "production";
79
+
80
+ const params = new URLSearchParams({
81
+ platform,
82
+ nativeVersion,
83
+ otaVersion: state.currentVersion,
84
+ channel
85
+ });
86
+
87
+ if (options.appId) {
88
+ params.append("appId", options.appId);
89
+ }
90
+ if (options.apiKey) {
91
+ params.append("apiKey", options.apiKey);
92
+ }
93
+
94
+ const headers: Record<string, string> = {};
95
+ if (options.apiKey) {
96
+ headers["X-OTA-API-KEY"] = options.apiKey;
97
+ }
98
+
99
+ const response = await fetch(`${options.serverUrl.replace(/\/$/, "")}/ota/check?${params}`, {
100
+ headers
101
+ });
102
+
103
+ if (!response.ok) {
104
+ throw new Error(`OTA Check failed: HTTP ${response.status}`);
105
+ }
106
+
107
+ const rawData = await response.json();
108
+ if (!rawData.updateAvailable) {
109
+ return null;
110
+ }
111
+
112
+ const targetVersion = rawData.otaVersion || rawData.version;
113
+
114
+ // Không báo cập nhật nếu phiên bản đã được cài đặt trên máy
115
+ if (targetVersion && state.currentVersion === targetVersion) {
116
+ return null;
117
+ }
118
+
119
+ // Không báo cập nhật nếu phiên bản này từng bị crash và tự rollback
120
+ if (targetVersion && state.failedVersions?.includes(targetVersion)) {
121
+ return null;
122
+ }
123
+
124
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
125
+ const rawUrl = rawData.downloadUrl || "";
126
+ const fullDownloadUrl = rawUrl.startsWith("http")
127
+ ? rawUrl
128
+ : `${serverUrlClean}${rawUrl.startsWith("/") ? "" : "/"}${rawUrl}`;
129
+
130
+ const data: RemotePackage = {
131
+ ...rawData,
132
+ updateAvailable: true,
133
+ version: targetVersion,
134
+ otaVersion: targetVersion,
135
+ downloadUrl: fullDownloadUrl,
136
+ mandatory: Boolean(rawData.mandatory || rawData.isMandatory),
137
+ isMandatory: Boolean(rawData.mandatory || rawData.isMandatory)
138
+ };
139
+ return data;
140
+ }
141
+
142
+ /**
143
+ * Tải và cài đặt bản cập nhật.
144
+ */
145
+ static async downloadAndInstall(
146
+ update: RemotePackage,
147
+ onProgress?: (progress: number) => void,
148
+ installMode?: InstallMode
149
+ ): Promise<boolean> {
150
+ const version = update.otaVersion || update.version;
151
+ if (!update.downloadUrl || !version || !update.sha256) {
152
+ const missingFields = [
153
+ !update.downloadUrl && "downloadUrl",
154
+ !version && "version",
155
+ !update.sha256 && "sha256"
156
+ ].filter(Boolean).join(", ");
157
+ throw new Error(`Invalid update package metadata: missing ${missingFields}`);
158
+ }
159
+
160
+ const zipPath = `${TEMP_DIR}/update-${version}.zip`;
161
+ await RNFS.unlink(zipPath).catch(() => {});
162
+
163
+ // Download zip file
164
+ const downloadRes = RNFS.downloadFile({
165
+ fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
166
+ toFile: zipPath,
167
+ progressDivider: 1,
168
+ progress: ({ bytesWritten, contentLength }) => {
169
+ if (contentLength > 0 && onProgress) {
170
+ onProgress(bytesWritten / contentLength);
171
+ }
172
+ }
173
+ });
174
+
175
+ const res = await downloadRes.promise;
176
+ if (res.statusCode < 200 || res.statusCode >= 300) {
177
+ throw new Error(`Download failed with status ${res.statusCode}`);
178
+ }
179
+
180
+ // SHA-256 verification
181
+ const actualHash = await RNFS.hash(zipPath, "sha256");
182
+ if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
183
+ await RNFS.unlink(zipPath).catch(() => {});
184
+ throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
185
+ }
186
+
187
+ // Unpack temp directory
188
+ const tempDir = `${TEMP_DIR}/${version}`;
189
+ await RNFS.unlink(tempDir).catch(() => {});
190
+ await RNFS.mkdir(tempDir);
191
+
192
+ await OTAUpdater.unzip(zipPath, tempDir);
193
+
194
+ const bundleName = Platform.OS === "ios" ? "main.jsbundle" : "index.android.bundle";
195
+ const bundleFile = `${tempDir}/${bundleName}`;
196
+
197
+ if (!(await RNFS.exists(bundleFile))) {
198
+ throw new Error(`Bundle missing after unzip: ${bundleName}`);
199
+ }
200
+
201
+ // Atomic move & backup
202
+ await RNFS.unlink(BACKUP_DIR).catch(() => {});
203
+ if (await RNFS.exists(CURRENT_DIR)) {
204
+ await RNFS.moveFile(CURRENT_DIR, BACKUP_DIR);
205
+ }
206
+ await RNFS.moveFile(tempDir, CURRENT_DIR);
207
+
208
+ // Save state
209
+ const oldState = await readState();
210
+ await writeState({
211
+ currentVersion: version,
212
+ previousVersion: oldState.currentVersion !== "0.0.0" ? oldState.currentVersion : null,
213
+ pending: true,
214
+ ready: false,
215
+ trial: false,
216
+ failedVersions: oldState.failedVersions || []
217
+ });
218
+
219
+ await RNFS.unlink(zipPath).catch(() => {});
220
+
221
+ if (installMode === InstallMode.IMMEDIATE) {
222
+ await this.restartApp();
223
+ }
224
+
225
+ return true;
226
+ }
227
+
228
+ /**
229
+ * Phương thức đồng bộ tự động như CodePush.sync()
230
+ */
231
+ static async sync(
232
+ options: SyncOptions,
233
+ syncStatusCallback?: (status: SyncStatus) => void,
234
+ downloadProgressCallback?: (progress: number) => void
235
+ ): Promise<SyncStatus> {
236
+ try {
237
+ // 1. Tự động markReady khi khởi động
238
+ await this.markReady();
239
+
240
+ // 2. Kiểm tra update
241
+ const update = await this.checkForUpdate({
242
+ serverUrl: options.serverUrl,
243
+ appId: options.appId,
244
+ apiKey: options.apiKey,
245
+ nativeVersion: options.nativeVersion,
246
+ channel: options.channel
247
+ });
248
+
249
+ if (!update || !update.updateAvailable) {
250
+ syncStatusCallback?.(SyncStatus.UP_TO_DATE);
251
+ return SyncStatus.UP_TO_DATE;
252
+ }
253
+
254
+ syncStatusCallback?.(SyncStatus.UPDATE_AVAILABLE);
255
+
256
+ const proceedWithInstall = async () => {
257
+ syncStatusCallback?.(SyncStatus.DOWNLOADING_PACKAGE);
258
+
259
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
260
+ const fullDownloadUrl = update.downloadUrl?.startsWith("http")
261
+ ? update.downloadUrl
262
+ : `${serverUrlClean}${update.downloadUrl}`;
263
+
264
+ await this.downloadAndInstall(
265
+ { ...update, downloadUrl: fullDownloadUrl },
266
+ downloadProgressCallback
267
+ );
268
+
269
+ syncStatusCallback?.(SyncStatus.INSTALLING_UPDATE);
270
+ syncStatusCallback?.(SyncStatus.UPDATE_INSTALLED);
271
+
272
+ const effectiveInstallMode = (update.mandatory && options.mandatoryInstallMode)
273
+ ? options.mandatoryInstallMode
274
+ : (options.installMode || InstallMode.ON_NEXT_RESTART);
275
+
276
+ if (effectiveInstallMode === InstallMode.ON_NEXT_RESTART) {
277
+ return SyncStatus.UPDATE_INSTALLED;
278
+ }
279
+
280
+ // Restart immediate
281
+ await this.restartApp();
282
+ return SyncStatus.UPDATE_INSTALLED;
283
+ };
284
+
285
+ // 3. Nếu cấu hình hiển thị Update Dialog
286
+ if (options.updateDialog) {
287
+ const dialogOpts: UpdateDialogOptions =
288
+ typeof options.updateDialog === "object"
289
+ ? { ...this.defaultDialog, ...options.updateDialog }
290
+ : this.defaultDialog;
291
+
292
+ const isMandatory = Boolean(update.mandatory);
293
+ const message = isMandatory
294
+ ? `${dialogOpts.mandatoryUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`
295
+ : `${dialogOpts.optionalUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`;
296
+
297
+ const buttons: any[] = [];
298
+
299
+ if (!isMandatory) {
300
+ buttons.push({
301
+ text: dialogOpts.optionalIgnoreButtonLabel,
302
+ style: "cancel"
303
+ });
304
+ }
305
+
306
+ buttons.push({
307
+ text: dialogOpts.optionalInstallButtonLabel,
308
+ onPress: () => {
309
+ proceedWithInstall();
310
+ }
311
+ });
312
+
313
+ Alert.alert(dialogOpts.title || "Cập nhật ứng dụng", message, buttons, {
314
+ cancelable: !isMandatory
315
+ });
316
+
317
+ return SyncStatus.UPDATE_AVAILABLE;
318
+ }
319
+
320
+ // Tự động tải ngầm không hiện dialog
321
+ return await proceedWithInstall();
322
+ } catch (error) {
323
+ console.error("[react-native-custom-ota] Sync error:", error);
324
+ syncStatusCallback?.(SyncStatus.ERROR);
325
+ return SyncStatus.ERROR;
326
+ }
327
+ }
328
+ }
package/src/native.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { NativeModules } from "react-native";
2
+
3
+ interface OTAUpdaterInterface {
4
+ unzip(zipPath: string, targetDir: string): Promise<void>;
5
+ markReady(): Promise<void>;
6
+ getCurrentVersion(): Promise<string>;
7
+ restart(): Promise<void>;
8
+ }
9
+
10
+ export const OTAUpdater = NativeModules.OTAUpdater as OTAUpdaterInterface;
11
+
12
+ if (!OTAUpdater) {
13
+ console.warn(
14
+ "[react-native-custom-ota] NativeModule.OTAUpdater is null! " +
15
+ "Make sure you have linked the library and rebuilt the native binary."
16
+ );
17
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,45 @@
1
+ import RNFS from "react-native-fs";
2
+ import { LocalPackage } from "./types";
3
+
4
+ export const OTA_ROOT = `${RNFS.DocumentDirectoryPath}/ota`;
5
+ export const CURRENT_DIR = `${OTA_ROOT}/current`;
6
+ export const BACKUP_DIR = `${OTA_ROOT}/backup`;
7
+ export const TEMP_DIR = `${OTA_ROOT}/temp`;
8
+ const STATE_FILE = `${OTA_ROOT}/state.json`;
9
+
10
+ export async function ensureOTADirs(): Promise<void> {
11
+ await RNFS.mkdir(OTA_ROOT).catch(() => {});
12
+ await RNFS.mkdir(TEMP_DIR).catch(() => {});
13
+ }
14
+
15
+ export async function readState(): Promise<LocalPackage> {
16
+ await ensureOTADirs();
17
+
18
+ if (!(await RNFS.exists(STATE_FILE))) {
19
+ return {
20
+ currentVersion: "0.0.0",
21
+ previousVersion: null,
22
+ pending: false,
23
+ ready: true,
24
+ failedVersions: []
25
+ };
26
+ }
27
+
28
+ try {
29
+ const content = await RNFS.readFile(STATE_FILE, "utf8");
30
+ return JSON.parse(content);
31
+ } catch {
32
+ return {
33
+ currentVersion: "0.0.0",
34
+ previousVersion: null,
35
+ pending: false,
36
+ ready: true,
37
+ failedVersions: []
38
+ };
39
+ }
40
+ }
41
+
42
+ export async function writeState(state: LocalPackage): Promise<void> {
43
+ await ensureOTADirs();
44
+ await RNFS.writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf8");
45
+ }