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 +1 -0
- package/bin/custom-ota.js +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
|
@@ -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/typescript/hoc.d.ts
CHANGED
package/lib/typescript/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;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { CustomOTAManager } from "./manager";
|
|
2
2
|
import { withCustomOTA } from "./hoc";
|
|
3
|
+
import { useCustomOTA } from "./useCustomOTA";
|
|
3
4
|
export * from "./types";
|
|
4
5
|
export * from "./manager";
|
|
5
6
|
export * from "./hoc";
|
|
6
7
|
export * from "./storage";
|
|
8
|
+
export * from "./ProgressModal";
|
|
9
|
+
export * from "./useCustomOTA";
|
|
7
10
|
declare const CustomOTA: {
|
|
8
11
|
sync: typeof CustomOTAManager.sync;
|
|
9
12
|
checkForUpdate: typeof CustomOTAManager.checkForUpdate;
|
|
@@ -11,6 +14,10 @@ declare const CustomOTA: {
|
|
|
11
14
|
restartApp: typeof CustomOTAManager.restartApp;
|
|
12
15
|
getUpdateMetadata: typeof CustomOTAManager.getUpdateMetadata;
|
|
13
16
|
markReady: typeof CustomOTAManager.markReady;
|
|
17
|
+
addProgressListener: typeof CustomOTAManager.addProgressListener;
|
|
18
|
+
addDetailedStatusListener: typeof CustomOTAManager.addDetailedStatusListener;
|
|
14
19
|
wrap: typeof withCustomOTA;
|
|
20
|
+
ProgressModal: import("@types/react").FC<import("./ProgressModal").OTAProgressModalProps>;
|
|
21
|
+
useCustomOTA: typeof useCustomOTA;
|
|
15
22
|
};
|
|
16
23
|
export default CustomOTA;
|
package/lib/typescript/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;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { InstallMode, LocalPackage, RemotePackage, SyncOptions, SyncStatus } from "./types";
|
|
1
|
+
import { DownloadProgress, InstallMode, LocalPackage, RemotePackage, SyncOptions, SyncStatus, DetailedStatus } from "./types";
|
|
2
2
|
export declare class CustomOTAManager {
|
|
3
3
|
private static defaultDialog;
|
|
4
|
+
private static progressListeners;
|
|
5
|
+
private static detailedStatusListeners;
|
|
6
|
+
/**
|
|
7
|
+
* Đăng ký lắng nghe tiến độ tải về toàn cục
|
|
8
|
+
* @returns Hàm hủy đăng ký (unsubscribe)
|
|
9
|
+
*/
|
|
10
|
+
static addProgressListener(listener: (progress: DownloadProgress) => void): () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Đăng ký lắng nghe trạng thái chi tiết của quá trình OTA
|
|
13
|
+
* @returns Hàm hủy đăng ký (unsubscribe)
|
|
14
|
+
*/
|
|
15
|
+
static addDetailedStatusListener(listener: (status: DetailedStatus, message?: string) => void): () => void;
|
|
16
|
+
private static emitProgress;
|
|
17
|
+
private static emitDetailedStatus;
|
|
4
18
|
/**
|
|
5
19
|
* Đánh dấu bản build OTA hiện tại đã khởi động thành công.
|
|
6
20
|
* Xóa bản backup trước đó và tắt cờ pending.
|
|
@@ -15,7 +29,7 @@ export declare class CustomOTAManager {
|
|
|
15
29
|
*/
|
|
16
30
|
static getUpdateMetadata(): Promise<LocalPackage>;
|
|
17
31
|
/**
|
|
18
|
-
* Kiểm tra xem
|
|
32
|
+
* Kiểm tra xem có bản cập nhật mới từ OTA Server hay không.
|
|
19
33
|
*/
|
|
20
34
|
static checkForUpdate(options: {
|
|
21
35
|
serverUrl: string;
|
|
@@ -25,11 +39,11 @@ export declare class CustomOTAManager {
|
|
|
25
39
|
channel?: string;
|
|
26
40
|
}): Promise<RemotePackage | null>;
|
|
27
41
|
/**
|
|
28
|
-
* Tải và cài đặt bản cập nhật.
|
|
42
|
+
* Tải và cài đặt bản cập nhật với theo dõi tiến độ chi tiết.
|
|
29
43
|
*/
|
|
30
|
-
static downloadAndInstall(update: RemotePackage, onProgress?: (progress: number) => void, installMode?: InstallMode): Promise<boolean>;
|
|
44
|
+
static downloadAndInstall(update: RemotePackage, onProgress?: ((progress: DownloadProgress) => void) | ((progressValue: number) => void), installMode?: InstallMode): Promise<boolean>;
|
|
31
45
|
/**
|
|
32
46
|
* Phương thức đồng bộ tự động như CodePush.sync()
|
|
33
47
|
*/
|
|
34
|
-
static sync(options: SyncOptions, syncStatusCallback?: (status: SyncStatus) => void, downloadProgressCallback?: (progress: number) => void): Promise<SyncStatus>;
|
|
48
|
+
static sync(options: SyncOptions, syncStatusCallback?: (status: SyncStatus) => void, downloadProgressCallback?: ((progress: DownloadProgress) => void) | ((progressValue: number) => void)): Promise<SyncStatus>;
|
|
35
49
|
}
|
|
@@ -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();
|
|
@@ -6,6 +6,16 @@ export declare enum SyncStatus {
|
|
|
6
6
|
UPDATE_INSTALLED = 4,
|
|
7
7
|
ERROR = -1
|
|
8
8
|
}
|
|
9
|
+
export declare enum DetailedStatus {
|
|
10
|
+
IDLE = "IDLE",
|
|
11
|
+
CHECKING = "CHECKING",
|
|
12
|
+
UPDATE_AVAILABLE = "UPDATE_AVAILABLE",
|
|
13
|
+
DOWNLOADING = "DOWNLOADING",
|
|
14
|
+
VERIFYING = "VERIFYING",
|
|
15
|
+
INSTALLING = "INSTALLING",
|
|
16
|
+
COMPLETED = "COMPLETED",
|
|
17
|
+
ERROR = "ERROR"
|
|
18
|
+
}
|
|
9
19
|
export declare enum CheckFrequency {
|
|
10
20
|
ON_APP_START = "ON_APP_START",
|
|
11
21
|
ON_APP_RESUME = "ON_APP_RESUME",
|
|
@@ -15,12 +25,29 @@ export declare enum InstallMode {
|
|
|
15
25
|
IMMEDIATE = "IMMEDIATE",
|
|
16
26
|
ON_NEXT_RESTART = "ON_NEXT_RESTART"
|
|
17
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Thông tin tiến độ tải về bản cập nhật
|
|
30
|
+
*/
|
|
31
|
+
export interface DownloadProgress {
|
|
32
|
+
/** Tổng số byte cần tải */
|
|
33
|
+
totalBytes: number;
|
|
34
|
+
/** Số byte đã tải về máy */
|
|
35
|
+
receivedBytes: number;
|
|
36
|
+
/** Tỷ lệ hoàn thành từ 0 đến 100 (%) */
|
|
37
|
+
percentage: number;
|
|
38
|
+
/** Giá trị tỷ lệ từ 0.0 đến 1.0 (tương thích CodePush / Progress Bar) */
|
|
39
|
+
progress: number;
|
|
40
|
+
}
|
|
41
|
+
export type DownloadProgressCallback = (progress: DownloadProgress) => void;
|
|
42
|
+
export type DetailedStatusCallback = (status: DetailedStatus, message?: string) => void;
|
|
18
43
|
export interface UpdateDialogOptions {
|
|
19
44
|
title?: string;
|
|
20
45
|
optionalUpdateMessage?: string;
|
|
21
46
|
mandatoryUpdateMessage?: string;
|
|
22
47
|
optionalInstallButtonLabel?: string;
|
|
23
48
|
optionalIgnoreButtonLabel?: string;
|
|
49
|
+
/** Hiển thị modal tiến độ tải sau khi người dùng bấm cập nhật */
|
|
50
|
+
showProgressModal?: boolean;
|
|
24
51
|
}
|
|
25
52
|
export interface SyncOptions {
|
|
26
53
|
serverUrl: string;
|
|
@@ -31,6 +58,8 @@ export interface SyncOptions {
|
|
|
31
58
|
installMode?: InstallMode;
|
|
32
59
|
mandatoryInstallMode?: InstallMode;
|
|
33
60
|
updateDialog?: boolean | UpdateDialogOptions;
|
|
61
|
+
/** Tự động hiển thị modal tiến độ tải cập nhật */
|
|
62
|
+
showProgressModal?: boolean;
|
|
34
63
|
checkFrequency?: CheckFrequency;
|
|
35
64
|
}
|
|
36
65
|
export interface RemotePackage {
|
package/lib/typescript/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,35 @@
|
|
|
1
|
+
import { DownloadProgress, LocalPackage, RemotePackage, SyncOptions, SyncStatus, DetailedStatus } from "./types";
|
|
2
|
+
export interface UseCustomOTAReturn {
|
|
3
|
+
/** Đang kiểm tra cập nhật */
|
|
4
|
+
isChecking: boolean;
|
|
5
|
+
/** Đang trong tiến trình tải xuống hoặc cài đặt */
|
|
6
|
+
isDownloading: boolean;
|
|
7
|
+
/** Thông tin tiến độ tải chi tiết */
|
|
8
|
+
progress: DownloadProgress;
|
|
9
|
+
/** Trạng thái tổng quát của Sync */
|
|
10
|
+
syncStatus: SyncStatus;
|
|
11
|
+
/** Trạng thái chi tiết (IDLE, CHECKING, DOWNLOADING, VERIFYING, INSTALLING, COMPLETED, ERROR) */
|
|
12
|
+
detailedStatus: DetailedStatus;
|
|
13
|
+
/** Thông điệp trạng thái bằng chữ (tiếng Việt thân thiện) */
|
|
14
|
+
statusText: string;
|
|
15
|
+
/** Thông tin phiên bản OTA hiện tại của máy */
|
|
16
|
+
activeMetadata: LocalPackage | null;
|
|
17
|
+
/** Phiên bản active hiện tại (vd: '1.0.8') */
|
|
18
|
+
activeVersion: string;
|
|
19
|
+
/** Kiểm tra bản cập nhật mới từ server */
|
|
20
|
+
checkForUpdate: (options: {
|
|
21
|
+
serverUrl: string;
|
|
22
|
+
appId?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
nativeVersion?: string;
|
|
25
|
+
channel?: string;
|
|
26
|
+
}) => Promise<RemotePackage | null>;
|
|
27
|
+
/** Thực hiện đồng bộ tự động */
|
|
28
|
+
sync: (options: SyncOptions) => Promise<SyncStatus>;
|
|
29
|
+
/** Khởi động lại ứng dụng để áp dụng bản OTA */
|
|
30
|
+
restartApp: () => Promise<void>;
|
|
31
|
+
/** Làm mới thông tin metadata phiên bản hiện tại */
|
|
32
|
+
refreshMetadata: () => Promise<LocalPackage>;
|
|
33
|
+
}
|
|
34
|
+
export declare function useCustomOTA(): UseCustomOTAReturn;
|
|
35
|
+
export default useCustomOTA;
|