react-native-custom-ota 1.0.2 → 1.0.4

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.
package/README.md CHANGED
@@ -7,6 +7,7 @@ Thư viện cập nhật ứng dụng từ xa qua mạng (Over-The-Air - OTA) t
7
7
  ## 🌟 Tính năng nổi bật
8
8
 
9
9
  - 🔄 **Đồng bộ 1 chạm (CodePush-like API)**: Hỗ trợ `CustomOTA.sync()` và HOC `withCustomOTA(App)`.
10
+ - 📊 **Theo dõi tiến độ tải thời gian thực**: Cung cấp component `<OTAProgressModal />`, Hook `useCustomOTA()`, hiển thị chi tiết % và dung lượng byte (`received / total`).
10
11
  - 🛡️ **Cơ chế Rollback an toàn**: Nếu bản cập nhật bị crash ở tầng JS trước khi boot thành công, Native Resolver sẽ tự động phát hiện và khôi phục về bản backup ổn định trước đó.
11
12
  - 📦 **Tích hợp Native cực kỳ đơn giản**: Chỉ cần thêm 1 dòng code ở Android và 1 dòng ở iOS (Hỗ trợ React Native Autolinking).
12
13
  - 🛠️ **Tích hợp CLI phát hành nhanh**: `npx custom-ota release` tự động chạy Metro, nén zip và upload lên server chỉ trong 1-2 giây.
package/bin/custom-ota.js CHANGED
@@ -161,6 +161,7 @@ async function buildAndPublish(p) {
161
161
  console.log(`📡 [3/3] Đang upload lên server: ${serverUrl} (App: ${appId})...`);
162
162
  const zipBuffer = fs.readFileSync(zipPath);
163
163
  manifest.size = zipBuffer.length;
164
+ manifest.sha256 = crypto.createHash("sha256").update(zipBuffer).digest("hex");
164
165
 
165
166
  const url = new URL(`/admin/publish/${appId}/${p}/${version}`, serverUrl);
166
167
  const payload = Buffer.from(
@@ -0,0 +1,163 @@
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.OTAProgressModal = void 0;
7
+ exports.formatBytes = formatBytes;
8
+ const react_1 = __importDefault(require("react"));
9
+ const react_native_1 = require("react-native");
10
+ function formatBytes(bytes, decimals = 1) {
11
+ if (!bytes || bytes <= 0)
12
+ return "0 B";
13
+ const k = 1024;
14
+ const dm = decimals < 0 ? 0 : decimals;
15
+ const sizes = ["B", "KB", "MB", "GB"];
16
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
17
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
18
+ }
19
+ const OTAProgressModal = ({ visible, progress, title = "Cập nhật ứng dụng", statusText = "Đang tải bản cập nhật...", version }) => {
20
+ // Chuẩn hóa dữ liệu tiến độ
21
+ let percent = 0;
22
+ let receivedBytes = 0;
23
+ let totalBytes = 0;
24
+ if (typeof progress === "number") {
25
+ percent = Math.min(100, Math.max(0, Math.round(progress * 100)));
26
+ }
27
+ else if (progress && typeof progress === "object") {
28
+ percent = Math.min(100, Math.max(0, Math.round(progress.percentage || 0)));
29
+ receivedBytes = progress.receivedBytes || 0;
30
+ totalBytes = progress.totalBytes || 0;
31
+ }
32
+ const bytesInfo = totalBytes > 0
33
+ ? `${formatBytes(receivedBytes)} / ${formatBytes(totalBytes)}`
34
+ : "";
35
+ return (react_1.default.createElement(react_native_1.Modal, { transparent: true, animationType: "fade", visible: visible, statusBarTranslucent: true },
36
+ react_1.default.createElement(react_native_1.View, { style: styles.overlay },
37
+ react_1.default.createElement(react_native_1.View, { style: styles.card },
38
+ react_1.default.createElement(react_native_1.View, { style: styles.header },
39
+ react_1.default.createElement(react_native_1.View, { style: styles.iconContainer },
40
+ react_1.default.createElement(react_native_1.Text, { style: styles.iconText }, "\u26A1")),
41
+ react_1.default.createElement(react_native_1.View, { style: styles.headerTextContainer },
42
+ react_1.default.createElement(react_native_1.Text, { style: styles.title }, title),
43
+ version ? (react_1.default.createElement(react_native_1.Text, { style: styles.versionBadge },
44
+ "Phi\u00EAn b\u1EA3n v",
45
+ version)) : null)),
46
+ react_1.default.createElement(react_native_1.Text, { style: styles.statusText }, statusText),
47
+ react_1.default.createElement(react_native_1.View, { style: styles.progressBarTrack },
48
+ react_1.default.createElement(react_native_1.View, { style: [styles.progressBarFill, { width: `${percent}%` }] })),
49
+ react_1.default.createElement(react_native_1.View, { style: styles.detailsRow },
50
+ react_1.default.createElement(react_native_1.Text, { style: styles.percentageText },
51
+ percent,
52
+ "%"),
53
+ bytesInfo ? (react_1.default.createElement(react_native_1.Text, { style: styles.bytesText }, bytesInfo)) : null),
54
+ react_1.default.createElement(react_native_1.View, { style: styles.footer },
55
+ react_1.default.createElement(react_native_1.ActivityIndicator, { size: "small", color: "#38bdf8", style: { marginRight: 8 } }),
56
+ react_1.default.createElement(react_native_1.Text, { style: styles.footerText }, "Vui l\u00F2ng kh\u00F4ng \u0111\u00F3ng \u1EE9ng d\u1EE5ng..."))))));
57
+ };
58
+ exports.OTAProgressModal = OTAProgressModal;
59
+ const styles = react_native_1.StyleSheet.create({
60
+ overlay: {
61
+ flex: 1,
62
+ backgroundColor: "rgba(0, 0, 0, 0.72)",
63
+ justifyContent: "center",
64
+ alignItems: "center",
65
+ paddingHorizontal: 24
66
+ },
67
+ card: {
68
+ width: "100%",
69
+ maxWidth: 380,
70
+ backgroundColor: "#0f172a",
71
+ borderRadius: 20,
72
+ padding: 24,
73
+ borderWidth: 1,
74
+ borderColor: "rgba(255, 255, 255, 0.12)",
75
+ shadowColor: "#000",
76
+ shadowOffset: { width: 0, height: 12 },
77
+ shadowOpacity: 0.5,
78
+ shadowRadius: 20,
79
+ elevation: 16
80
+ },
81
+ header: {
82
+ flexDirection: "row",
83
+ alignItems: "center",
84
+ marginBottom: 16
85
+ },
86
+ iconContainer: {
87
+ width: 44,
88
+ height: 44,
89
+ borderRadius: 12,
90
+ backgroundColor: "rgba(56, 189, 248, 0.15)",
91
+ justifyContent: "center",
92
+ alignItems: "center",
93
+ marginRight: 14,
94
+ borderWidth: 1,
95
+ borderColor: "rgba(56, 189, 248, 0.3)"
96
+ },
97
+ iconText: {
98
+ fontSize: 22
99
+ },
100
+ headerTextContainer: {
101
+ flex: 1
102
+ },
103
+ title: {
104
+ fontSize: 18,
105
+ fontWeight: "700",
106
+ color: "#f8fafc",
107
+ letterSpacing: 0.2
108
+ },
109
+ versionBadge: {
110
+ fontSize: 12,
111
+ color: "#38bdf8",
112
+ fontWeight: "600",
113
+ marginTop: 2
114
+ },
115
+ statusText: {
116
+ fontSize: 14,
117
+ color: "#94a3b8",
118
+ marginBottom: 16,
119
+ lineHeight: 20
120
+ },
121
+ progressBarTrack: {
122
+ height: 10,
123
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
124
+ borderRadius: 999,
125
+ overflow: "hidden",
126
+ marginBottom: 10
127
+ },
128
+ progressBarFill: {
129
+ height: "100%",
130
+ backgroundColor: "#38bdf8",
131
+ borderRadius: 999
132
+ },
133
+ detailsRow: {
134
+ flexDirection: "row",
135
+ justifyContent: "space-between",
136
+ alignItems: "center",
137
+ marginBottom: 20
138
+ },
139
+ percentageText: {
140
+ fontSize: 16,
141
+ fontWeight: "800",
142
+ color: "#38bdf8"
143
+ },
144
+ bytesText: {
145
+ fontSize: 13,
146
+ color: "#64748b",
147
+ fontWeight: "500"
148
+ },
149
+ footer: {
150
+ flexDirection: "row",
151
+ alignItems: "center",
152
+ justifyContent: "center",
153
+ paddingTop: 12,
154
+ borderTopWidth: 1,
155
+ borderTopColor: "rgba(255, 255, 255, 0.06)"
156
+ },
157
+ footerText: {
158
+ fontSize: 12,
159
+ color: "#64748b",
160
+ fontWeight: "500"
161
+ }
162
+ });
163
+ exports.default = exports.OTAProgressModal;
@@ -38,10 +38,39 @@ const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
39
  const manager_1 = require("./manager");
40
40
  const types_1 = require("./types");
41
+ const ProgressModal_1 = require("./ProgressModal");
41
42
  function withCustomOTA(options) {
42
43
  return function (WrappedComponent) {
43
44
  return function CustomOTAWrapper(props) {
45
+ const [isDownloading, setIsDownloading] = (0, react_1.useState)(false);
46
+ const [progress, setProgress] = (0, react_1.useState)({
47
+ totalBytes: 0,
48
+ receivedBytes: 0,
49
+ percentage: 0,
50
+ progress: 0
51
+ });
52
+ const [statusText, setStatusText] = (0, react_1.useState)("Đang tải bản cập nhật...");
44
53
  (0, react_1.useEffect)(() => {
54
+ let removeProg;
55
+ let removeStatus;
56
+ if (options.showProgressModal) {
57
+ removeProg = manager_1.CustomOTAManager.addProgressListener((p) => {
58
+ setProgress(p);
59
+ setIsDownloading(true);
60
+ });
61
+ removeStatus = manager_1.CustomOTAManager.addDetailedStatusListener((s, msg) => {
62
+ if (msg)
63
+ setStatusText(msg);
64
+ if (s === types_1.DetailedStatus.DOWNLOADING ||
65
+ s === types_1.DetailedStatus.VERIFYING ||
66
+ s === types_1.DetailedStatus.INSTALLING) {
67
+ setIsDownloading(true);
68
+ }
69
+ else if (s === types_1.DetailedStatus.COMPLETED || s === types_1.DetailedStatus.ERROR) {
70
+ setIsDownloading(false);
71
+ }
72
+ });
73
+ }
45
74
  // Tự động kiểm tra cập nhật khi app khởi động
46
75
  if (options.checkFrequency !== types_1.CheckFrequency.MANUAL) {
47
76
  manager_1.CustomOTAManager.sync(options);
@@ -55,10 +84,19 @@ function withCustomOTA(options) {
55
84
  });
56
85
  return () => {
57
86
  subscription.remove();
87
+ removeProg?.();
88
+ removeStatus?.();
58
89
  };
59
90
  }
91
+ return () => {
92
+ removeProg?.();
93
+ removeStatus?.();
94
+ };
60
95
  }, []);
61
- return react_1.default.createElement(WrappedComponent, { ...props });
96
+ return (react_1.default.createElement(react_native_1.View, { style: { flex: 1 } },
97
+ react_1.default.createElement(WrappedComponent, { ...props }),
98
+ options.showProgressModal && (react_1.default.createElement(ProgressModal_1.OTAProgressModal, { visible: isDownloading, progress: progress, statusText: statusText }))));
62
99
  };
63
100
  };
64
101
  }
102
+ exports.default = withCustomOTA;
@@ -16,10 +16,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  const manager_1 = require("./manager");
18
18
  const hoc_1 = require("./hoc");
19
+ const ProgressModal_1 = require("./ProgressModal");
20
+ const useCustomOTA_1 = require("./useCustomOTA");
19
21
  __exportStar(require("./types"), exports);
20
22
  __exportStar(require("./manager"), exports);
21
23
  __exportStar(require("./hoc"), exports);
22
24
  __exportStar(require("./storage"), exports);
25
+ __exportStar(require("./ProgressModal"), exports);
26
+ __exportStar(require("./useCustomOTA"), exports);
23
27
  const CustomOTA = {
24
28
  sync: manager_1.CustomOTAManager.sync.bind(manager_1.CustomOTAManager),
25
29
  checkForUpdate: manager_1.CustomOTAManager.checkForUpdate.bind(manager_1.CustomOTAManager),
@@ -27,6 +31,10 @@ const CustomOTA = {
27
31
  restartApp: manager_1.CustomOTAManager.restartApp.bind(manager_1.CustomOTAManager),
28
32
  getUpdateMetadata: manager_1.CustomOTAManager.getUpdateMetadata.bind(manager_1.CustomOTAManager),
29
33
  markReady: manager_1.CustomOTAManager.markReady.bind(manager_1.CustomOTAManager),
30
- wrap: hoc_1.withCustomOTA
34
+ addProgressListener: manager_1.CustomOTAManager.addProgressListener.bind(manager_1.CustomOTAManager),
35
+ addDetailedStatusListener: manager_1.CustomOTAManager.addDetailedStatusListener.bind(manager_1.CustomOTAManager),
36
+ wrap: hoc_1.withCustomOTA,
37
+ ProgressModal: ProgressModal_1.OTAProgressModal,
38
+ useCustomOTA: useCustomOTA_1.useCustomOTA
31
39
  };
32
40
  exports.default = CustomOTA;
@@ -10,6 +10,46 @@ const native_1 = require("./native");
10
10
  const storage_1 = require("./storage");
11
11
  const types_1 = require("./types");
12
12
  class CustomOTAManager {
13
+ /**
14
+ * Đăng ký lắng nghe tiến độ tải về toàn cục
15
+ * @returns Hàm hủy đăng ký (unsubscribe)
16
+ */
17
+ static addProgressListener(listener) {
18
+ this.progressListeners.add(listener);
19
+ return () => {
20
+ this.progressListeners.delete(listener);
21
+ };
22
+ }
23
+ /**
24
+ * Đăng ký lắng nghe trạng thái chi tiết của quá trình OTA
25
+ * @returns Hàm hủy đăng ký (unsubscribe)
26
+ */
27
+ static addDetailedStatusListener(listener) {
28
+ this.detailedStatusListeners.add(listener);
29
+ return () => {
30
+ this.detailedStatusListeners.delete(listener);
31
+ };
32
+ }
33
+ static emitProgress(progress) {
34
+ for (const listener of this.progressListeners) {
35
+ try {
36
+ listener(progress);
37
+ }
38
+ catch (e) {
39
+ console.warn("[CustomOTA] Error in progress listener:", e);
40
+ }
41
+ }
42
+ }
43
+ static emitDetailedStatus(status, message) {
44
+ for (const listener of this.detailedStatusListeners) {
45
+ try {
46
+ listener(status, message);
47
+ }
48
+ catch (e) {
49
+ console.warn("[CustomOTA] Error in detailed status listener:", e);
50
+ }
51
+ }
52
+ }
13
53
  /**
14
54
  * Đánh dấu bản build OTA hiện tại đã khởi động thành công.
15
55
  * Xóa bản backup trước đó và tắt cờ pending.
@@ -49,7 +89,7 @@ class CustomOTAManager {
49
89
  return (0, storage_1.readState)();
50
90
  }
51
91
  /**
52
- * Kiểm tra xem server có bản cập nhật mới hay không.
92
+ * Kiểm tra xem có bản cập nhật mới từ OTA Server hay không.
53
93
  */
54
94
  static async checkForUpdate(options) {
55
95
  await (0, storage_1.ensureOTADirs)();
@@ -109,7 +149,7 @@ class CustomOTAManager {
109
149
  return data;
110
150
  }
111
151
  /**
112
- * Tải và cài đặt bản cập nhật.
152
+ * Tải và cài đặt bản cập nhật với theo dõi tiến độ chi tiết.
113
153
  */
114
154
  static async downloadAndInstall(update, onProgress, installMode) {
115
155
  const version = update.otaVersion || update.version;
@@ -121,30 +161,51 @@ class CustomOTAManager {
121
161
  ].filter(Boolean).join(", ");
122
162
  throw new Error(`Invalid update package metadata: missing ${missingFields}`);
123
163
  }
164
+ this.emitDetailedStatus(types_1.DetailedStatus.DOWNLOADING, `Bắt đầu tải bản cập nhật v${version}...`);
124
165
  const zipPath = `${storage_1.TEMP_DIR}/update-${version}.zip`;
125
166
  await react_native_fs_1.default.unlink(zipPath).catch(() => { });
126
- // Download zip file
167
+ // Download zip file với theo dõi byte chính xác
127
168
  const downloadRes = react_native_fs_1.default.downloadFile({
128
169
  fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
129
170
  toFile: zipPath,
130
171
  progressDivider: 1,
131
172
  progress: ({ bytesWritten, contentLength }) => {
132
- if (contentLength > 0 && onProgress) {
133
- onProgress(bytesWritten / contentLength);
173
+ const total = contentLength > 0 ? contentLength : (update.size || 0);
174
+ const percent = total > 0 ? Math.min(100, Math.round((bytesWritten / total) * 100)) : 0;
175
+ const progressVal = total > 0 ? Math.min(1, bytesWritten / total) : 0;
176
+ const progressObj = {
177
+ totalBytes: total,
178
+ receivedBytes: bytesWritten,
179
+ percentage: percent,
180
+ progress: progressVal
181
+ };
182
+ if (onProgress) {
183
+ // Callback nhận cả object lẫn hỗ trợ kiểu cũ nhận số
184
+ try {
185
+ onProgress(progressObj, progressVal);
186
+ }
187
+ catch {
188
+ onProgress(progressVal);
189
+ }
134
190
  }
191
+ this.emitProgress(progressObj);
135
192
  }
136
193
  });
137
194
  const res = await downloadRes.promise;
138
195
  if (res.statusCode < 200 || res.statusCode >= 300) {
196
+ this.emitDetailedStatus(types_1.DetailedStatus.ERROR, `Tải thất bại với mã lỗi HTTP ${res.statusCode}`);
139
197
  throw new Error(`Download failed with status ${res.statusCode}`);
140
198
  }
141
199
  // SHA-256 verification
200
+ this.emitDetailedStatus(types_1.DetailedStatus.VERIFYING, "Đang kiểm tra tính toàn vẹn (SHA-256)...");
142
201
  const actualHash = await react_native_fs_1.default.hash(zipPath, "sha256");
143
202
  if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
144
203
  await react_native_fs_1.default.unlink(zipPath).catch(() => { });
204
+ this.emitDetailedStatus(types_1.DetailedStatus.ERROR, "Mã băm SHA-256 không trùng khớp!");
145
205
  throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
146
206
  }
147
207
  // Unpack temp directory
208
+ this.emitDetailedStatus(types_1.DetailedStatus.INSTALLING, "Đang giải nén và nạp gói cập nhật...");
148
209
  const tempDir = `${storage_1.TEMP_DIR}/${version}`;
149
210
  await react_native_fs_1.default.unlink(tempDir).catch(() => { });
150
211
  await react_native_fs_1.default.mkdir(tempDir);
@@ -171,6 +232,7 @@ class CustomOTAManager {
171
232
  failedVersions: oldState.failedVersions || []
172
233
  });
173
234
  await react_native_fs_1.default.unlink(zipPath).catch(() => { });
235
+ this.emitDetailedStatus(types_1.DetailedStatus.COMPLETED, `Đã cài đặt bản cập nhật v${version} thành công!`);
174
236
  if (installMode === types_1.InstallMode.IMMEDIATE) {
175
237
  await this.restartApp();
176
238
  }
@@ -258,5 +320,8 @@ CustomOTAManager.defaultDialog = {
258
320
  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
321
  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
322
  optionalInstallButtonLabel: "Cập nhật",
261
- optionalIgnoreButtonLabel: "Để sau"
323
+ optionalIgnoreButtonLabel: "Để sau",
324
+ showProgressModal: true
262
325
  };
326
+ CustomOTAManager.progressListeners = new Set();
327
+ CustomOTAManager.detailedStatusListeners = new Set();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.InstallMode = exports.CheckFrequency = exports.SyncStatus = void 0;
3
+ exports.InstallMode = exports.CheckFrequency = exports.DetailedStatus = exports.SyncStatus = void 0;
4
4
  var SyncStatus;
5
5
  (function (SyncStatus) {
6
6
  SyncStatus[SyncStatus["UP_TO_DATE"] = 0] = "UP_TO_DATE";
@@ -10,6 +10,17 @@ var SyncStatus;
10
10
  SyncStatus[SyncStatus["UPDATE_INSTALLED"] = 4] = "UPDATE_INSTALLED";
11
11
  SyncStatus[SyncStatus["ERROR"] = -1] = "ERROR";
12
12
  })(SyncStatus || (exports.SyncStatus = SyncStatus = {}));
13
+ var DetailedStatus;
14
+ (function (DetailedStatus) {
15
+ DetailedStatus["IDLE"] = "IDLE";
16
+ DetailedStatus["CHECKING"] = "CHECKING";
17
+ DetailedStatus["UPDATE_AVAILABLE"] = "UPDATE_AVAILABLE";
18
+ DetailedStatus["DOWNLOADING"] = "DOWNLOADING";
19
+ DetailedStatus["VERIFYING"] = "VERIFYING";
20
+ DetailedStatus["INSTALLING"] = "INSTALLING";
21
+ DetailedStatus["COMPLETED"] = "COMPLETED";
22
+ DetailedStatus["ERROR"] = "ERROR";
23
+ })(DetailedStatus || (exports.DetailedStatus = DetailedStatus = {}));
13
24
  var CheckFrequency;
14
25
  (function (CheckFrequency) {
15
26
  CheckFrequency["ON_APP_START"] = "ON_APP_START";
@@ -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;