react-native-custom-ota 1.0.1 → 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.
- package/README.md +1 -0
- package/ios/CustomOTABundleResolver.h +13 -0
- package/lib/commonjs/ProgressModal.js +163 -0
- package/lib/commonjs/hoc.js +39 -1
- package/lib/commonjs/index.js +9 -1
- package/lib/commonjs/manager.js +71 -6
- package/lib/commonjs/types.js +12 -1
- package/lib/commonjs/useCustomOTA.js +119 -0
- package/lib/module/ProgressModal.js +155 -0
- package/lib/module/hoc.js +42 -4
- package/lib/module/index.js +9 -1
- package/lib/module/manager.js +72 -7
- package/lib/module/types.js +11 -0
- package/lib/module/useCustomOTA.js +116 -0
- package/lib/typescript/ProgressModal.d.ts +17 -0
- package/lib/typescript/ProgressModal.js +163 -0
- package/lib/typescript/hoc.d.ts +1 -0
- package/lib/typescript/hoc.js +39 -1
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.js +9 -1
- package/lib/typescript/manager.d.ts +19 -5
- package/lib/typescript/manager.js +71 -6
- package/lib/typescript/types.d.ts +29 -0
- package/lib/typescript/types.js +12 -1
- package/lib/typescript/useCustomOTA.d.ts +35 -0
- package/lib/typescript/useCustomOTA.js +119 -0
- package/package.json +1 -1
- package/src/ProgressModal.tsx +214 -0
- package/src/hoc.tsx +56 -4
- package/src/index.ts +9 -1
- package/src/manager.ts +83 -9
- package/src/types.ts +32 -0
- package/src/useCustomOTA.ts +177 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Modal, StyleSheet, Text, View, ActivityIndicator } from "react-native";
|
|
3
|
+
export function formatBytes(bytes, decimals = 1) {
|
|
4
|
+
if (!bytes || bytes <= 0)
|
|
5
|
+
return "0 B";
|
|
6
|
+
const k = 1024;
|
|
7
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
8
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
9
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
10
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
11
|
+
}
|
|
12
|
+
export const OTAProgressModal = ({ visible, progress, title = "Cập nhật ứng dụng", statusText = "Đang tải bản cập nhật...", version }) => {
|
|
13
|
+
// Chuẩn hóa dữ liệu tiến độ
|
|
14
|
+
let percent = 0;
|
|
15
|
+
let receivedBytes = 0;
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
if (typeof progress === "number") {
|
|
18
|
+
percent = Math.min(100, Math.max(0, Math.round(progress * 100)));
|
|
19
|
+
}
|
|
20
|
+
else if (progress && typeof progress === "object") {
|
|
21
|
+
percent = Math.min(100, Math.max(0, Math.round(progress.percentage || 0)));
|
|
22
|
+
receivedBytes = progress.receivedBytes || 0;
|
|
23
|
+
totalBytes = progress.totalBytes || 0;
|
|
24
|
+
}
|
|
25
|
+
const bytesInfo = totalBytes > 0
|
|
26
|
+
? `${formatBytes(receivedBytes)} / ${formatBytes(totalBytes)}`
|
|
27
|
+
: "";
|
|
28
|
+
return (React.createElement(Modal, { transparent: true, animationType: "fade", visible: visible, statusBarTranslucent: true },
|
|
29
|
+
React.createElement(View, { style: styles.overlay },
|
|
30
|
+
React.createElement(View, { style: styles.card },
|
|
31
|
+
React.createElement(View, { style: styles.header },
|
|
32
|
+
React.createElement(View, { style: styles.iconContainer },
|
|
33
|
+
React.createElement(Text, { style: styles.iconText }, "\u26A1")),
|
|
34
|
+
React.createElement(View, { style: styles.headerTextContainer },
|
|
35
|
+
React.createElement(Text, { style: styles.title }, title),
|
|
36
|
+
version ? (React.createElement(Text, { style: styles.versionBadge },
|
|
37
|
+
"Phi\u00EAn b\u1EA3n v",
|
|
38
|
+
version)) : null)),
|
|
39
|
+
React.createElement(Text, { style: styles.statusText }, statusText),
|
|
40
|
+
React.createElement(View, { style: styles.progressBarTrack },
|
|
41
|
+
React.createElement(View, { style: [styles.progressBarFill, { width: `${percent}%` }] })),
|
|
42
|
+
React.createElement(View, { style: styles.detailsRow },
|
|
43
|
+
React.createElement(Text, { style: styles.percentageText },
|
|
44
|
+
percent,
|
|
45
|
+
"%"),
|
|
46
|
+
bytesInfo ? (React.createElement(Text, { style: styles.bytesText }, bytesInfo)) : null),
|
|
47
|
+
React.createElement(View, { style: styles.footer },
|
|
48
|
+
React.createElement(ActivityIndicator, { size: "small", color: "#38bdf8", style: { marginRight: 8 } }),
|
|
49
|
+
React.createElement(Text, { style: styles.footerText }, "Vui l\u00F2ng kh\u00F4ng \u0111\u00F3ng \u1EE9ng d\u1EE5ng..."))))));
|
|
50
|
+
};
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
overlay: {
|
|
53
|
+
flex: 1,
|
|
54
|
+
backgroundColor: "rgba(0, 0, 0, 0.72)",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
paddingHorizontal: 24
|
|
58
|
+
},
|
|
59
|
+
card: {
|
|
60
|
+
width: "100%",
|
|
61
|
+
maxWidth: 380,
|
|
62
|
+
backgroundColor: "#0f172a",
|
|
63
|
+
borderRadius: 20,
|
|
64
|
+
padding: 24,
|
|
65
|
+
borderWidth: 1,
|
|
66
|
+
borderColor: "rgba(255, 255, 255, 0.12)",
|
|
67
|
+
shadowColor: "#000",
|
|
68
|
+
shadowOffset: { width: 0, height: 12 },
|
|
69
|
+
shadowOpacity: 0.5,
|
|
70
|
+
shadowRadius: 20,
|
|
71
|
+
elevation: 16
|
|
72
|
+
},
|
|
73
|
+
header: {
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
alignItems: "center",
|
|
76
|
+
marginBottom: 16
|
|
77
|
+
},
|
|
78
|
+
iconContainer: {
|
|
79
|
+
width: 44,
|
|
80
|
+
height: 44,
|
|
81
|
+
borderRadius: 12,
|
|
82
|
+
backgroundColor: "rgba(56, 189, 248, 0.15)",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
alignItems: "center",
|
|
85
|
+
marginRight: 14,
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: "rgba(56, 189, 248, 0.3)"
|
|
88
|
+
},
|
|
89
|
+
iconText: {
|
|
90
|
+
fontSize: 22
|
|
91
|
+
},
|
|
92
|
+
headerTextContainer: {
|
|
93
|
+
flex: 1
|
|
94
|
+
},
|
|
95
|
+
title: {
|
|
96
|
+
fontSize: 18,
|
|
97
|
+
fontWeight: "700",
|
|
98
|
+
color: "#f8fafc",
|
|
99
|
+
letterSpacing: 0.2
|
|
100
|
+
},
|
|
101
|
+
versionBadge: {
|
|
102
|
+
fontSize: 12,
|
|
103
|
+
color: "#38bdf8",
|
|
104
|
+
fontWeight: "600",
|
|
105
|
+
marginTop: 2
|
|
106
|
+
},
|
|
107
|
+
statusText: {
|
|
108
|
+
fontSize: 14,
|
|
109
|
+
color: "#94a3b8",
|
|
110
|
+
marginBottom: 16,
|
|
111
|
+
lineHeight: 20
|
|
112
|
+
},
|
|
113
|
+
progressBarTrack: {
|
|
114
|
+
height: 10,
|
|
115
|
+
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
|
116
|
+
borderRadius: 999,
|
|
117
|
+
overflow: "hidden",
|
|
118
|
+
marginBottom: 10
|
|
119
|
+
},
|
|
120
|
+
progressBarFill: {
|
|
121
|
+
height: "100%",
|
|
122
|
+
backgroundColor: "#38bdf8",
|
|
123
|
+
borderRadius: 999
|
|
124
|
+
},
|
|
125
|
+
detailsRow: {
|
|
126
|
+
flexDirection: "row",
|
|
127
|
+
justifyContent: "space-between",
|
|
128
|
+
alignItems: "center",
|
|
129
|
+
marginBottom: 20
|
|
130
|
+
},
|
|
131
|
+
percentageText: {
|
|
132
|
+
fontSize: 16,
|
|
133
|
+
fontWeight: "800",
|
|
134
|
+
color: "#38bdf8"
|
|
135
|
+
},
|
|
136
|
+
bytesText: {
|
|
137
|
+
fontSize: 13,
|
|
138
|
+
color: "#64748b",
|
|
139
|
+
fontWeight: "500"
|
|
140
|
+
},
|
|
141
|
+
footer: {
|
|
142
|
+
flexDirection: "row",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
justifyContent: "center",
|
|
145
|
+
paddingTop: 12,
|
|
146
|
+
borderTopWidth: 1,
|
|
147
|
+
borderTopColor: "rgba(255, 255, 255, 0.06)"
|
|
148
|
+
},
|
|
149
|
+
footerText: {
|
|
150
|
+
fontSize: 12,
|
|
151
|
+
color: "#64748b",
|
|
152
|
+
fontWeight: "500"
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
export default OTAProgressModal;
|
package/lib/module/hoc.js
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
2
|
-
import { AppState } from "react-native";
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { AppState, View } from "react-native";
|
|
3
3
|
import { CustomOTAManager } from "./manager";
|
|
4
|
-
import { CheckFrequency } from "./types";
|
|
4
|
+
import { CheckFrequency, DetailedStatus } from "./types";
|
|
5
|
+
import { OTAProgressModal } from "./ProgressModal";
|
|
5
6
|
export function withCustomOTA(options) {
|
|
6
7
|
return function (WrappedComponent) {
|
|
7
8
|
return function CustomOTAWrapper(props) {
|
|
9
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
10
|
+
const [progress, setProgress] = useState({
|
|
11
|
+
totalBytes: 0,
|
|
12
|
+
receivedBytes: 0,
|
|
13
|
+
percentage: 0,
|
|
14
|
+
progress: 0
|
|
15
|
+
});
|
|
16
|
+
const [statusText, setStatusText] = useState("Đang tải bản cập nhật...");
|
|
8
17
|
useEffect(() => {
|
|
18
|
+
let removeProg;
|
|
19
|
+
let removeStatus;
|
|
20
|
+
if (options.showProgressModal) {
|
|
21
|
+
removeProg = CustomOTAManager.addProgressListener((p) => {
|
|
22
|
+
setProgress(p);
|
|
23
|
+
setIsDownloading(true);
|
|
24
|
+
});
|
|
25
|
+
removeStatus = CustomOTAManager.addDetailedStatusListener((s, msg) => {
|
|
26
|
+
if (msg)
|
|
27
|
+
setStatusText(msg);
|
|
28
|
+
if (s === DetailedStatus.DOWNLOADING ||
|
|
29
|
+
s === DetailedStatus.VERIFYING ||
|
|
30
|
+
s === DetailedStatus.INSTALLING) {
|
|
31
|
+
setIsDownloading(true);
|
|
32
|
+
}
|
|
33
|
+
else if (s === DetailedStatus.COMPLETED || s === DetailedStatus.ERROR) {
|
|
34
|
+
setIsDownloading(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
9
38
|
// Tự động kiểm tra cập nhật khi app khởi động
|
|
10
39
|
if (options.checkFrequency !== CheckFrequency.MANUAL) {
|
|
11
40
|
CustomOTAManager.sync(options);
|
|
@@ -19,10 +48,19 @@ export function withCustomOTA(options) {
|
|
|
19
48
|
});
|
|
20
49
|
return () => {
|
|
21
50
|
subscription.remove();
|
|
51
|
+
removeProg?.();
|
|
52
|
+
removeStatus?.();
|
|
22
53
|
};
|
|
23
54
|
}
|
|
55
|
+
return () => {
|
|
56
|
+
removeProg?.();
|
|
57
|
+
removeStatus?.();
|
|
58
|
+
};
|
|
24
59
|
}, []);
|
|
25
|
-
return React.createElement(
|
|
60
|
+
return (React.createElement(View, { style: { flex: 1 } },
|
|
61
|
+
React.createElement(WrappedComponent, { ...props }),
|
|
62
|
+
options.showProgressModal && (React.createElement(OTAProgressModal, { visible: isDownloading, progress: progress, statusText: statusText }))));
|
|
26
63
|
};
|
|
27
64
|
};
|
|
28
65
|
}
|
|
66
|
+
export default withCustomOTA;
|
package/lib/module/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { CustomOTAManager } from "./manager";
|
|
2
2
|
import { withCustomOTA } from "./hoc";
|
|
3
|
+
import { OTAProgressModal } from "./ProgressModal";
|
|
4
|
+
import { useCustomOTA } from "./useCustomOTA";
|
|
3
5
|
export * from "./types";
|
|
4
6
|
export * from "./manager";
|
|
5
7
|
export * from "./hoc";
|
|
6
8
|
export * from "./storage";
|
|
9
|
+
export * from "./ProgressModal";
|
|
10
|
+
export * from "./useCustomOTA";
|
|
7
11
|
const CustomOTA = {
|
|
8
12
|
sync: CustomOTAManager.sync.bind(CustomOTAManager),
|
|
9
13
|
checkForUpdate: CustomOTAManager.checkForUpdate.bind(CustomOTAManager),
|
|
@@ -11,6 +15,10 @@ const CustomOTA = {
|
|
|
11
15
|
restartApp: CustomOTAManager.restartApp.bind(CustomOTAManager),
|
|
12
16
|
getUpdateMetadata: CustomOTAManager.getUpdateMetadata.bind(CustomOTAManager),
|
|
13
17
|
markReady: CustomOTAManager.markReady.bind(CustomOTAManager),
|
|
14
|
-
|
|
18
|
+
addProgressListener: CustomOTAManager.addProgressListener.bind(CustomOTAManager),
|
|
19
|
+
addDetailedStatusListener: CustomOTAManager.addDetailedStatusListener.bind(CustomOTAManager),
|
|
20
|
+
wrap: withCustomOTA,
|
|
21
|
+
ProgressModal: OTAProgressModal,
|
|
22
|
+
useCustomOTA: useCustomOTA
|
|
15
23
|
};
|
|
16
24
|
export default CustomOTA;
|
package/lib/module/manager.js
CHANGED
|
@@ -2,8 +2,48 @@ import { Alert, Platform } from "react-native";
|
|
|
2
2
|
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
|
-
import { InstallMode, SyncStatus } from "./types";
|
|
5
|
+
import { InstallMode, SyncStatus, DetailedStatus } from "./types";
|
|
6
6
|
export class CustomOTAManager {
|
|
7
|
+
/**
|
|
8
|
+
* Đăng ký lắng nghe tiến độ tải về toàn cục
|
|
9
|
+
* @returns Hàm hủy đăng ký (unsubscribe)
|
|
10
|
+
*/
|
|
11
|
+
static addProgressListener(listener) {
|
|
12
|
+
this.progressListeners.add(listener);
|
|
13
|
+
return () => {
|
|
14
|
+
this.progressListeners.delete(listener);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Đăng ký lắng nghe trạng thái chi tiết của quá trình OTA
|
|
19
|
+
* @returns Hàm hủy đăng ký (unsubscribe)
|
|
20
|
+
*/
|
|
21
|
+
static addDetailedStatusListener(listener) {
|
|
22
|
+
this.detailedStatusListeners.add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
this.detailedStatusListeners.delete(listener);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
static emitProgress(progress) {
|
|
28
|
+
for (const listener of this.progressListeners) {
|
|
29
|
+
try {
|
|
30
|
+
listener(progress);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.warn("[CustomOTA] Error in progress listener:", e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
static emitDetailedStatus(status, message) {
|
|
38
|
+
for (const listener of this.detailedStatusListeners) {
|
|
39
|
+
try {
|
|
40
|
+
listener(status, message);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
console.warn("[CustomOTA] Error in detailed status listener:", e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
7
47
|
/**
|
|
8
48
|
* Đánh dấu bản build OTA hiện tại đã khởi động thành công.
|
|
9
49
|
* Xóa bản backup trước đó và tắt cờ pending.
|
|
@@ -43,7 +83,7 @@ export class CustomOTAManager {
|
|
|
43
83
|
return readState();
|
|
44
84
|
}
|
|
45
85
|
/**
|
|
46
|
-
* Kiểm tra xem
|
|
86
|
+
* Kiểm tra xem có bản cập nhật mới từ OTA Server hay không.
|
|
47
87
|
*/
|
|
48
88
|
static async checkForUpdate(options) {
|
|
49
89
|
await ensureOTADirs();
|
|
@@ -103,7 +143,7 @@ export class CustomOTAManager {
|
|
|
103
143
|
return data;
|
|
104
144
|
}
|
|
105
145
|
/**
|
|
106
|
-
* Tải và cài đặt bản cập nhật.
|
|
146
|
+
* Tải và cài đặt bản cập nhật với theo dõi tiến độ chi tiết.
|
|
107
147
|
*/
|
|
108
148
|
static async downloadAndInstall(update, onProgress, installMode) {
|
|
109
149
|
const version = update.otaVersion || update.version;
|
|
@@ -115,30 +155,51 @@ export class CustomOTAManager {
|
|
|
115
155
|
].filter(Boolean).join(", ");
|
|
116
156
|
throw new Error(`Invalid update package metadata: missing ${missingFields}`);
|
|
117
157
|
}
|
|
158
|
+
this.emitDetailedStatus(DetailedStatus.DOWNLOADING, `Bắt đầu tải bản cập nhật v${version}...`);
|
|
118
159
|
const zipPath = `${TEMP_DIR}/update-${version}.zip`;
|
|
119
160
|
await RNFS.unlink(zipPath).catch(() => { });
|
|
120
|
-
// Download zip file
|
|
161
|
+
// Download zip file với theo dõi byte chính xác
|
|
121
162
|
const downloadRes = RNFS.downloadFile({
|
|
122
163
|
fromUrl: update.downloadUrl.startsWith("http") ? update.downloadUrl : `${update.downloadUrl}`,
|
|
123
164
|
toFile: zipPath,
|
|
124
165
|
progressDivider: 1,
|
|
125
166
|
progress: ({ bytesWritten, contentLength }) => {
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
const total = contentLength > 0 ? contentLength : (update.size || 0);
|
|
168
|
+
const percent = total > 0 ? Math.min(100, Math.round((bytesWritten / total) * 100)) : 0;
|
|
169
|
+
const progressVal = total > 0 ? Math.min(1, bytesWritten / total) : 0;
|
|
170
|
+
const progressObj = {
|
|
171
|
+
totalBytes: total,
|
|
172
|
+
receivedBytes: bytesWritten,
|
|
173
|
+
percentage: percent,
|
|
174
|
+
progress: progressVal
|
|
175
|
+
};
|
|
176
|
+
if (onProgress) {
|
|
177
|
+
// Callback nhận cả object lẫn hỗ trợ kiểu cũ nhận số
|
|
178
|
+
try {
|
|
179
|
+
onProgress(progressObj, progressVal);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
onProgress(progressVal);
|
|
183
|
+
}
|
|
128
184
|
}
|
|
185
|
+
this.emitProgress(progressObj);
|
|
129
186
|
}
|
|
130
187
|
});
|
|
131
188
|
const res = await downloadRes.promise;
|
|
132
189
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
190
|
+
this.emitDetailedStatus(DetailedStatus.ERROR, `Tải thất bại với mã lỗi HTTP ${res.statusCode}`);
|
|
133
191
|
throw new Error(`Download failed with status ${res.statusCode}`);
|
|
134
192
|
}
|
|
135
193
|
// SHA-256 verification
|
|
194
|
+
this.emitDetailedStatus(DetailedStatus.VERIFYING, "Đang kiểm tra tính toàn vẹn (SHA-256)...");
|
|
136
195
|
const actualHash = await RNFS.hash(zipPath, "sha256");
|
|
137
196
|
if (actualHash.toLowerCase() !== update.sha256.toLowerCase()) {
|
|
138
197
|
await RNFS.unlink(zipPath).catch(() => { });
|
|
198
|
+
this.emitDetailedStatus(DetailedStatus.ERROR, "Mã băm SHA-256 không trùng khớp!");
|
|
139
199
|
throw new Error(`SHA-256 Checksum mismatch! Expected ${update.sha256}, got ${actualHash}`);
|
|
140
200
|
}
|
|
141
201
|
// Unpack temp directory
|
|
202
|
+
this.emitDetailedStatus(DetailedStatus.INSTALLING, "Đang giải nén và nạp gói cập nhật...");
|
|
142
203
|
const tempDir = `${TEMP_DIR}/${version}`;
|
|
143
204
|
await RNFS.unlink(tempDir).catch(() => { });
|
|
144
205
|
await RNFS.mkdir(tempDir);
|
|
@@ -165,6 +226,7 @@ export class CustomOTAManager {
|
|
|
165
226
|
failedVersions: oldState.failedVersions || []
|
|
166
227
|
});
|
|
167
228
|
await RNFS.unlink(zipPath).catch(() => { });
|
|
229
|
+
this.emitDetailedStatus(DetailedStatus.COMPLETED, `Đã cài đặt bản cập nhật v${version} thành công!`);
|
|
168
230
|
if (installMode === InstallMode.IMMEDIATE) {
|
|
169
231
|
await this.restartApp();
|
|
170
232
|
}
|
|
@@ -251,5 +313,8 @@ CustomOTAManager.defaultDialog = {
|
|
|
251
313
|
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
314
|
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
315
|
optionalInstallButtonLabel: "Cập nhật",
|
|
254
|
-
optionalIgnoreButtonLabel: "Để sau"
|
|
316
|
+
optionalIgnoreButtonLabel: "Để sau",
|
|
317
|
+
showProgressModal: true
|
|
255
318
|
};
|
|
319
|
+
CustomOTAManager.progressListeners = new Set();
|
|
320
|
+
CustomOTAManager.detailedStatusListeners = new Set();
|
package/lib/module/types.js
CHANGED
|
@@ -7,6 +7,17 @@ export var SyncStatus;
|
|
|
7
7
|
SyncStatus[SyncStatus["UPDATE_INSTALLED"] = 4] = "UPDATE_INSTALLED";
|
|
8
8
|
SyncStatus[SyncStatus["ERROR"] = -1] = "ERROR";
|
|
9
9
|
})(SyncStatus || (SyncStatus = {}));
|
|
10
|
+
export var DetailedStatus;
|
|
11
|
+
(function (DetailedStatus) {
|
|
12
|
+
DetailedStatus["IDLE"] = "IDLE";
|
|
13
|
+
DetailedStatus["CHECKING"] = "CHECKING";
|
|
14
|
+
DetailedStatus["UPDATE_AVAILABLE"] = "UPDATE_AVAILABLE";
|
|
15
|
+
DetailedStatus["DOWNLOADING"] = "DOWNLOADING";
|
|
16
|
+
DetailedStatus["VERIFYING"] = "VERIFYING";
|
|
17
|
+
DetailedStatus["INSTALLING"] = "INSTALLING";
|
|
18
|
+
DetailedStatus["COMPLETED"] = "COMPLETED";
|
|
19
|
+
DetailedStatus["ERROR"] = "ERROR";
|
|
20
|
+
})(DetailedStatus || (DetailedStatus = {}));
|
|
10
21
|
export var CheckFrequency;
|
|
11
22
|
(function (CheckFrequency) {
|
|
12
23
|
CheckFrequency["ON_APP_START"] = "ON_APP_START";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { CustomOTAManager } from "./manager";
|
|
3
|
+
import { SyncStatus, DetailedStatus } from "./types";
|
|
4
|
+
export function useCustomOTA() {
|
|
5
|
+
const [isChecking, setIsChecking] = useState(false);
|
|
6
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
7
|
+
const [progress, setProgress] = useState({
|
|
8
|
+
totalBytes: 0,
|
|
9
|
+
receivedBytes: 0,
|
|
10
|
+
percentage: 0,
|
|
11
|
+
progress: 0
|
|
12
|
+
});
|
|
13
|
+
const [syncStatus, setSyncStatus] = useState(SyncStatus.UP_TO_DATE);
|
|
14
|
+
const [detailedStatus, setDetailedStatus] = useState(DetailedStatus.IDLE);
|
|
15
|
+
const [statusText, setStatusText] = useState("");
|
|
16
|
+
const [activeMetadata, setActiveMetadata] = useState(null);
|
|
17
|
+
const refreshMetadata = useCallback(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const meta = await CustomOTAManager.getUpdateMetadata();
|
|
20
|
+
setActiveMetadata(meta);
|
|
21
|
+
return meta;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {
|
|
25
|
+
currentVersion: "0.0.0",
|
|
26
|
+
previousVersion: null,
|
|
27
|
+
pending: false,
|
|
28
|
+
ready: true,
|
|
29
|
+
failedVersions: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
refreshMetadata();
|
|
35
|
+
// Đăng ký nhận sự kiện tiến độ toàn cục
|
|
36
|
+
const removeProgressListener = CustomOTAManager.addProgressListener((prog) => {
|
|
37
|
+
setProgress(prog);
|
|
38
|
+
setIsDownloading(true);
|
|
39
|
+
setDetailedStatus(DetailedStatus.DOWNLOADING);
|
|
40
|
+
setStatusText(`Đang tải bản cập nhật: ${prog.percentage}%`);
|
|
41
|
+
});
|
|
42
|
+
const removeStatusListener = CustomOTAManager.addDetailedStatusListener((status, msg) => {
|
|
43
|
+
setDetailedStatus(status);
|
|
44
|
+
if (msg)
|
|
45
|
+
setStatusText(msg);
|
|
46
|
+
if (status === DetailedStatus.DOWNLOADING) {
|
|
47
|
+
setIsDownloading(true);
|
|
48
|
+
}
|
|
49
|
+
else if (status === DetailedStatus.COMPLETED || status === DetailedStatus.ERROR) {
|
|
50
|
+
setIsDownloading(false);
|
|
51
|
+
refreshMetadata();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
removeProgressListener();
|
|
56
|
+
removeStatusListener();
|
|
57
|
+
};
|
|
58
|
+
}, [refreshMetadata]);
|
|
59
|
+
const checkForUpdate = useCallback(async (options) => {
|
|
60
|
+
setIsChecking(true);
|
|
61
|
+
setDetailedStatus(DetailedStatus.CHECKING);
|
|
62
|
+
setStatusText("Đang kiểm tra phiên bản mới...");
|
|
63
|
+
try {
|
|
64
|
+
const update = await CustomOTAManager.checkForUpdate(options);
|
|
65
|
+
if (update && update.updateAvailable) {
|
|
66
|
+
setDetailedStatus(DetailedStatus.UPDATE_AVAILABLE);
|
|
67
|
+
setStatusText(`Tìm thấy bản cập nhật mới v${update.otaVersion}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
setDetailedStatus(DetailedStatus.IDLE);
|
|
71
|
+
setStatusText("Ứng dụng đang ở phiên bản mới nhất");
|
|
72
|
+
}
|
|
73
|
+
return update;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
setDetailedStatus(DetailedStatus.ERROR);
|
|
77
|
+
setStatusText(err.message || "Lỗi khi kiểm tra cập nhật");
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
setIsChecking(false);
|
|
82
|
+
}
|
|
83
|
+
}, []);
|
|
84
|
+
const sync = useCallback(async (options) => {
|
|
85
|
+
setIsChecking(true);
|
|
86
|
+
setProgress({ totalBytes: 0, receivedBytes: 0, percentage: 0, progress: 0 });
|
|
87
|
+
try {
|
|
88
|
+
const status = await CustomOTAManager.sync(options, (s) => setSyncStatus(s), (prog) => setProgress(prog));
|
|
89
|
+
setSyncStatus(status);
|
|
90
|
+
await refreshMetadata();
|
|
91
|
+
return status;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
setIsChecking(false);
|
|
95
|
+
setIsDownloading(false);
|
|
96
|
+
}
|
|
97
|
+
}, [refreshMetadata]);
|
|
98
|
+
const restartApp = useCallback(async () => {
|
|
99
|
+
await CustomOTAManager.restartApp();
|
|
100
|
+
}, []);
|
|
101
|
+
return {
|
|
102
|
+
isChecking,
|
|
103
|
+
isDownloading,
|
|
104
|
+
progress,
|
|
105
|
+
syncStatus,
|
|
106
|
+
detailedStatus,
|
|
107
|
+
statusText,
|
|
108
|
+
activeMetadata,
|
|
109
|
+
activeVersion: activeMetadata?.currentVersion || "0.0.0",
|
|
110
|
+
checkForUpdate,
|
|
111
|
+
sync,
|
|
112
|
+
restartApp,
|
|
113
|
+
refreshMetadata
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export default useCustomOTA;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DownloadProgress } from "./types";
|
|
3
|
+
export interface OTAProgressModalProps {
|
|
4
|
+
/** Trạng thái hiển thị modal */
|
|
5
|
+
visible: boolean;
|
|
6
|
+
/** Thông tin tiến độ tải (object hoặc số thập phân 0.0 - 1.0) */
|
|
7
|
+
progress?: DownloadProgress | number;
|
|
8
|
+
/** Tiêu đề modal (Mặc định: 'Cập nhật ứng dụng') */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Thông điệp trạng thái hiện tại (vd: 'Đang tải gói cập nhật...') */
|
|
11
|
+
statusText?: string;
|
|
12
|
+
/** Phiên bản đang được tải về (vd: '1.0.9') */
|
|
13
|
+
version?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function formatBytes(bytes: number, decimals?: number): string;
|
|
16
|
+
export declare const OTAProgressModal: React.FC<OTAProgressModalProps>;
|
|
17
|
+
export default OTAProgressModal;
|