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.
- package/README.md +1 -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
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.
|
|
@@ -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;
|
package/lib/commonjs/hoc.js
CHANGED
|
@@ -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(
|
|
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;
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/commonjs/manager.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
133
|
-
|
|
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();
|
package/lib/commonjs/types.js
CHANGED
|
@@ -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;
|