@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.
Files changed (128) hide show
  1. package/HotUpdater.podspec +1 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
  4. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
  6. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
  7. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
  8. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
  9. package/android/src/newarch/HotUpdater.kt +14 -2
  10. package/android/src/newarch/HotUpdaterModule.kt +112 -1
  11. package/android/src/oldarch/HotUpdater.kt +14 -2
  12. package/android/src/oldarch/HotUpdaterModule.kt +107 -3
  13. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
  14. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
  15. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
  16. package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
  17. package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
  18. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
  19. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
  20. package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
  21. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
  22. package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
  23. package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
  24. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
  25. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
  26. package/lib/commonjs/DefaultResolver.js +18 -4
  27. package/lib/commonjs/DefaultResolver.js.map +1 -1
  28. package/lib/commonjs/DefaultResolver.spec.js +136 -0
  29. package/lib/commonjs/DefaultResolver.spec.js.map +1 -0
  30. package/lib/commonjs/checkForUpdate.js +6 -0
  31. package/lib/commonjs/checkForUpdate.js.map +1 -1
  32. package/lib/commonjs/index.js +53 -18
  33. package/lib/commonjs/index.js.map +1 -1
  34. package/lib/commonjs/index.spec.js +133 -1
  35. package/lib/commonjs/index.spec.js.map +1 -1
  36. package/lib/commonjs/native.js +40 -16
  37. package/lib/commonjs/native.js.map +1 -1
  38. package/lib/commonjs/native.spec.js +119 -3
  39. package/lib/commonjs/native.spec.js.map +1 -1
  40. package/lib/commonjs/sdkVersion.js +8 -0
  41. package/lib/commonjs/sdkVersion.js.map +1 -0
  42. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  43. package/lib/commonjs/store.js +72 -5
  44. package/lib/commonjs/store.js.map +1 -1
  45. package/lib/commonjs/store.spec.js +265 -8
  46. package/lib/commonjs/store.spec.js.map +1 -1
  47. package/lib/commonjs/types.js.map +1 -1
  48. package/lib/commonjs/wrap.js +19 -5
  49. package/lib/commonjs/wrap.js.map +1 -1
  50. package/lib/commonjs/wrap.spec.js +74 -0
  51. package/lib/commonjs/wrap.spec.js.map +1 -1
  52. package/lib/module/DefaultResolver.js +18 -4
  53. package/lib/module/DefaultResolver.js.map +1 -1
  54. package/lib/module/DefaultResolver.spec.js +135 -0
  55. package/lib/module/DefaultResolver.spec.js.map +1 -0
  56. package/lib/module/checkForUpdate.js +6 -0
  57. package/lib/module/checkForUpdate.js.map +1 -1
  58. package/lib/module/index.js +53 -18
  59. package/lib/module/index.js.map +1 -1
  60. package/lib/module/index.spec.js +133 -1
  61. package/lib/module/index.spec.js.map +1 -1
  62. package/lib/module/native.js +40 -16
  63. package/lib/module/native.js.map +1 -1
  64. package/lib/module/native.spec.js +119 -3
  65. package/lib/module/native.spec.js.map +1 -1
  66. package/lib/module/sdkVersion.js +4 -0
  67. package/lib/module/sdkVersion.js.map +1 -0
  68. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  69. package/lib/module/store.js +72 -5
  70. package/lib/module/store.js.map +1 -1
  71. package/lib/module/store.spec.js +265 -8
  72. package/lib/module/store.spec.js.map +1 -1
  73. package/lib/module/types.js.map +1 -1
  74. package/lib/module/wrap.js +19 -5
  75. package/lib/module/wrap.js.map +1 -1
  76. package/lib/module/wrap.spec.js +74 -0
  77. package/lib/module/wrap.spec.js.map +1 -1
  78. package/lib/typescript/commonjs/DefaultResolver.d.ts +4 -3
  79. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
  81. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/index.d.ts +37 -9
  83. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/native.d.ts +56 -3
  85. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
  87. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
  89. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/store.d.ts +5 -0
  91. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/types.d.ts +1 -0
  93. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/wrap.d.ts +48 -32
  95. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  96. package/lib/typescript/module/DefaultResolver.d.ts +4 -3
  97. package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
  98. package/lib/typescript/module/checkForUpdate.d.ts +2 -2
  99. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  100. package/lib/typescript/module/index.d.ts +37 -9
  101. package/lib/typescript/module/index.d.ts.map +1 -1
  102. package/lib/typescript/module/native.d.ts +56 -3
  103. package/lib/typescript/module/native.d.ts.map +1 -1
  104. package/lib/typescript/module/sdkVersion.d.ts +2 -0
  105. package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
  106. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
  107. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  108. package/lib/typescript/module/store.d.ts +5 -0
  109. package/lib/typescript/module/store.d.ts.map +1 -1
  110. package/lib/typescript/module/types.d.ts +1 -0
  111. package/lib/typescript/module/types.d.ts.map +1 -1
  112. package/lib/typescript/module/wrap.d.ts +48 -32
  113. package/lib/typescript/module/wrap.d.ts.map +1 -1
  114. package/package.json +17 -9
  115. package/src/DefaultResolver.spec.ts +178 -0
  116. package/src/DefaultResolver.ts +32 -6
  117. package/src/checkForUpdate.ts +9 -2
  118. package/src/index.spec.ts +170 -2
  119. package/src/index.ts +100 -40
  120. package/src/native.spec.ts +139 -1
  121. package/src/native.ts +145 -31
  122. package/src/sdkVersion.ts +1 -0
  123. package/src/specs/NativeHotUpdater.ts +28 -0
  124. package/src/store.spec.ts +324 -9
  125. package/src/store.ts +133 -7
  126. package/src/types.ts +2 -0
  127. package/src/wrap.spec.ts +91 -0
  128. 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.isUpdateDownloaded = newState.progress >= 1;
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
- state.progress === nextState.progress &&
51
- state.isUpdateDownloaded === nextState.isUpdateDownloaded
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", setState);
193
+ addListener("onProgress", applyProgressEvent);
68
194
 
69
195
  return { getSnapshot, setState, subscribe };
70
196
  };
package/src/types.ts CHANGED
@@ -2,6 +2,8 @@ import type { AppUpdateInfo } from "@hot-updater/core";
2
2
 
3
3
  import type { NotifyAppReadyResult } from "./native";
4
4
 
5
+ export type HotUpdaterBaseURL = string | (() => string | Promise<string>);
6
+
5
7
  /**
6
8
  * Parameters passed to resolver.checkUpdate method
7
9
  */
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 between auto and manual update modes
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.wrap({
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
- * })(App);
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: string;
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
- * Update mode
171
- * - "manual": Only notify app ready, user manually calls checkForUpdate()
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 ManualBaseURLUpdateOptions = CommonHotUpdaterOptions &
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
- onError?: (error: HotUpdaterError | Error | unknown) => void;
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<P extends React.JSX.IntrinsicAttributes = object>(
310
+ export function wrap(
280
311
  options: InternalWrapOptions,
281
- ): (WrappedComponent: React.ComponentType<P>) => React.ComponentType<P> {
312
+ ): <P extends object>(
313
+ WrappedComponent: React.ComponentType<P>,
314
+ ) => React.ComponentType<P> {
282
315
  if (options.updateMode === "manual") {
283
- return (WrappedComponent: React.ComponentType<P>) => {
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 progress = useHotUpdaterStore((state) => state.progress);
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
  }