@hot-updater/react-native 0.30.11 → 0.31.0
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/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +18 -4
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +136 -0
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +133 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +18 -4
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +135 -0
- package/lib/module/DefaultResolver.spec.js.map +1 -0
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +133 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts +4 -3
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +37 -9
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +48 -32
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts +4 -3
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +37 -9
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +1 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +48 -32
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +17 -9
- package/src/DefaultResolver.spec.ts +178 -0
- package/src/DefaultResolver.ts +32 -6
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +170 -2
- package/src/index.ts +100 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/types.ts +2 -0
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +84 -45
package/src/store.ts
CHANGED
|
@@ -1,18 +1,88 @@
|
|
|
1
1
|
import useSyncExternalStoreExports from "use-sync-external-store/shim/with-selector";
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
HotUpdaterDiffFileSnapshot,
|
|
5
|
+
HotUpdaterDiffProgressDetails,
|
|
6
|
+
HotUpdaterProgressEvent,
|
|
7
|
+
} from "./native";
|
|
3
8
|
import { addListener } from "./native";
|
|
4
9
|
|
|
5
10
|
export type HotUpdaterState = {
|
|
6
11
|
progress: number;
|
|
12
|
+
downloadedBytes: number | undefined;
|
|
13
|
+
totalBytes: number | undefined;
|
|
7
14
|
isUpdateDownloaded: boolean;
|
|
15
|
+
artifactType: "archive" | "diff" | null;
|
|
16
|
+
details: HotUpdaterDiffProgressDetails | null;
|
|
8
17
|
};
|
|
9
18
|
|
|
10
19
|
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
|
|
11
20
|
|
|
21
|
+
const areDiffDetailsEqual = (
|
|
22
|
+
left: HotUpdaterDiffProgressDetails | null,
|
|
23
|
+
right: HotUpdaterDiffProgressDetails | null,
|
|
24
|
+
) => {
|
|
25
|
+
if (left === right) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (left === null || right === null) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
left.totalFilesCount !== right.totalFilesCount ||
|
|
35
|
+
left.completedFilesCount !== right.completedFilesCount ||
|
|
36
|
+
left.files.length !== right.files.length
|
|
37
|
+
) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return left.files.every((leftFile, index) => {
|
|
42
|
+
const rightFile = right.files[index];
|
|
43
|
+
return (
|
|
44
|
+
rightFile !== undefined &&
|
|
45
|
+
leftFile.order === rightFile.order &&
|
|
46
|
+
leftFile.path === rightFile.path &&
|
|
47
|
+
leftFile.downloadPath === rightFile.downloadPath &&
|
|
48
|
+
leftFile.downloadedBytes === rightFile.downloadedBytes &&
|
|
49
|
+
leftFile.progress === rightFile.progress &&
|
|
50
|
+
leftFile.status === rightFile.status &&
|
|
51
|
+
leftFile.totalBytes === rightFile.totalBytes
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const areStatesEqual = (left: HotUpdaterState, right: HotUpdaterState) => {
|
|
57
|
+
return (
|
|
58
|
+
left.progress === right.progress &&
|
|
59
|
+
left.downloadedBytes === right.downloadedBytes &&
|
|
60
|
+
left.totalBytes === right.totalBytes &&
|
|
61
|
+
left.isUpdateDownloaded === right.isUpdateDownloaded &&
|
|
62
|
+
left.artifactType === right.artifactType &&
|
|
63
|
+
areDiffDetailsEqual(left.details, right.details)
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const normalizeByteCount = (value: number | undefined) => {
|
|
68
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return Math.round(value);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const normalizeDownloadPath = (value: string | undefined) => {
|
|
75
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
12
78
|
const createHotUpdaterStore = () => {
|
|
13
79
|
let state: HotUpdaterState = {
|
|
14
80
|
progress: 0,
|
|
81
|
+
downloadedBytes: undefined,
|
|
82
|
+
totalBytes: undefined,
|
|
15
83
|
isUpdateDownloaded: false,
|
|
84
|
+
artifactType: null,
|
|
85
|
+
details: null,
|
|
16
86
|
};
|
|
17
87
|
|
|
18
88
|
const getSnapshot = () => {
|
|
@@ -27,8 +97,39 @@ const createHotUpdaterStore = () => {
|
|
|
27
97
|
}
|
|
28
98
|
};
|
|
29
99
|
|
|
100
|
+
const normalizeDiffDetails = (
|
|
101
|
+
details: HotUpdaterDiffProgressDetails,
|
|
102
|
+
): HotUpdaterDiffProgressDetails => {
|
|
103
|
+
const totalFilesCount = Math.max(0, details.totalFilesCount);
|
|
104
|
+
const normalizedFiles: HotUpdaterDiffFileSnapshot[] = details.files
|
|
105
|
+
.map((file) => {
|
|
106
|
+
const downloadPath =
|
|
107
|
+
normalizeDownloadPath(file.downloadPath) ?? file.path;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
downloadPath,
|
|
111
|
+
downloadedBytes: normalizeByteCount(file.downloadedBytes),
|
|
112
|
+
order: Math.max(0, file.order),
|
|
113
|
+
path: file.path,
|
|
114
|
+
progress: Math.max(0, Math.min(file.progress, 1)),
|
|
115
|
+
status: file.status,
|
|
116
|
+
totalBytes: normalizeByteCount(file.totalBytes),
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.sort((left, right) => left.order - right.order);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
completedFilesCount: Math.max(
|
|
123
|
+
0,
|
|
124
|
+
Math.min(details.completedFilesCount, totalFilesCount),
|
|
125
|
+
),
|
|
126
|
+
files: normalizedFiles,
|
|
127
|
+
totalFilesCount,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
30
131
|
const setState = (newState: Partial<HotUpdaterState>) => {
|
|
31
|
-
// Merge first, then normalize derived fields
|
|
132
|
+
// Merge first, then normalize derived fields.
|
|
32
133
|
const nextState: HotUpdaterState = {
|
|
33
134
|
...state,
|
|
34
135
|
...newState,
|
|
@@ -38,7 +139,8 @@ const createHotUpdaterStore = () => {
|
|
|
38
139
|
// If `progress` is not provided but `isUpdateDownloaded` is,
|
|
39
140
|
// honor the explicit value.
|
|
40
141
|
if ("progress" in newState && typeof newState.progress === "number") {
|
|
41
|
-
nextState.
|
|
142
|
+
nextState.progress = Math.max(0, Math.min(newState.progress, 1));
|
|
143
|
+
nextState.isUpdateDownloaded = nextState.progress >= 1;
|
|
42
144
|
} else if (
|
|
43
145
|
"isUpdateDownloaded" in newState &&
|
|
44
146
|
typeof newState.isUpdateDownloaded === "boolean"
|
|
@@ -46,10 +148,15 @@ const createHotUpdaterStore = () => {
|
|
|
46
148
|
nextState.isUpdateDownloaded = newState.isUpdateDownloaded;
|
|
47
149
|
}
|
|
48
150
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
151
|
+
if (newState.details === undefined) {
|
|
152
|
+
nextState.details = state.details;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (nextState.artifactType !== "diff") {
|
|
156
|
+
nextState.details = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (areStatesEqual(state, nextState)) {
|
|
53
160
|
return;
|
|
54
161
|
}
|
|
55
162
|
|
|
@@ -57,6 +164,25 @@ const createHotUpdaterStore = () => {
|
|
|
57
164
|
emitChange();
|
|
58
165
|
};
|
|
59
166
|
|
|
167
|
+
const applyProgressEvent = (event: HotUpdaterProgressEvent) => {
|
|
168
|
+
setState({
|
|
169
|
+
artifactType: event.artifactType,
|
|
170
|
+
details:
|
|
171
|
+
event.artifactType === "diff"
|
|
172
|
+
? normalizeDiffDetails(event.details)
|
|
173
|
+
: null,
|
|
174
|
+
downloadedBytes:
|
|
175
|
+
event.artifactType === "archive"
|
|
176
|
+
? normalizeByteCount(event.downloadedBytes)
|
|
177
|
+
: undefined,
|
|
178
|
+
progress: event.progress,
|
|
179
|
+
totalBytes:
|
|
180
|
+
event.artifactType === "archive"
|
|
181
|
+
? normalizeByteCount(event.totalBytes)
|
|
182
|
+
: undefined,
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
60
186
|
const subscribe = (listener: () => void) => {
|
|
61
187
|
listeners.add(listener);
|
|
62
188
|
return () => listeners.delete(listener);
|
|
@@ -64,7 +190,7 @@ const createHotUpdaterStore = () => {
|
|
|
64
190
|
|
|
65
191
|
// Subscribe to native onProgress events
|
|
66
192
|
// This listener is registered once when the store is created
|
|
67
|
-
addListener("onProgress",
|
|
193
|
+
addListener("onProgress", applyProgressEvent);
|
|
68
194
|
|
|
69
195
|
return { getSnapshot, setState, subscribe };
|
|
70
196
|
};
|
package/src/types.ts
CHANGED
package/src/wrap.spec.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
|
|
4
|
+
import type { HotUpdaterOptions } from "./wrap";
|
|
5
|
+
|
|
3
6
|
const mocks = vi.hoisted(() => ({
|
|
4
7
|
addListener: vi.fn(() => () => {}),
|
|
5
8
|
checkForUpdate: vi.fn(),
|
|
@@ -78,4 +81,92 @@ describe("HotUpdater wrap initialization", () => {
|
|
|
78
81
|
requestTimeout: 1000,
|
|
79
82
|
});
|
|
80
83
|
});
|
|
84
|
+
|
|
85
|
+
it("calls init onError when app-ready notification fails", async () => {
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
|
|
88
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
89
|
+
const error = new Error("notify failed");
|
|
90
|
+
const onError = vi.fn();
|
|
91
|
+
const requestAnimationFrame = vi.fn(
|
|
92
|
+
(callback: (timestamp: number) => void) => {
|
|
93
|
+
setTimeout(() => callback(0), 0);
|
|
94
|
+
return 1;
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
|
|
98
|
+
const resolver = {
|
|
99
|
+
checkUpdate: vi.fn(),
|
|
100
|
+
notifyAppReady: vi.fn().mockRejectedValue(error),
|
|
101
|
+
};
|
|
102
|
+
const { init } = await import("./wrap");
|
|
103
|
+
|
|
104
|
+
init({
|
|
105
|
+
resolver,
|
|
106
|
+
onError,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await vi.runOnlyPendingTimersAsync();
|
|
110
|
+
|
|
111
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
112
|
+
warn.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("warns when the deprecated manual wrap HOC is used", async () => {
|
|
116
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
const { wrap } = await import("./wrap");
|
|
118
|
+
|
|
119
|
+
wrap({
|
|
120
|
+
resolver: {
|
|
121
|
+
checkUpdate: vi.fn(),
|
|
122
|
+
notifyAppReady: vi.fn(),
|
|
123
|
+
},
|
|
124
|
+
updateMode: "manual",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(warn).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringContaining(
|
|
129
|
+
'HotUpdater.wrap({ updateMode: "manual" }) is deprecated',
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
warn.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("preserves wrapped component prop inference", async () => {
|
|
136
|
+
const { wrap } = await import("./wrap");
|
|
137
|
+
const Component: React.ComponentType<{ title: string }> = () => null;
|
|
138
|
+
|
|
139
|
+
const WrappedComponent = wrap({
|
|
140
|
+
resolver: {
|
|
141
|
+
checkUpdate: vi.fn(),
|
|
142
|
+
notifyAppReady: vi.fn(),
|
|
143
|
+
},
|
|
144
|
+
updateMode: "auto",
|
|
145
|
+
updateStrategy: "appVersion",
|
|
146
|
+
})(Component);
|
|
147
|
+
|
|
148
|
+
const acceptsTitleProps: React.ComponentType<{ title: string }> =
|
|
149
|
+
WrappedComponent;
|
|
150
|
+
expect(acceptsTitleProps).toBe(WrappedComponent);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("types public wrap options as automatic mode by default", () => {
|
|
154
|
+
const autoOptions = {
|
|
155
|
+
baseURL: "https://updates.example.com",
|
|
156
|
+
updateStrategy: "appVersion",
|
|
157
|
+
} satisfies HotUpdaterOptions;
|
|
158
|
+
const explicitAutoOptions = {
|
|
159
|
+
baseURL: "https://updates.example.com",
|
|
160
|
+
updateMode: "auto",
|
|
161
|
+
updateStrategy: "appVersion",
|
|
162
|
+
} satisfies HotUpdaterOptions;
|
|
163
|
+
const manualOptions = {
|
|
164
|
+
baseURL: "https://updates.example.com",
|
|
165
|
+
updateMode: "manual",
|
|
166
|
+
} satisfies HotUpdaterOptions;
|
|
167
|
+
|
|
168
|
+
expect(autoOptions.updateStrategy).toBe("appVersion");
|
|
169
|
+
expect(explicitAutoOptions.updateMode).toBe("auto");
|
|
170
|
+
expect(manualOptions.updateMode).toBe("manual");
|
|
171
|
+
});
|
|
81
172
|
});
|
package/src/wrap.tsx
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
notifyAppReady as nativeNotifyAppReady,
|
|
10
10
|
reload,
|
|
11
11
|
} from "./native";
|
|
12
|
-
import { useHotUpdaterStore } from "./store";
|
|
13
|
-
import type { HotUpdaterResolver } from "./types";
|
|
12
|
+
import { type HotUpdaterState, useHotUpdaterStore } from "./store";
|
|
13
|
+
import type { HotUpdaterBaseURL, HotUpdaterResolver } from "./types";
|
|
14
14
|
|
|
15
15
|
export interface RunUpdateProcessResponse {
|
|
16
16
|
status: "ROLLBACK" | "UPDATE" | "UP_TO_DATE";
|
|
@@ -24,8 +24,18 @@ type UpdateStatus =
|
|
|
24
24
|
| "UPDATING"
|
|
25
25
|
| "UPDATE_PROCESS_COMPLETED";
|
|
26
26
|
|
|
27
|
+
export type HotUpdaterFallbackComponentProps = {
|
|
28
|
+
status: Exclude<UpdateStatus, "UPDATE_PROCESS_COMPLETED">;
|
|
29
|
+
progress: number;
|
|
30
|
+
downloadedBytes: HotUpdaterState["downloadedBytes"];
|
|
31
|
+
totalBytes: HotUpdaterState["totalBytes"];
|
|
32
|
+
message: string | null;
|
|
33
|
+
artifactType: HotUpdaterState["artifactType"];
|
|
34
|
+
details: HotUpdaterState["details"];
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
|
-
* Common options shared
|
|
38
|
+
* Common options shared by HotUpdater initialization APIs.
|
|
29
39
|
*/
|
|
30
40
|
interface CommonHotUpdaterOptions {
|
|
31
41
|
/**
|
|
@@ -51,18 +61,24 @@ interface CommonHotUpdaterOptions {
|
|
|
51
61
|
*
|
|
52
62
|
* @example
|
|
53
63
|
* ```tsx
|
|
54
|
-
* HotUpdater.
|
|
64
|
+
* HotUpdater.init({
|
|
55
65
|
* baseURL: "https://api.example.com",
|
|
56
|
-
* updateMode: "manual",
|
|
57
66
|
* onNotifyAppReady: ({ status, crashedBundleId }) => {
|
|
58
67
|
* if (status === "RECOVERED") {
|
|
59
68
|
* analytics.track("bundle_rollback", { crashedBundleId });
|
|
60
69
|
* }
|
|
61
|
-
* }
|
|
62
|
-
* })
|
|
70
|
+
* },
|
|
71
|
+
* });
|
|
63
72
|
* ```
|
|
64
73
|
*/
|
|
65
74
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default callback invoked when Hot Updater initialization or update checks
|
|
78
|
+
* fail. Per-call `HotUpdater.checkForUpdate({ onError })` handlers override
|
|
79
|
+
* this callback for that check.
|
|
80
|
+
*/
|
|
81
|
+
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/**
|
|
@@ -70,10 +86,12 @@ interface CommonHotUpdaterOptions {
|
|
|
70
86
|
*/
|
|
71
87
|
interface BaseURLConfig {
|
|
72
88
|
/**
|
|
73
|
-
* Base URL for update server
|
|
89
|
+
* Base URL for update server. Use a function to resolve it dynamically
|
|
90
|
+
* before each update check.
|
|
74
91
|
* @example "https://update.example.com"
|
|
92
|
+
* @example () => getUpdateServerURL()
|
|
75
93
|
*/
|
|
76
|
-
baseURL:
|
|
94
|
+
baseURL: HotUpdaterBaseURL;
|
|
77
95
|
|
|
78
96
|
/**
|
|
79
97
|
* Resolver is not allowed when using baseURL
|
|
@@ -103,6 +121,11 @@ type NetworkConfig = BaseURLConfig | ResolverConfig;
|
|
|
103
121
|
|
|
104
122
|
export type AutoUpdateOptions = CommonHotUpdaterOptions &
|
|
105
123
|
NetworkConfig & {
|
|
124
|
+
/**
|
|
125
|
+
* Automatic update mode. This is the default and can be omitted.
|
|
126
|
+
*/
|
|
127
|
+
updateMode?: "auto";
|
|
128
|
+
|
|
106
129
|
/**
|
|
107
130
|
* Update strategy
|
|
108
131
|
* - "fingerprint": Use fingerprint hash to check for updates
|
|
@@ -110,14 +133,6 @@ export type AutoUpdateOptions = CommonHotUpdaterOptions &
|
|
|
110
133
|
*/
|
|
111
134
|
updateStrategy: "fingerprint" | "appVersion";
|
|
112
135
|
|
|
113
|
-
/**
|
|
114
|
-
* Update mode
|
|
115
|
-
* - "auto": Automatically check and download updates
|
|
116
|
-
*/
|
|
117
|
-
updateMode: "auto";
|
|
118
|
-
|
|
119
|
-
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
120
|
-
|
|
121
136
|
/**
|
|
122
137
|
* Component to show while downloading a new bundle update.
|
|
123
138
|
*
|
|
@@ -140,11 +155,7 @@ export type AutoUpdateOptions = CommonHotUpdaterOptions &
|
|
|
140
155
|
*
|
|
141
156
|
* If not defined, the bundle will download in the background without blocking the screen.
|
|
142
157
|
*/
|
|
143
|
-
fallbackComponent?: React.FC<
|
|
144
|
-
status: Exclude<UpdateStatus, "UPDATE_PROCESS_COMPLETED">;
|
|
145
|
-
progress: number;
|
|
146
|
-
message: string | null;
|
|
147
|
-
}>;
|
|
158
|
+
fallbackComponent?: React.FC<HotUpdaterFallbackComponentProps>;
|
|
148
159
|
|
|
149
160
|
onProgress?: (progress: number) => void;
|
|
150
161
|
|
|
@@ -167,21 +178,25 @@ export type AutoUpdateOptions = CommonHotUpdaterOptions &
|
|
|
167
178
|
export type ManualUpdateOptions = CommonHotUpdaterOptions &
|
|
168
179
|
NetworkConfig & {
|
|
169
180
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
181
|
+
* @deprecated Replace manual `HotUpdater.wrap` with `HotUpdater.init`.
|
|
182
|
+
*
|
|
183
|
+
* ```tsx
|
|
184
|
+
* import { HotUpdater } from "@hot-updater/react-native";
|
|
185
|
+
*
|
|
186
|
+
* HotUpdater.init({
|
|
187
|
+
* baseURL: "<your-update-server-url>",
|
|
188
|
+
* });
|
|
189
|
+
*
|
|
190
|
+
* export default App;
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* Then call `HotUpdater.checkForUpdate(...)` from your manual update flow.
|
|
194
|
+
* See https://hot-updater.dev/docs/guides/custom-update
|
|
172
195
|
*/
|
|
173
196
|
updateMode: "manual";
|
|
174
197
|
};
|
|
175
198
|
|
|
176
|
-
type
|
|
177
|
-
BaseURLConfig & {
|
|
178
|
-
updateMode: "manual";
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
export type HotUpdaterInitOptions = Omit<
|
|
182
|
-
ManualBaseURLUpdateOptions,
|
|
183
|
-
"updateMode"
|
|
184
|
-
>;
|
|
199
|
+
export type HotUpdaterInitOptions = CommonHotUpdaterOptions & NetworkConfig;
|
|
185
200
|
|
|
186
201
|
export type HotUpdaterOptions = AutoUpdateOptions | ManualUpdateOptions;
|
|
187
202
|
|
|
@@ -194,17 +209,13 @@ type InternalCommonOptions = {
|
|
|
194
209
|
requestHeaders?: Record<string, string>;
|
|
195
210
|
requestTimeout?: number;
|
|
196
211
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
212
|
+
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
197
213
|
};
|
|
198
214
|
|
|
199
215
|
type InternalAutoUpdateOptions = InternalCommonOptions & {
|
|
200
216
|
updateStrategy: "fingerprint" | "appVersion";
|
|
201
217
|
updateMode: "auto";
|
|
202
|
-
|
|
203
|
-
fallbackComponent?: React.FC<{
|
|
204
|
-
status: Exclude<UpdateStatus, "UPDATE_PROCESS_COMPLETED">;
|
|
205
|
-
progress: number;
|
|
206
|
-
message: string | null;
|
|
207
|
-
}>;
|
|
218
|
+
fallbackComponent?: React.FC<HotUpdaterFallbackComponentProps>;
|
|
208
219
|
onProgress?: (progress: number) => void;
|
|
209
220
|
reloadOnForceUpdate?: boolean;
|
|
210
221
|
onUpdateProcessCompleted?: (response: RunUpdateProcessResponse) => void;
|
|
@@ -222,6 +233,23 @@ export type InternalWrapOptions =
|
|
|
222
233
|
|
|
223
234
|
type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
|
|
224
235
|
|
|
236
|
+
let didWarnManualWrapDeprecation = false;
|
|
237
|
+
|
|
238
|
+
const warnManualWrapDeprecation = () => {
|
|
239
|
+
if (didWarnManualWrapDeprecation) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
didWarnManualWrapDeprecation = true;
|
|
244
|
+
console.warn(
|
|
245
|
+
'[HotUpdater] HotUpdater.wrap({ updateMode: "manual" }) is deprecated. ' +
|
|
246
|
+
"Move the same baseURL/resolver options to HotUpdater.init({ ... }), " +
|
|
247
|
+
"export your root component directly, and call " +
|
|
248
|
+
"HotUpdater.checkForUpdate(...) from your manual update flow. " +
|
|
249
|
+
"See https://hot-updater.dev/docs/guides/custom-update",
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
225
253
|
const waitForNextFrame = () =>
|
|
226
254
|
new Promise<void>((resolve) => {
|
|
227
255
|
const requestAnimationFrame = (
|
|
@@ -246,6 +274,7 @@ const handleNotifyAppReady = async (options: {
|
|
|
246
274
|
requestHeaders?: Record<string, string>;
|
|
247
275
|
requestTimeout?: number;
|
|
248
276
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
277
|
+
onError?: (error: HotUpdaterError | Error | unknown) => void;
|
|
249
278
|
}): Promise<void> => {
|
|
250
279
|
await waitForNextFrame();
|
|
251
280
|
|
|
@@ -262,12 +291,14 @@ const handleNotifyAppReady = async (options: {
|
|
|
262
291
|
requestTimeout: options.requestTimeout,
|
|
263
292
|
})
|
|
264
293
|
.catch((e: unknown) => {
|
|
294
|
+
options.onError?.(e);
|
|
265
295
|
console.warn("[HotUpdater] Resolver notifyAppReady failed:", e);
|
|
266
296
|
});
|
|
267
297
|
}
|
|
268
298
|
|
|
269
299
|
options.onNotifyAppReady?.(nativeResult);
|
|
270
300
|
} catch (e) {
|
|
301
|
+
options.onError?.(e);
|
|
271
302
|
console.warn("[HotUpdater] Failed to notify app ready:", e);
|
|
272
303
|
}
|
|
273
304
|
};
|
|
@@ -276,11 +307,15 @@ export function init(options: InternalInitOptions): void {
|
|
|
276
307
|
void handleNotifyAppReady(options);
|
|
277
308
|
}
|
|
278
309
|
|
|
279
|
-
export function wrap
|
|
310
|
+
export function wrap(
|
|
280
311
|
options: InternalWrapOptions,
|
|
281
|
-
):
|
|
312
|
+
): <P extends object>(
|
|
313
|
+
WrappedComponent: React.ComponentType<P>,
|
|
314
|
+
) => React.ComponentType<P> {
|
|
282
315
|
if (options.updateMode === "manual") {
|
|
283
|
-
|
|
316
|
+
warnManualWrapDeprecation();
|
|
317
|
+
|
|
318
|
+
return <P extends object>(WrappedComponent: React.ComponentType<P>) => {
|
|
284
319
|
const ManualHOC: React.FC<P> = (props: P) => {
|
|
285
320
|
useEffect(() => {
|
|
286
321
|
void handleNotifyAppReady(options);
|
|
@@ -293,12 +328,12 @@ export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
|
293
328
|
};
|
|
294
329
|
}
|
|
295
330
|
|
|
296
|
-
// updateMode: "auto"
|
|
297
331
|
const { reloadOnForceUpdate = true, ...restOptions } = options;
|
|
298
332
|
|
|
299
|
-
return (WrappedComponent: React.ComponentType<P>) => {
|
|
333
|
+
return <P extends object>(WrappedComponent: React.ComponentType<P>) => {
|
|
300
334
|
const HotUpdaterHOC: React.FC<P> = (props: P) => {
|
|
301
|
-
const
|
|
335
|
+
const progressState = useHotUpdaterStore((state) => state);
|
|
336
|
+
const progress = progressState.progress;
|
|
302
337
|
|
|
303
338
|
const [message, setMessage] = useState<string | null>(null);
|
|
304
339
|
const [updateStatus, setUpdateStatus] =
|
|
@@ -392,9 +427,13 @@ export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
|
392
427
|
const Fallback = restOptions.fallbackComponent;
|
|
393
428
|
return (
|
|
394
429
|
<Fallback
|
|
430
|
+
artifactType={progressState.artifactType}
|
|
431
|
+
details={progressState.details}
|
|
432
|
+
downloadedBytes={progressState.downloadedBytes}
|
|
395
433
|
progress={progress}
|
|
396
434
|
status={updateStatus}
|
|
397
435
|
message={message}
|
|
436
|
+
totalBytes={progressState.totalBytes}
|
|
398
437
|
/>
|
|
399
438
|
);
|
|
400
439
|
}
|