react-native-custom-ota 0.0.0-stage → 1.0.0

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 +49 -4
  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,262 @@
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.CustomOTAManager = void 0;
7
+ const react_native_1 = require("react-native");
8
+ const react_native_fs_1 = __importDefault(require("react-native-fs"));
9
+ const native_1 = require("./native");
10
+ const storage_1 = require("./storage");
11
+ const types_1 = require("./types");
12
+ class CustomOTAManager {
13
+ /**
14
+ * Đánh dấu bản build OTA hiện tại đã khởi động thành công.
15
+ * Xóa bản backup trước đó và tắt cờ pending.
16
+ */
17
+ static async markReady() {
18
+ try {
19
+ const state = await (0, storage_1.readState)();
20
+ state.pending = false;
21
+ state.ready = true;
22
+ state.trial = false;
23
+ await (0, storage_1.writeState)(state);
24
+ }
25
+ catch (e) {
26
+ console.warn("[CustomOTA] Error updating state in markReady:", e);
27
+ }
28
+ if (native_1.OTAUpdater?.markReady) {
29
+ try {
30
+ await native_1.OTAUpdater.markReady();
31
+ }
32
+ catch (e) {
33
+ console.warn("[CustomOTA] Error calling native markReady:", e);
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * Khởi động lại ứng dụng để nạp ngay bản OTA mới.
39
+ */
40
+ static async restartApp() {
41
+ if (native_1.OTAUpdater?.restart) {
42
+ await native_1.OTAUpdater.restart();
43
+ }
44
+ }
45
+ /**
46
+ * Lấy thông tin phiên bản OTA hiện hành trên máy.
47
+ */
48
+ static async getUpdateMetadata() {
49
+ return (0, storage_1.readState)();
50
+ }
51
+ /**
52
+ * Kiểm tra xem server có bản cập nhật mới hay không.
53
+ */
54
+ static async checkForUpdate(options) {
55
+ await (0, storage_1.ensureOTADirs)();
56
+ const state = await (0, storage_1.readState)();
57
+ const platform = react_native_1.Platform.OS === "ios" ? "ios" : "android";
58
+ const nativeVersion = options.nativeVersion || "1.0.0";
59
+ const channel = options.channel || "production";
60
+ const params = new URLSearchParams({
61
+ platform,
62
+ nativeVersion,
63
+ otaVersion: state.currentVersion,
64
+ channel
65
+ });
66
+ if (options.appId) {
67
+ params.append("appId", options.appId);
68
+ }
69
+ if (options.apiKey) {
70
+ params.append("apiKey", options.apiKey);
71
+ }
72
+ const headers = {};
73
+ if (options.apiKey) {
74
+ headers["X-OTA-API-KEY"] = options.apiKey;
75
+ }
76
+ const response = await fetch(`${options.serverUrl.replace(/\/$/, "")}/ota/check?${params}`, {
77
+ headers
78
+ });
79
+ if (!response.ok) {
80
+ throw new Error(`OTA Check failed: HTTP ${response.status}`);
81
+ }
82
+ const rawData = await response.json();
83
+ if (!rawData.updateAvailable) {
84
+ return null;
85
+ }
86
+ const targetVersion = rawData.otaVersion || rawData.version;
87
+ // Không báo cập nhật nếu phiên bản đã được cài đặt trên máy
88
+ if (targetVersion && state.currentVersion === targetVersion) {
89
+ return null;
90
+ }
91
+ // Không báo cập nhật nếu phiên bản này từng bị crash và tự rollback
92
+ if (targetVersion && state.failedVersions?.includes(targetVersion)) {
93
+ return null;
94
+ }
95
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
96
+ const rawUrl = rawData.downloadUrl || "";
97
+ const fullDownloadUrl = rawUrl.startsWith("http")
98
+ ? rawUrl
99
+ : `${serverUrlClean}${rawUrl.startsWith("/") ? "" : "/"}${rawUrl}`;
100
+ const data = {
101
+ ...rawData,
102
+ updateAvailable: true,
103
+ version: targetVersion,
104
+ otaVersion: targetVersion,
105
+ downloadUrl: fullDownloadUrl,
106
+ mandatory: Boolean(rawData.mandatory || rawData.isMandatory),
107
+ isMandatory: Boolean(rawData.mandatory || rawData.isMandatory)
108
+ };
109
+ return data;
110
+ }
111
+ /**
112
+ * Tải và cài đặt bản cập nhật.
113
+ */
114
+ static async downloadAndInstall(update, onProgress, installMode) {
115
+ const version = update.otaVersion || update.version;
116
+ if (!update.downloadUrl || !version || !update.sha256) {
117
+ const missingFields = [
118
+ !update.downloadUrl && "downloadUrl",
119
+ !version && "version",
120
+ !update.sha256 && "sha256"
121
+ ].filter(Boolean).join(", ");
122
+ throw new Error(`Invalid update package metadata: missing ${missingFields}`);
123
+ }
124
+ const zipPath = `${storage_1.TEMP_DIR}/update-${version}.zip`;
125
+ await react_native_fs_1.default.unlink(zipPath).catch(() => { });
126
+ // Download zip file
127
+ const downloadRes = react_native_fs_1.default.downloadFile({
128
+ fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
129
+ toFile: zipPath,
130
+ progressDivider: 1,
131
+ progress: ({ bytesWritten, contentLength }) => {
132
+ if (contentLength > 0 && onProgress) {
133
+ onProgress(bytesWritten / contentLength);
134
+ }
135
+ }
136
+ });
137
+ const res = await downloadRes.promise;
138
+ if (res.statusCode < 200 || res.statusCode >= 300) {
139
+ throw new Error(`Download failed with status ${res.statusCode}`);
140
+ }
141
+ // SHA-256 verification
142
+ const actualHash = await react_native_fs_1.default.hash(zipPath, "sha256");
143
+ if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
144
+ await react_native_fs_1.default.unlink(zipPath).catch(() => { });
145
+ throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
146
+ }
147
+ // Unpack temp directory
148
+ const tempDir = `${storage_1.TEMP_DIR}/${version}`;
149
+ await react_native_fs_1.default.unlink(tempDir).catch(() => { });
150
+ await react_native_fs_1.default.mkdir(tempDir);
151
+ await native_1.OTAUpdater.unzip(zipPath, tempDir);
152
+ const bundleName = react_native_1.Platform.OS === "ios" ? "main.jsbundle" : "index.android.bundle";
153
+ const bundleFile = `${tempDir}/${bundleName}`;
154
+ if (!(await react_native_fs_1.default.exists(bundleFile))) {
155
+ throw new Error(`Bundle missing after unzip: ${bundleName}`);
156
+ }
157
+ // Atomic move & backup
158
+ await react_native_fs_1.default.unlink(storage_1.BACKUP_DIR).catch(() => { });
159
+ if (await react_native_fs_1.default.exists(storage_1.CURRENT_DIR)) {
160
+ await react_native_fs_1.default.moveFile(storage_1.CURRENT_DIR, storage_1.BACKUP_DIR);
161
+ }
162
+ await react_native_fs_1.default.moveFile(tempDir, storage_1.CURRENT_DIR);
163
+ // Save state
164
+ const oldState = await (0, storage_1.readState)();
165
+ await (0, storage_1.writeState)({
166
+ currentVersion: version,
167
+ previousVersion: oldState.currentVersion !== "0.0.0" ? oldState.currentVersion : null,
168
+ pending: true,
169
+ ready: false,
170
+ trial: false,
171
+ failedVersions: oldState.failedVersions || []
172
+ });
173
+ await react_native_fs_1.default.unlink(zipPath).catch(() => { });
174
+ if (installMode === types_1.InstallMode.IMMEDIATE) {
175
+ await this.restartApp();
176
+ }
177
+ return true;
178
+ }
179
+ /**
180
+ * Phương thức đồng bộ tự động như CodePush.sync()
181
+ */
182
+ static async sync(options, syncStatusCallback, downloadProgressCallback) {
183
+ try {
184
+ // 1. Tự động markReady khi khởi động
185
+ await this.markReady();
186
+ // 2. Kiểm tra update
187
+ const update = await this.checkForUpdate({
188
+ serverUrl: options.serverUrl,
189
+ appId: options.appId,
190
+ apiKey: options.apiKey,
191
+ nativeVersion: options.nativeVersion,
192
+ channel: options.channel
193
+ });
194
+ if (!update || !update.updateAvailable) {
195
+ syncStatusCallback?.(types_1.SyncStatus.UP_TO_DATE);
196
+ return types_1.SyncStatus.UP_TO_DATE;
197
+ }
198
+ syncStatusCallback?.(types_1.SyncStatus.UPDATE_AVAILABLE);
199
+ const proceedWithInstall = async () => {
200
+ syncStatusCallback?.(types_1.SyncStatus.DOWNLOADING_PACKAGE);
201
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
202
+ const fullDownloadUrl = update.downloadUrl?.startsWith("http")
203
+ ? update.downloadUrl
204
+ : `${serverUrlClean}${update.downloadUrl}`;
205
+ await this.downloadAndInstall({ ...update, downloadUrl: fullDownloadUrl }, downloadProgressCallback);
206
+ syncStatusCallback?.(types_1.SyncStatus.INSTALLING_UPDATE);
207
+ syncStatusCallback?.(types_1.SyncStatus.UPDATE_INSTALLED);
208
+ const effectiveInstallMode = (update.mandatory && options.mandatoryInstallMode)
209
+ ? options.mandatoryInstallMode
210
+ : (options.installMode || types_1.InstallMode.ON_NEXT_RESTART);
211
+ if (effectiveInstallMode === types_1.InstallMode.ON_NEXT_RESTART) {
212
+ return types_1.SyncStatus.UPDATE_INSTALLED;
213
+ }
214
+ // Restart immediate
215
+ await this.restartApp();
216
+ return types_1.SyncStatus.UPDATE_INSTALLED;
217
+ };
218
+ // 3. Nếu cấu hình hiển thị Update Dialog
219
+ if (options.updateDialog) {
220
+ const dialogOpts = typeof options.updateDialog === "object"
221
+ ? { ...this.defaultDialog, ...options.updateDialog }
222
+ : this.defaultDialog;
223
+ const isMandatory = Boolean(update.mandatory);
224
+ const message = isMandatory
225
+ ? `${dialogOpts.mandatoryUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`
226
+ : `${dialogOpts.optionalUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`;
227
+ const buttons = [];
228
+ if (!isMandatory) {
229
+ buttons.push({
230
+ text: dialogOpts.optionalIgnoreButtonLabel,
231
+ style: "cancel"
232
+ });
233
+ }
234
+ buttons.push({
235
+ text: dialogOpts.optionalInstallButtonLabel,
236
+ onPress: () => {
237
+ proceedWithInstall();
238
+ }
239
+ });
240
+ react_native_1.Alert.alert(dialogOpts.title || "Cập nhật ứng dụng", message, buttons, {
241
+ cancelable: !isMandatory
242
+ });
243
+ return types_1.SyncStatus.UPDATE_AVAILABLE;
244
+ }
245
+ // Tự động tải ngầm không hiện dialog
246
+ return await proceedWithInstall();
247
+ }
248
+ catch (error) {
249
+ console.error("[react-native-custom-ota] Sync error:", error);
250
+ syncStatusCallback?.(types_1.SyncStatus.ERROR);
251
+ return types_1.SyncStatus.ERROR;
252
+ }
253
+ }
254
+ }
255
+ exports.CustomOTAManager = CustomOTAManager;
256
+ CustomOTAManager.defaultDialog = {
257
+ title: "Có bản cập nhật mới",
258
+ 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ờ?",
259
+ 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.",
260
+ optionalInstallButtonLabel: "Cập nhật",
261
+ optionalIgnoreButtonLabel: "Để sau"
262
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OTAUpdater = void 0;
4
+ const react_native_1 = require("react-native");
5
+ exports.OTAUpdater = react_native_1.NativeModules.OTAUpdater;
6
+ if (!exports.OTAUpdater) {
7
+ console.warn("[react-native-custom-ota] NativeModule.OTAUpdater is null! " +
8
+ "Make sure you have linked the library and rebuilt the native binary.");
9
+ }
@@ -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,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 = {}));
@@ -0,0 +1,28 @@
1
+ import React, { useEffect } from "react";
2
+ import { AppState } from "react-native";
3
+ import { CustomOTAManager } from "./manager";
4
+ import { CheckFrequency } from "./types";
5
+ export function withCustomOTA(options) {
6
+ return function (WrappedComponent) {
7
+ return function CustomOTAWrapper(props) {
8
+ useEffect(() => {
9
+ // Tự động kiểm tra cập nhật khi app khởi động
10
+ if (options.checkFrequency !== CheckFrequency.MANUAL) {
11
+ CustomOTAManager.sync(options);
12
+ }
13
+ // Nếu bật kiểm tra khi App Resume từ background
14
+ if (options.checkFrequency === CheckFrequency.ON_APP_RESUME) {
15
+ const subscription = AppState.addEventListener("change", (nextAppState) => {
16
+ if (nextAppState === "active") {
17
+ CustomOTAManager.sync(options);
18
+ }
19
+ });
20
+ return () => {
21
+ subscription.remove();
22
+ };
23
+ }
24
+ }, []);
25
+ return React.createElement(WrappedComponent, { ...props });
26
+ };
27
+ };
28
+ }
@@ -0,0 +1,16 @@
1
+ import { CustomOTAManager } from "./manager";
2
+ import { withCustomOTA } from "./hoc";
3
+ export * from "./types";
4
+ export * from "./manager";
5
+ export * from "./hoc";
6
+ export * from "./storage";
7
+ const CustomOTA = {
8
+ sync: CustomOTAManager.sync.bind(CustomOTAManager),
9
+ checkForUpdate: CustomOTAManager.checkForUpdate.bind(CustomOTAManager),
10
+ downloadAndInstall: CustomOTAManager.downloadAndInstall.bind(CustomOTAManager),
11
+ restartApp: CustomOTAManager.restartApp.bind(CustomOTAManager),
12
+ getUpdateMetadata: CustomOTAManager.getUpdateMetadata.bind(CustomOTAManager),
13
+ markReady: CustomOTAManager.markReady.bind(CustomOTAManager),
14
+ wrap: withCustomOTA
15
+ };
16
+ export default CustomOTA;
@@ -0,0 +1,255 @@
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 { InstallMode, SyncStatus } from "./types";
6
+ export class CustomOTAManager {
7
+ /**
8
+ * Đánh dấu bản build OTA hiện tại đã khởi động thành công.
9
+ * Xóa bản backup trước đó và tắt cờ pending.
10
+ */
11
+ static async markReady() {
12
+ try {
13
+ const state = await readState();
14
+ state.pending = false;
15
+ state.ready = true;
16
+ state.trial = false;
17
+ await writeState(state);
18
+ }
19
+ catch (e) {
20
+ console.warn("[CustomOTA] Error updating state in markReady:", e);
21
+ }
22
+ if (OTAUpdater?.markReady) {
23
+ try {
24
+ await OTAUpdater.markReady();
25
+ }
26
+ catch (e) {
27
+ console.warn("[CustomOTA] Error calling native markReady:", e);
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Khởi động lại ứng dụng để nạp ngay bản OTA mới.
33
+ */
34
+ static async restartApp() {
35
+ if (OTAUpdater?.restart) {
36
+ await OTAUpdater.restart();
37
+ }
38
+ }
39
+ /**
40
+ * Lấy thông tin phiên bản OTA hiện hành trên máy.
41
+ */
42
+ static async getUpdateMetadata() {
43
+ return readState();
44
+ }
45
+ /**
46
+ * Kiểm tra xem server có bản cập nhật mới hay không.
47
+ */
48
+ static async checkForUpdate(options) {
49
+ await ensureOTADirs();
50
+ const state = await readState();
51
+ const platform = Platform.OS === "ios" ? "ios" : "android";
52
+ const nativeVersion = options.nativeVersion || "1.0.0";
53
+ const channel = options.channel || "production";
54
+ const params = new URLSearchParams({
55
+ platform,
56
+ nativeVersion,
57
+ otaVersion: state.currentVersion,
58
+ channel
59
+ });
60
+ if (options.appId) {
61
+ params.append("appId", options.appId);
62
+ }
63
+ if (options.apiKey) {
64
+ params.append("apiKey", options.apiKey);
65
+ }
66
+ const headers = {};
67
+ if (options.apiKey) {
68
+ headers["X-OTA-API-KEY"] = options.apiKey;
69
+ }
70
+ const response = await fetch(`${options.serverUrl.replace(/\/$/, "")}/ota/check?${params}`, {
71
+ headers
72
+ });
73
+ if (!response.ok) {
74
+ throw new Error(`OTA Check failed: HTTP ${response.status}`);
75
+ }
76
+ const rawData = await response.json();
77
+ if (!rawData.updateAvailable) {
78
+ return null;
79
+ }
80
+ const targetVersion = rawData.otaVersion || rawData.version;
81
+ // Không báo cập nhật nếu phiên bản đã được cài đặt trên máy
82
+ if (targetVersion && state.currentVersion === targetVersion) {
83
+ return null;
84
+ }
85
+ // Không báo cập nhật nếu phiên bản này từng bị crash và tự rollback
86
+ if (targetVersion && state.failedVersions?.includes(targetVersion)) {
87
+ return null;
88
+ }
89
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
90
+ const rawUrl = rawData.downloadUrl || "";
91
+ const fullDownloadUrl = rawUrl.startsWith("http")
92
+ ? rawUrl
93
+ : `${serverUrlClean}${rawUrl.startsWith("/") ? "" : "/"}${rawUrl}`;
94
+ const data = {
95
+ ...rawData,
96
+ updateAvailable: true,
97
+ version: targetVersion,
98
+ otaVersion: targetVersion,
99
+ downloadUrl: fullDownloadUrl,
100
+ mandatory: Boolean(rawData.mandatory || rawData.isMandatory),
101
+ isMandatory: Boolean(rawData.mandatory || rawData.isMandatory)
102
+ };
103
+ return data;
104
+ }
105
+ /**
106
+ * Tải và cài đặt bản cập nhật.
107
+ */
108
+ static async downloadAndInstall(update, onProgress, installMode) {
109
+ const version = update.otaVersion || update.version;
110
+ if (!update.downloadUrl || !version || !update.sha256) {
111
+ const missingFields = [
112
+ !update.downloadUrl && "downloadUrl",
113
+ !version && "version",
114
+ !update.sha256 && "sha256"
115
+ ].filter(Boolean).join(", ");
116
+ throw new Error(`Invalid update package metadata: missing ${missingFields}`);
117
+ }
118
+ const zipPath = `${TEMP_DIR}/update-${version}.zip`;
119
+ await RNFS.unlink(zipPath).catch(() => { });
120
+ // Download zip file
121
+ const downloadRes = RNFS.downloadFile({
122
+ fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
123
+ toFile: zipPath,
124
+ progressDivider: 1,
125
+ progress: ({ bytesWritten, contentLength }) => {
126
+ if (contentLength > 0 && onProgress) {
127
+ onProgress(bytesWritten / contentLength);
128
+ }
129
+ }
130
+ });
131
+ const res = await downloadRes.promise;
132
+ if (res.statusCode < 200 || res.statusCode >= 300) {
133
+ throw new Error(`Download failed with status ${res.statusCode}`);
134
+ }
135
+ // SHA-256 verification
136
+ const actualHash = await RNFS.hash(zipPath, "sha256");
137
+ if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
138
+ await RNFS.unlink(zipPath).catch(() => { });
139
+ throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
140
+ }
141
+ // Unpack temp directory
142
+ const tempDir = `${TEMP_DIR}/${version}`;
143
+ await RNFS.unlink(tempDir).catch(() => { });
144
+ await RNFS.mkdir(tempDir);
145
+ await OTAUpdater.unzip(zipPath, tempDir);
146
+ const bundleName = Platform.OS === "ios" ? "main.jsbundle" : "index.android.bundle";
147
+ const bundleFile = `${tempDir}/${bundleName}`;
148
+ if (!(await RNFS.exists(bundleFile))) {
149
+ throw new Error(`Bundle missing after unzip: ${bundleName}`);
150
+ }
151
+ // Atomic move & backup
152
+ await RNFS.unlink(BACKUP_DIR).catch(() => { });
153
+ if (await RNFS.exists(CURRENT_DIR)) {
154
+ await RNFS.moveFile(CURRENT_DIR, BACKUP_DIR);
155
+ }
156
+ await RNFS.moveFile(tempDir, CURRENT_DIR);
157
+ // Save state
158
+ const oldState = await readState();
159
+ await writeState({
160
+ currentVersion: version,
161
+ previousVersion: oldState.currentVersion !== "0.0.0" ? oldState.currentVersion : null,
162
+ pending: true,
163
+ ready: false,
164
+ trial: false,
165
+ failedVersions: oldState.failedVersions || []
166
+ });
167
+ await RNFS.unlink(zipPath).catch(() => { });
168
+ if (installMode === InstallMode.IMMEDIATE) {
169
+ await this.restartApp();
170
+ }
171
+ return true;
172
+ }
173
+ /**
174
+ * Phương thức đồng bộ tự động như CodePush.sync()
175
+ */
176
+ static async sync(options, syncStatusCallback, downloadProgressCallback) {
177
+ try {
178
+ // 1. Tự động markReady khi khởi động
179
+ await this.markReady();
180
+ // 2. Kiểm tra update
181
+ const update = await this.checkForUpdate({
182
+ serverUrl: options.serverUrl,
183
+ appId: options.appId,
184
+ apiKey: options.apiKey,
185
+ nativeVersion: options.nativeVersion,
186
+ channel: options.channel
187
+ });
188
+ if (!update || !update.updateAvailable) {
189
+ syncStatusCallback?.(SyncStatus.UP_TO_DATE);
190
+ return SyncStatus.UP_TO_DATE;
191
+ }
192
+ syncStatusCallback?.(SyncStatus.UPDATE_AVAILABLE);
193
+ const proceedWithInstall = async () => {
194
+ syncStatusCallback?.(SyncStatus.DOWNLOADING_PACKAGE);
195
+ const serverUrlClean = options.serverUrl.replace(/\/$/, "");
196
+ const fullDownloadUrl = update.downloadUrl?.startsWith("http")
197
+ ? update.downloadUrl
198
+ : `${serverUrlClean}${update.downloadUrl}`;
199
+ await this.downloadAndInstall({ ...update, downloadUrl: fullDownloadUrl }, downloadProgressCallback);
200
+ syncStatusCallback?.(SyncStatus.INSTALLING_UPDATE);
201
+ syncStatusCallback?.(SyncStatus.UPDATE_INSTALLED);
202
+ const effectiveInstallMode = (update.mandatory && options.mandatoryInstallMode)
203
+ ? options.mandatoryInstallMode
204
+ : (options.installMode || InstallMode.ON_NEXT_RESTART);
205
+ if (effectiveInstallMode === InstallMode.ON_NEXT_RESTART) {
206
+ return SyncStatus.UPDATE_INSTALLED;
207
+ }
208
+ // Restart immediate
209
+ await this.restartApp();
210
+ return SyncStatus.UPDATE_INSTALLED;
211
+ };
212
+ // 3. Nếu cấu hình hiển thị Update Dialog
213
+ if (options.updateDialog) {
214
+ const dialogOpts = typeof options.updateDialog === "object"
215
+ ? { ...this.defaultDialog, ...options.updateDialog }
216
+ : this.defaultDialog;
217
+ const isMandatory = Boolean(update.mandatory);
218
+ const message = isMandatory
219
+ ? `${dialogOpts.mandatoryUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`
220
+ : `${dialogOpts.optionalUpdateMessage}\n\nPhiên bản: ${update.otaVersion}\n${update.releaseNotes || ""}`;
221
+ const buttons = [];
222
+ if (!isMandatory) {
223
+ buttons.push({
224
+ text: dialogOpts.optionalIgnoreButtonLabel,
225
+ style: "cancel"
226
+ });
227
+ }
228
+ buttons.push({
229
+ text: dialogOpts.optionalInstallButtonLabel,
230
+ onPress: () => {
231
+ proceedWithInstall();
232
+ }
233
+ });
234
+ Alert.alert(dialogOpts.title || "Cập nhật ứng dụng", message, buttons, {
235
+ cancelable: !isMandatory
236
+ });
237
+ return SyncStatus.UPDATE_AVAILABLE;
238
+ }
239
+ // Tự động tải ngầm không hiện dialog
240
+ return await proceedWithInstall();
241
+ }
242
+ catch (error) {
243
+ console.error("[react-native-custom-ota] Sync error:", error);
244
+ syncStatusCallback?.(SyncStatus.ERROR);
245
+ return SyncStatus.ERROR;
246
+ }
247
+ }
248
+ }
249
+ CustomOTAManager.defaultDialog = {
250
+ title: "Có bản cập nhật mới",
251
+ 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ờ?",
252
+ 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.",
253
+ optionalInstallButtonLabel: "Cập nhật",
254
+ optionalIgnoreButtonLabel: "Để sau"
255
+ };
@@ -0,0 +1,6 @@
1
+ import { NativeModules } from "react-native";
2
+ export const OTAUpdater = NativeModules.OTAUpdater;
3
+ if (!OTAUpdater) {
4
+ console.warn("[react-native-custom-ota] NativeModule.OTAUpdater is null! " +
5
+ "Make sure you have linked the library and rebuilt the native binary.");
6
+ }