react-native-custom-ota 1.0.2 → 1.0.3

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,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCustomOTA = useCustomOTA;
4
+ const react_1 = require("react");
5
+ const manager_1 = require("./manager");
6
+ const types_1 = require("./types");
7
+ function useCustomOTA() {
8
+ const [isChecking, setIsChecking] = (0, react_1.useState)(false);
9
+ const [isDownloading, setIsDownloading] = (0, react_1.useState)(false);
10
+ const [progress, setProgress] = (0, react_1.useState)({
11
+ totalBytes: 0,
12
+ receivedBytes: 0,
13
+ percentage: 0,
14
+ progress: 0
15
+ });
16
+ const [syncStatus, setSyncStatus] = (0, react_1.useState)(types_1.SyncStatus.UP_TO_DATE);
17
+ const [detailedStatus, setDetailedStatus] = (0, react_1.useState)(types_1.DetailedStatus.IDLE);
18
+ const [statusText, setStatusText] = (0, react_1.useState)("");
19
+ const [activeMetadata, setActiveMetadata] = (0, react_1.useState)(null);
20
+ const refreshMetadata = (0, react_1.useCallback)(async () => {
21
+ try {
22
+ const meta = await manager_1.CustomOTAManager.getUpdateMetadata();
23
+ setActiveMetadata(meta);
24
+ return meta;
25
+ }
26
+ catch {
27
+ return {
28
+ currentVersion: "0.0.0",
29
+ previousVersion: null,
30
+ pending: false,
31
+ ready: true,
32
+ failedVersions: []
33
+ };
34
+ }
35
+ }, []);
36
+ (0, react_1.useEffect)(() => {
37
+ refreshMetadata();
38
+ // Đăng ký nhận sự kiện tiến độ toàn cục
39
+ const removeProgressListener = manager_1.CustomOTAManager.addProgressListener((prog) => {
40
+ setProgress(prog);
41
+ setIsDownloading(true);
42
+ setDetailedStatus(types_1.DetailedStatus.DOWNLOADING);
43
+ setStatusText(`Đang tải bản cập nhật: ${prog.percentage}%`);
44
+ });
45
+ const removeStatusListener = manager_1.CustomOTAManager.addDetailedStatusListener((status, msg) => {
46
+ setDetailedStatus(status);
47
+ if (msg)
48
+ setStatusText(msg);
49
+ if (status === types_1.DetailedStatus.DOWNLOADING) {
50
+ setIsDownloading(true);
51
+ }
52
+ else if (status === types_1.DetailedStatus.COMPLETED || status === types_1.DetailedStatus.ERROR) {
53
+ setIsDownloading(false);
54
+ refreshMetadata();
55
+ }
56
+ });
57
+ return () => {
58
+ removeProgressListener();
59
+ removeStatusListener();
60
+ };
61
+ }, [refreshMetadata]);
62
+ const checkForUpdate = (0, react_1.useCallback)(async (options) => {
63
+ setIsChecking(true);
64
+ setDetailedStatus(types_1.DetailedStatus.CHECKING);
65
+ setStatusText("Đang kiểm tra phiên bản mới...");
66
+ try {
67
+ const update = await manager_1.CustomOTAManager.checkForUpdate(options);
68
+ if (update && update.updateAvailable) {
69
+ setDetailedStatus(types_1.DetailedStatus.UPDATE_AVAILABLE);
70
+ setStatusText(`Tìm thấy bản cập nhật mới v${update.otaVersion}`);
71
+ }
72
+ else {
73
+ setDetailedStatus(types_1.DetailedStatus.IDLE);
74
+ setStatusText("Ứng dụng đang ở phiên bản mới nhất");
75
+ }
76
+ return update;
77
+ }
78
+ catch (err) {
79
+ setDetailedStatus(types_1.DetailedStatus.ERROR);
80
+ setStatusText(err.message || "Lỗi khi kiểm tra cập nhật");
81
+ throw err;
82
+ }
83
+ finally {
84
+ setIsChecking(false);
85
+ }
86
+ }, []);
87
+ const sync = (0, react_1.useCallback)(async (options) => {
88
+ setIsChecking(true);
89
+ setProgress({ totalBytes: 0, receivedBytes: 0, percentage: 0, progress: 0 });
90
+ try {
91
+ const status = await manager_1.CustomOTAManager.sync(options, (s) => setSyncStatus(s), (prog) => setProgress(prog));
92
+ setSyncStatus(status);
93
+ await refreshMetadata();
94
+ return status;
95
+ }
96
+ finally {
97
+ setIsChecking(false);
98
+ setIsDownloading(false);
99
+ }
100
+ }, [refreshMetadata]);
101
+ const restartApp = (0, react_1.useCallback)(async () => {
102
+ await manager_1.CustomOTAManager.restartApp();
103
+ }, []);
104
+ return {
105
+ isChecking,
106
+ isDownloading,
107
+ progress,
108
+ syncStatus,
109
+ detailedStatus,
110
+ statusText,
111
+ activeMetadata,
112
+ activeVersion: activeMetadata?.currentVersion || "0.0.0",
113
+ checkForUpdate,
114
+ sync,
115
+ restartApp,
116
+ refreshMetadata
117
+ };
118
+ }
119
+ exports.default = useCustomOTA;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-custom-ota",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Self-hosted OTA (Over-The-Air) update library for React Native with automatic rollback, similar to react-native-code-push",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -0,0 +1,214 @@
1
+ import React from "react";
2
+ import {
3
+ Modal,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ ActivityIndicator,
8
+ Dimensions
9
+ } from "react-native";
10
+ import { DownloadProgress } from "./types";
11
+
12
+ export interface OTAProgressModalProps {
13
+ /** Trạng thái hiển thị modal */
14
+ visible: boolean;
15
+ /** Thông tin tiến độ tải (object hoặc số thập phân 0.0 - 1.0) */
16
+ progress?: DownloadProgress | number;
17
+ /** Tiêu đề modal (Mặc định: 'Cập nhật ứng dụng') */
18
+ title?: string;
19
+ /** Thông điệp trạng thái hiện tại (vd: 'Đang tải gói cập nhật...') */
20
+ statusText?: string;
21
+ /** Phiên bản đang được tải về (vd: '1.0.9') */
22
+ version?: string;
23
+ }
24
+
25
+ export function formatBytes(bytes: number, decimals = 1): string {
26
+ if (!bytes || bytes <= 0) return "0 B";
27
+ const k = 1024;
28
+ const dm = decimals < 0 ? 0 : decimals;
29
+ const sizes = ["B", "KB", "MB", "GB"];
30
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
31
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
32
+ }
33
+
34
+ export const OTAProgressModal: React.FC<OTAProgressModalProps> = ({
35
+ visible,
36
+ progress,
37
+ title = "Cập nhật ứng dụng",
38
+ statusText = "Đang tải bản cập nhật...",
39
+ version
40
+ }) => {
41
+ // Chuẩn hóa dữ liệu tiến độ
42
+ let percent = 0;
43
+ let receivedBytes = 0;
44
+ let totalBytes = 0;
45
+
46
+ if (typeof progress === "number") {
47
+ percent = Math.min(100, Math.max(0, Math.round(progress * 100)));
48
+ } else if (progress && typeof progress === "object") {
49
+ percent = Math.min(100, Math.max(0, Math.round(progress.percentage || 0)));
50
+ receivedBytes = progress.receivedBytes || 0;
51
+ totalBytes = progress.totalBytes || 0;
52
+ }
53
+
54
+ const bytesInfo = totalBytes > 0
55
+ ? `${formatBytes(receivedBytes)} / ${formatBytes(totalBytes)}`
56
+ : "";
57
+
58
+ return (
59
+ <Modal
60
+ transparent
61
+ animationType="fade"
62
+ visible={visible}
63
+ statusBarTranslucent
64
+ >
65
+ <View style={styles.overlay}>
66
+ <View style={styles.card}>
67
+ {/* Header */}
68
+ <View style={styles.header}>
69
+ <View style={styles.iconContainer}>
70
+ <Text style={styles.iconText}>⚡</Text>
71
+ </View>
72
+ <View style={styles.headerTextContainer}>
73
+ <Text style={styles.title}>{title}</Text>
74
+ {version ? (
75
+ <Text style={styles.versionBadge}>Phiên bản v{version}</Text>
76
+ ) : null}
77
+ </View>
78
+ </View>
79
+
80
+ {/* Status Message */}
81
+ <Text style={styles.statusText}>{statusText}</Text>
82
+
83
+ {/* Progress Bar */}
84
+ <View style={styles.progressBarTrack}>
85
+ <View style={[styles.progressBarFill, { width: `${percent}%` }]} />
86
+ </View>
87
+
88
+ {/* Progress Details */}
89
+ <View style={styles.detailsRow}>
90
+ <Text style={styles.percentageText}>{percent}%</Text>
91
+ {bytesInfo ? (
92
+ <Text style={styles.bytesText}>{bytesInfo}</Text>
93
+ ) : null}
94
+ </View>
95
+
96
+ {/* Footer note */}
97
+ <View style={styles.footer}>
98
+ <ActivityIndicator size="small" color="#38bdf8" style={{ marginRight: 8 }} />
99
+ <Text style={styles.footerText}>
100
+ Vui lòng không đóng ứng dụng...
101
+ </Text>
102
+ </View>
103
+ </View>
104
+ </View>
105
+ </Modal>
106
+ );
107
+ };
108
+
109
+ const styles = StyleSheet.create({
110
+ overlay: {
111
+ flex: 1,
112
+ backgroundColor: "rgba(0, 0, 0, 0.72)",
113
+ justifyContent: "center",
114
+ alignItems: "center",
115
+ paddingHorizontal: 24
116
+ },
117
+ card: {
118
+ width: "100%",
119
+ maxWidth: 380,
120
+ backgroundColor: "#0f172a",
121
+ borderRadius: 20,
122
+ padding: 24,
123
+ borderWidth: 1,
124
+ borderColor: "rgba(255, 255, 255, 0.12)",
125
+ shadowColor: "#000",
126
+ shadowOffset: { width: 0, height: 12 },
127
+ shadowOpacity: 0.5,
128
+ shadowRadius: 20,
129
+ elevation: 16
130
+ },
131
+ header: {
132
+ flexDirection: "row",
133
+ alignItems: "center",
134
+ marginBottom: 16
135
+ },
136
+ iconContainer: {
137
+ width: 44,
138
+ height: 44,
139
+ borderRadius: 12,
140
+ backgroundColor: "rgba(56, 189, 248, 0.15)",
141
+ justifyContent: "center",
142
+ alignItems: "center",
143
+ marginRight: 14,
144
+ borderWidth: 1,
145
+ borderColor: "rgba(56, 189, 248, 0.3)"
146
+ },
147
+ iconText: {
148
+ fontSize: 22
149
+ },
150
+ headerTextContainer: {
151
+ flex: 1
152
+ },
153
+ title: {
154
+ fontSize: 18,
155
+ fontWeight: "700",
156
+ color: "#f8fafc",
157
+ letterSpacing: 0.2
158
+ },
159
+ versionBadge: {
160
+ fontSize: 12,
161
+ color: "#38bdf8",
162
+ fontWeight: "600",
163
+ marginTop: 2
164
+ },
165
+ statusText: {
166
+ fontSize: 14,
167
+ color: "#94a3b8",
168
+ marginBottom: 16,
169
+ lineHeight: 20
170
+ },
171
+ progressBarTrack: {
172
+ height: 10,
173
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
174
+ borderRadius: 999,
175
+ overflow: "hidden",
176
+ marginBottom: 10
177
+ },
178
+ progressBarFill: {
179
+ height: "100%",
180
+ backgroundColor: "#38bdf8",
181
+ borderRadius: 999
182
+ },
183
+ detailsRow: {
184
+ flexDirection: "row",
185
+ justifyContent: "space-between",
186
+ alignItems: "center",
187
+ marginBottom: 20
188
+ },
189
+ percentageText: {
190
+ fontSize: 16,
191
+ fontWeight: "800",
192
+ color: "#38bdf8"
193
+ },
194
+ bytesText: {
195
+ fontSize: 13,
196
+ color: "#64748b",
197
+ fontWeight: "500"
198
+ },
199
+ footer: {
200
+ flexDirection: "row",
201
+ alignItems: "center",
202
+ justifyContent: "center",
203
+ paddingTop: 12,
204
+ borderTopWidth: 1,
205
+ borderTopColor: "rgba(255, 255, 255, 0.06)"
206
+ },
207
+ footerText: {
208
+ fontSize: 12,
209
+ color: "#64748b",
210
+ fontWeight: "500"
211
+ }
212
+ });
213
+
214
+ export default OTAProgressModal;
package/src/hoc.tsx CHANGED
@@ -1,12 +1,44 @@
1
- import React, { ComponentType, useEffect } from "react";
2
- import { AppState, AppStateStatus } from "react-native";
1
+ import React, { ComponentType, useEffect, useState } from "react";
2
+ import { AppState, AppStateStatus, View } from "react-native";
3
3
  import { CustomOTAManager } from "./manager";
4
- import { CheckFrequency, SyncOptions } from "./types";
4
+ import { CheckFrequency, DownloadProgress, SyncOptions, DetailedStatus } from "./types";
5
+ import { OTAProgressModal } from "./ProgressModal";
5
6
 
6
7
  export function withCustomOTA(options: SyncOptions) {
7
8
  return function <P extends object>(WrappedComponent: ComponentType<P>): ComponentType<P> {
8
9
  return function CustomOTAWrapper(props: P) {
10
+ const [isDownloading, setIsDownloading] = useState(false);
11
+ const [progress, setProgress] = useState<DownloadProgress>({
12
+ totalBytes: 0,
13
+ receivedBytes: 0,
14
+ percentage: 0,
15
+ progress: 0
16
+ });
17
+ const [statusText, setStatusText] = useState("Đang tải bản cập nhật...");
18
+
9
19
  useEffect(() => {
20
+ let removeProg: (() => void) | undefined;
21
+ let removeStatus: (() => void) | undefined;
22
+
23
+ if (options.showProgressModal) {
24
+ removeProg = CustomOTAManager.addProgressListener((p) => {
25
+ setProgress(p);
26
+ setIsDownloading(true);
27
+ });
28
+ removeStatus = CustomOTAManager.addDetailedStatusListener((s, msg) => {
29
+ if (msg) setStatusText(msg);
30
+ if (
31
+ s === DetailedStatus.DOWNLOADING ||
32
+ s === DetailedStatus.VERIFYING ||
33
+ s === DetailedStatus.INSTALLING
34
+ ) {
35
+ setIsDownloading(true);
36
+ } else if (s === DetailedStatus.COMPLETED || s === DetailedStatus.ERROR) {
37
+ setIsDownloading(false);
38
+ }
39
+ });
40
+ }
41
+
10
42
  // Tự động kiểm tra cập nhật khi app khởi động
11
43
  if (options.checkFrequency !== CheckFrequency.MANUAL) {
12
44
  CustomOTAManager.sync(options);
@@ -24,11 +56,31 @@ export function withCustomOTA(options: SyncOptions) {
24
56
  );
25
57
  return () => {
26
58
  subscription.remove();
59
+ removeProg?.();
60
+ removeStatus?.();
27
61
  };
28
62
  }
63
+
64
+ return () => {
65
+ removeProg?.();
66
+ removeStatus?.();
67
+ };
29
68
  }, []);
30
69
 
31
- return <WrappedComponent {...props} />;
70
+ return (
71
+ <View style={{ flex: 1 }}>
72
+ <WrappedComponent {...props} />
73
+ {options.showProgressModal && (
74
+ <OTAProgressModal
75
+ visible={isDownloading}
76
+ progress={progress}
77
+ statusText={statusText}
78
+ />
79
+ )}
80
+ </View>
81
+ );
32
82
  };
33
83
  };
34
84
  }
85
+
86
+ export default withCustomOTA;
package/src/index.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { CustomOTAManager } from "./manager";
2
2
  import { withCustomOTA } from "./hoc";
3
+ import { OTAProgressModal } from "./ProgressModal";
4
+ import { useCustomOTA } from "./useCustomOTA";
3
5
 
4
6
  export * from "./types";
5
7
  export * from "./manager";
6
8
  export * from "./hoc";
7
9
  export * from "./storage";
10
+ export * from "./ProgressModal";
11
+ export * from "./useCustomOTA";
8
12
 
9
13
  const CustomOTA = {
10
14
  sync: CustomOTAManager.sync.bind(CustomOTAManager),
@@ -13,7 +17,11 @@ const CustomOTA = {
13
17
  restartApp: CustomOTAManager.restartApp.bind(CustomOTAManager),
14
18
  getUpdateMetadata: CustomOTAManager.getUpdateMetadata.bind(CustomOTAManager),
15
19
  markReady: CustomOTAManager.markReady.bind(CustomOTAManager),
16
- wrap: withCustomOTA
20
+ addProgressListener: CustomOTAManager.addProgressListener.bind(CustomOTAManager),
21
+ addDetailedStatusListener: CustomOTAManager.addDetailedStatusListener.bind(CustomOTAManager),
22
+ wrap: withCustomOTA,
23
+ ProgressModal: OTAProgressModal,
24
+ useCustomOTA: useCustomOTA
17
25
  };
18
26
 
19
27
  export default CustomOTA;
package/src/manager.ts CHANGED
@@ -3,12 +3,14 @@ import RNFS from "react-native-fs";
3
3
  import { OTAUpdater } from "./native";
4
4
  import { BACKUP_DIR, CURRENT_DIR, TEMP_DIR, ensureOTADirs, readState, writeState } from "./storage";
5
5
  import {
6
+ DownloadProgress,
6
7
  InstallMode,
7
8
  LocalPackage,
8
9
  RemotePackage,
9
10
  SyncOptions,
10
11
  SyncStatus,
11
- UpdateDialogOptions
12
+ UpdateDialogOptions,
13
+ DetailedStatus
12
14
  } from "./types";
13
15
 
14
16
  export class CustomOTAManager {
@@ -17,9 +19,55 @@ export class CustomOTAManager {
17
19
  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
20
  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
21
  optionalInstallButtonLabel: "Cập nhật",
20
- optionalIgnoreButtonLabel: "Để sau"
22
+ optionalIgnoreButtonLabel: "Để sau",
23
+ showProgressModal: true
21
24
  };
22
25
 
26
+ private static progressListeners = new Set<(progress: DownloadProgress) => void>();
27
+ private static detailedStatusListeners = new Set<(status: DetailedStatus, message?: string) => void>();
28
+
29
+ /**
30
+ * Đăng ký lắng nghe tiến độ tải về toàn cục
31
+ * @returns Hàm hủy đăng ký (unsubscribe)
32
+ */
33
+ static addProgressListener(listener: (progress: DownloadProgress) => void): () => void {
34
+ this.progressListeners.add(listener);
35
+ return () => {
36
+ this.progressListeners.delete(listener);
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Đăng ký lắng nghe trạng thái chi tiết của quá trình OTA
42
+ * @returns Hàm hủy đăng ký (unsubscribe)
43
+ */
44
+ static addDetailedStatusListener(listener: (status: DetailedStatus, message?: string) => void): () => void {
45
+ this.detailedStatusListeners.add(listener);
46
+ return () => {
47
+ this.detailedStatusListeners.delete(listener);
48
+ };
49
+ }
50
+
51
+ private static emitProgress(progress: DownloadProgress) {
52
+ for (const listener of this.progressListeners) {
53
+ try {
54
+ listener(progress);
55
+ } catch (e) {
56
+ console.warn("[CustomOTA] Error in progress listener:", e);
57
+ }
58
+ }
59
+ }
60
+
61
+ private static emitDetailedStatus(status: DetailedStatus, message?: string) {
62
+ for (const listener of this.detailedStatusListeners) {
63
+ try {
64
+ listener(status, message);
65
+ } catch (e) {
66
+ console.warn("[CustomOTA] Error in detailed status listener:", e);
67
+ }
68
+ }
69
+ }
70
+
23
71
  /**
24
72
  * Đánh dấu bản build OTA hiện tại đã khởi động thành công.
25
73
  * Xóa bản backup trước đó và tắt cờ pending.
@@ -61,7 +109,7 @@ export class CustomOTAManager {
61
109
  }
62
110
 
63
111
  /**
64
- * Kiểm tra xem server có bản cập nhật mới hay không.
112
+ * Kiểm tra xem có bản cập nhật mới từ OTA Server hay không.
65
113
  */
66
114
  static async checkForUpdate(options: {
67
115
  serverUrl: string;
@@ -140,11 +188,11 @@ export class CustomOTAManager {
140
188
  }
141
189
 
142
190
  /**
143
- * Tải và cài đặt bản cập nhật.
191
+ * Tải và cài đặt bản cập nhật với theo dõi tiến độ chi tiết.
144
192
  */
145
193
  static async downloadAndInstall(
146
194
  update: RemotePackage,
147
- onProgress?: (progress: number) => void,
195
+ onProgress?: ((progress: DownloadProgress) => void) | ((progressValue: number) => void),
148
196
  installMode?: InstallMode
149
197
  ): Promise<boolean> {
150
198
  const version = update.otaVersion || update.version;
@@ -157,34 +205,58 @@ export class CustomOTAManager {
157
205
  throw new Error(`Invalid update package metadata: missing ${missingFields}`);
158
206
  }
159
207
 
208
+ this.emitDetailedStatus(DetailedStatus.DOWNLOADING, `Bắt đầu tải bản cập nhật v${version}...`);
209
+
160
210
  const zipPath = `${TEMP_DIR}/update-${version}.zip`;
161
211
  await RNFS.unlink(zipPath).catch(() => {});
162
212
 
163
- // Download zip file
213
+ // Download zip file với theo dõi byte chính xác
164
214
  const downloadRes = RNFS.downloadFile({
165
215
  fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
166
216
  toFile: zipPath,
167
217
  progressDivider: 1,
168
218
  progress: ({ bytesWritten, contentLength }) => {
169
- if (contentLength > 0 && onProgress) {
170
- onProgress(bytesWritten / contentLength);
219
+ const total = contentLength > 0 ? contentLength : (update.size || 0);
220
+ const percent = total > 0 ? Math.min(100, Math.round((bytesWritten / total) * 100)) : 0;
221
+ const progressVal = total > 0 ? Math.min(1, bytesWritten / total) : 0;
222
+
223
+ const progressObj: DownloadProgress = {
224
+ totalBytes: total,
225
+ receivedBytes: bytesWritten,
226
+ percentage: percent,
227
+ progress: progressVal
228
+ };
229
+
230
+ if (onProgress) {
231
+ // Callback nhận cả object lẫn hỗ trợ kiểu cũ nhận số
232
+ try {
233
+ (onProgress as any)(progressObj, progressVal);
234
+ } catch {
235
+ (onProgress as any)(progressVal);
236
+ }
171
237
  }
238
+
239
+ this.emitProgress(progressObj);
172
240
  }
173
241
  });
174
242
 
175
243
  const res = await downloadRes.promise;
176
244
  if (res.statusCode < 200 || res.statusCode >= 300) {
245
+ this.emitDetailedStatus(DetailedStatus.ERROR, `Tải thất bại với mã lỗi HTTP ${res.statusCode}`);
177
246
  throw new Error(`Download failed with status ${res.statusCode}`);
178
247
  }
179
248
 
180
249
  // SHA-256 verification
250
+ this.emitDetailedStatus(DetailedStatus.VERIFYING, "Đang kiểm tra tính toàn vẹn (SHA-256)...");
181
251
  const actualHash = await RNFS.hash(zipPath, "sha256");
182
252
  if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
183
253
  await RNFS.unlink(zipPath).catch(() => {});
254
+ this.emitDetailedStatus(DetailedStatus.ERROR, "Mã băm SHA-256 không trùng khớp!");
184
255
  throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
185
256
  }
186
257
 
187
258
  // Unpack temp directory
259
+ this.emitDetailedStatus(DetailedStatus.INSTALLING, "Đang giải nén và nạp gói cập nhật...");
188
260
  const tempDir = `${TEMP_DIR}/${version}`;
189
261
  await RNFS.unlink(tempDir).catch(() => {});
190
262
  await RNFS.mkdir(tempDir);
@@ -218,6 +290,8 @@ export class CustomOTAManager {
218
290
 
219
291
  await RNFS.unlink(zipPath).catch(() => {});
220
292
 
293
+ this.emitDetailedStatus(DetailedStatus.COMPLETED, `Đã cài đặt bản cập nhật v${version} thành công!`);
294
+
221
295
  if (installMode === InstallMode.IMMEDIATE) {
222
296
  await this.restartApp();
223
297
  }
@@ -231,7 +305,7 @@ export class CustomOTAManager {
231
305
  static async sync(
232
306
  options: SyncOptions,
233
307
  syncStatusCallback?: (status: SyncStatus) => void,
234
- downloadProgressCallback?: (progress: number) => void
308
+ downloadProgressCallback?: ((progress: DownloadProgress) => void) | ((progressValue: number) => void)
235
309
  ): Promise<SyncStatus> {
236
310
  try {
237
311
  // 1. Tự động markReady khi khởi động
package/src/types.ts CHANGED
@@ -7,6 +7,17 @@ export enum SyncStatus {
7
7
  ERROR = -1
8
8
  }
9
9
 
10
+ export enum DetailedStatus {
11
+ IDLE = "IDLE",
12
+ CHECKING = "CHECKING",
13
+ UPDATE_AVAILABLE = "UPDATE_AVAILABLE",
14
+ DOWNLOADING = "DOWNLOADING",
15
+ VERIFYING = "VERIFYING",
16
+ INSTALLING = "INSTALLING",
17
+ COMPLETED = "COMPLETED",
18
+ ERROR = "ERROR"
19
+ }
20
+
10
21
  export enum CheckFrequency {
11
22
  ON_APP_START = "ON_APP_START",
12
23
  ON_APP_RESUME = "ON_APP_RESUME",
@@ -18,12 +29,31 @@ export enum InstallMode {
18
29
  ON_NEXT_RESTART = "ON_NEXT_RESTART"
19
30
  }
20
31
 
32
+ /**
33
+ * Thông tin tiến độ tải về bản cập nhật
34
+ */
35
+ export interface DownloadProgress {
36
+ /** Tổng số byte cần tải */
37
+ totalBytes: number;
38
+ /** Số byte đã tải về máy */
39
+ receivedBytes: number;
40
+ /** Tỷ lệ hoàn thành từ 0 đến 100 (%) */
41
+ percentage: number;
42
+ /** Giá trị tỷ lệ từ 0.0 đến 1.0 (tương thích CodePush / Progress Bar) */
43
+ progress: number;
44
+ }
45
+
46
+ export type DownloadProgressCallback = (progress: DownloadProgress) => void;
47
+ export type DetailedStatusCallback = (status: DetailedStatus, message?: string) => void;
48
+
21
49
  export interface UpdateDialogOptions {
22
50
  title?: string;
23
51
  optionalUpdateMessage?: string;
24
52
  mandatoryUpdateMessage?: string;
25
53
  optionalInstallButtonLabel?: string;
26
54
  optionalIgnoreButtonLabel?: string;
55
+ /** Hiển thị modal tiến độ tải sau khi người dùng bấm cập nhật */
56
+ showProgressModal?: boolean;
27
57
  }
28
58
 
29
59
  export interface SyncOptions {
@@ -35,6 +65,8 @@ export interface SyncOptions {
35
65
  installMode?: InstallMode;
36
66
  mandatoryInstallMode?: InstallMode;
37
67
  updateDialog?: boolean | UpdateDialogOptions;
68
+ /** Tự động hiển thị modal tiến độ tải cập nhật */
69
+ showProgressModal?: boolean;
38
70
  checkFrequency?: CheckFrequency;
39
71
  }
40
72