@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
@@ -1,4 +1,4 @@
1
- import type { AppUpdateInfo } from "@hot-updater/core";
1
+ import type { AppUpdateAvailableInfo, AppUpdateInfo } from "@hot-updater/core";
2
2
  import { Platform } from "react-native";
3
3
 
4
4
  import { HotUpdaterError } from "./error";
@@ -42,7 +42,7 @@ export interface CheckForUpdateOptions {
42
42
  requestTimeout?: number;
43
43
  }
44
44
 
45
- export type CheckForUpdateResult = AppUpdateInfo & {
45
+ export type CheckForUpdateResult = AppUpdateAvailableInfo & {
46
46
  /**
47
47
  * Updates the bundle.
48
48
  * This method is equivalent to `HotUpdater.updateBundle()` but with all required arguments pre-filled.
@@ -142,6 +142,10 @@ export async function checkForUpdate(
142
142
  return null;
143
143
  }
144
144
 
145
+ if (updateInfo.status === "UP_TO_DATE") {
146
+ return null;
147
+ }
148
+
145
149
  if (
146
150
  explicitChannel &&
147
151
  explicitChannel !== defaultChannel &&
@@ -168,8 +172,11 @@ export async function checkForUpdate(
168
172
  return updateBundle({
169
173
  bundleId: updateInfo.id,
170
174
  channel: runtimeChannel,
175
+ changedAssets: updateInfo.changedAssets ?? null,
171
176
  fileUrl: updateInfo.fileUrl,
172
177
  fileHash: updateInfo.fileHash,
178
+ manifestFileHash: updateInfo.manifestFileHash ?? null,
179
+ manifestUrl: updateInfo.manifestUrl ?? null,
173
180
  status: updateInfo.status,
174
181
  shouldSkipCurrentBundleIdCheck: isFirstRuntimeChannelSwitchAttempt,
175
182
  });
package/src/index.spec.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
+ import type { HotUpdater as HotUpdaterValue } from "./index";
4
+ import type { HotUpdaterInitOptions, HotUpdaterOptions } from "./wrap";
5
+
3
6
  const mocks = vi.hoisted(() => ({
4
7
  addListener: vi.fn(() => () => {}),
5
8
  checkForUpdate: vi.fn(),
@@ -74,7 +77,7 @@ describe("HotUpdater client initialization", () => {
74
77
 
75
78
  mocks.addListener.mockReturnValue(() => {});
76
79
  mocks.checkForUpdate.mockResolvedValue(null);
77
- mocks.createDefaultResolver.mockImplementation((baseURL: string) => ({
80
+ mocks.createDefaultResolver.mockImplementation((baseURL: unknown) => ({
78
81
  baseURL,
79
82
  checkUpdate: vi.fn(),
80
83
  notifyAppReady: vi.fn(),
@@ -114,6 +117,171 @@ describe("HotUpdater client initialization", () => {
114
117
  });
115
118
  });
116
119
 
120
+ it("accepts dynamic baseURL resolvers for manual update flows", async () => {
121
+ const resolver = {
122
+ checkUpdate: vi.fn(),
123
+ notifyAppReady: vi.fn(),
124
+ };
125
+ const resolveBaseURL = vi.fn(() => "https://updates.example.com");
126
+ mocks.createDefaultResolver.mockReturnValue(resolver);
127
+
128
+ const HotUpdater = await importHotUpdater();
129
+
130
+ HotUpdater.init({
131
+ baseURL: resolveBaseURL,
132
+ });
133
+
134
+ expect(mocks.createDefaultResolver).toHaveBeenCalledWith(resolveBaseURL);
135
+ expect(mocks.init).toHaveBeenCalledWith({
136
+ resolver,
137
+ });
138
+ });
139
+
140
+ it("accepts custom resolvers for manual update flows", async () => {
141
+ const resolver = {
142
+ checkUpdate: vi.fn(),
143
+ notifyAppReady: vi.fn(),
144
+ };
145
+
146
+ const HotUpdater = await importHotUpdater();
147
+
148
+ HotUpdater.init({
149
+ resolver,
150
+ requestHeaders: {
151
+ Authorization: "Bearer token",
152
+ },
153
+ });
154
+
155
+ expect(mocks.createDefaultResolver).not.toHaveBeenCalled();
156
+ expect(mocks.init).toHaveBeenCalledWith({
157
+ resolver,
158
+ requestHeaders: {
159
+ Authorization: "Bearer token",
160
+ },
161
+ });
162
+ });
163
+
164
+ it("accepts onError during init and uses it for later manual update checks", async () => {
165
+ const resolver = {
166
+ checkUpdate: vi.fn(),
167
+ notifyAppReady: vi.fn(),
168
+ };
169
+ const onError = vi.fn();
170
+ mocks.createDefaultResolver.mockReturnValue(resolver);
171
+
172
+ const HotUpdater = await importHotUpdater();
173
+
174
+ HotUpdater.init({
175
+ baseURL: "https://updates.example.com",
176
+ onError,
177
+ });
178
+
179
+ await HotUpdater.checkForUpdate({
180
+ updateStrategy: "appVersion",
181
+ });
182
+
183
+ expect(mocks.init).toHaveBeenCalledWith({
184
+ onError,
185
+ resolver,
186
+ });
187
+ expect(mocks.checkForUpdate).toHaveBeenCalledWith({
188
+ onError,
189
+ requestHeaders: {},
190
+ requestTimeout: undefined,
191
+ resolver,
192
+ updateStrategy: "appVersion",
193
+ });
194
+ });
195
+
196
+ it("lets checkForUpdate override the init onError handler", async () => {
197
+ const resolver = {
198
+ checkUpdate: vi.fn(),
199
+ notifyAppReady: vi.fn(),
200
+ };
201
+ const initOnError = vi.fn();
202
+ const checkOnError = vi.fn();
203
+ mocks.createDefaultResolver.mockReturnValue(resolver);
204
+
205
+ const HotUpdater = await importHotUpdater();
206
+
207
+ HotUpdater.init({
208
+ baseURL: "https://updates.example.com",
209
+ onError: initOnError,
210
+ });
211
+
212
+ await HotUpdater.checkForUpdate({
213
+ onError: checkOnError,
214
+ updateStrategy: "appVersion",
215
+ });
216
+
217
+ expect(mocks.checkForUpdate).toHaveBeenCalledWith({
218
+ onError: checkOnError,
219
+ requestHeaders: {},
220
+ requestTimeout: undefined,
221
+ resolver,
222
+ updateStrategy: "appVersion",
223
+ });
224
+ });
225
+
226
+ it("defaults wrap to automatic update mode", async () => {
227
+ const resolver = {
228
+ checkUpdate: vi.fn(),
229
+ notifyAppReady: vi.fn(),
230
+ };
231
+ mocks.createDefaultResolver.mockReturnValue(resolver);
232
+
233
+ const HotUpdater = await importHotUpdater();
234
+
235
+ HotUpdater.wrap({
236
+ baseURL: "https://updates.example.com",
237
+ updateStrategy: "appVersion",
238
+ });
239
+
240
+ expect(mocks.wrap).toHaveBeenCalledWith({
241
+ resolver,
242
+ updateMode: "auto",
243
+ updateStrategy: "appVersion",
244
+ });
245
+ });
246
+
247
+ it("keeps deprecated manual wrap calls working", async () => {
248
+ const resolver = {
249
+ checkUpdate: vi.fn(),
250
+ notifyAppReady: vi.fn(),
251
+ };
252
+ mocks.createDefaultResolver.mockReturnValue(resolver);
253
+
254
+ const HotUpdater = await importHotUpdater();
255
+
256
+ HotUpdater.wrap({
257
+ baseURL: "https://updates.example.com",
258
+ updateMode: "manual",
259
+ });
260
+
261
+ expect(mocks.wrap).toHaveBeenCalledWith({
262
+ resolver,
263
+ updateMode: "manual",
264
+ });
265
+ });
266
+
267
+ it("types public wrap to accept pre-typed option unions", () => {
268
+ const compileOnly = (
269
+ wrap: typeof HotUpdaterValue.wrap,
270
+ options: HotUpdaterOptions,
271
+ ) => wrap(options);
272
+
273
+ expect(compileOnly).toBeTypeOf("function");
274
+ });
275
+
276
+ it("types public init options with onError", () => {
277
+ const options = {
278
+ baseURL: "https://updates.example.com",
279
+ onError: vi.fn(),
280
+ } satisfies HotUpdaterInitOptions;
281
+
282
+ expect(options.baseURL).toBe("https://updates.example.com");
283
+ });
284
+
117
285
  it("uses init configuration for later manual update checks", async () => {
118
286
  const resolver = {
119
287
  checkUpdate: vi.fn(),
@@ -163,7 +331,7 @@ describe("HotUpdater client initialization", () => {
163
331
  const HotUpdater = await importHotUpdater();
164
332
 
165
333
  expect(() => HotUpdater.init({} as never)).toThrow(
166
- "baseURL must be provided",
334
+ "Either baseURL or resolver must be provided",
167
335
  );
168
336
  expect(mocks.init).not.toHaveBeenCalled();
169
337
  });
package/src/index.ts CHANGED
@@ -28,17 +28,21 @@ import {
28
28
  import { hotUpdaterStore } from "./store";
29
29
  import type { HotUpdaterResolver } from "./types";
30
30
  import {
31
+ type AutoUpdateOptions,
31
32
  type HotUpdaterInitOptions,
32
33
  type HotUpdaterOptions,
33
34
  init,
34
35
  type InternalInitOptions,
35
36
  type InternalWrapOptions,
37
+ type ManualUpdateOptions,
36
38
  wrap,
37
39
  } from "./wrap";
38
40
 
39
41
  export type {
40
42
  CustomReloadHandler,
41
43
  HotUpdaterEvent,
44
+ HotUpdaterProgressArtifactType,
45
+ HotUpdaterProgressEvent,
42
46
  Manifest,
43
47
  ManifestAsset,
44
48
  NotifyAppReadyResult,
@@ -48,6 +52,7 @@ export type {
48
52
  export * from "./store";
49
53
  export {
50
54
  extractSignatureFailure,
55
+ type HotUpdaterBaseURL,
51
56
  type HotUpdaterResolver,
52
57
  isSignatureVerificationError,
53
58
  type ResolverCheckUpdateParams,
@@ -55,8 +60,10 @@ export {
55
60
  type SignatureVerificationFailure,
56
61
  } from "./types";
57
62
  export type {
63
+ HotUpdaterFallbackComponentProps,
58
64
  HotUpdaterInitOptions,
59
65
  HotUpdaterOptions,
66
+ ManualUpdateOptions,
60
67
  RunUpdateProcessResponse,
61
68
  } from "./wrap";
62
69
 
@@ -78,6 +85,33 @@ const registerGlobalGetBaseURL = () => {
78
85
  // Call registration immediately on module load
79
86
  registerGlobalGetBaseURL();
80
87
 
88
+ type HotUpdaterWrap = {
89
+ (options: AutoUpdateOptions): ReturnType<typeof wrap>;
90
+ /**
91
+ * @deprecated Replace manual `HotUpdater.wrap` with `HotUpdater.init`.
92
+ *
93
+ * ```tsx
94
+ * import { HotUpdater } from "@hot-updater/react-native";
95
+ *
96
+ * HotUpdater.init({
97
+ * baseURL: "<your-update-server-url>",
98
+ * });
99
+ *
100
+ * export default App;
101
+ * ```
102
+ *
103
+ * Then call `HotUpdater.checkForUpdate(...)` from your manual update flow.
104
+ * See https://hot-updater.dev/docs/guides/custom-update
105
+ */
106
+ (options: ManualUpdateOptions): ReturnType<typeof wrap>;
107
+ (options: HotUpdaterOptions): ReturnType<typeof wrap>;
108
+ };
109
+
110
+ const isManualWrapOptions = (
111
+ options: HotUpdaterOptions,
112
+ ): options is ManualUpdateOptions =>
113
+ "updateMode" in options && options.updateMode === "manual";
114
+
81
115
  /**
82
116
  * Creates a HotUpdater client instance with all update management methods.
83
117
  * This function is called once on module initialization to create a singleton instance.
@@ -88,6 +122,7 @@ function createHotUpdaterClient() {
88
122
  resolver: HotUpdaterResolver | null;
89
123
  requestHeaders?: Record<string, string>;
90
124
  requestTimeout?: number;
125
+ onError?: (error: unknown) => void;
91
126
  } = {
92
127
  resolver: null,
93
128
  };
@@ -95,54 +130,67 @@ function createHotUpdaterClient() {
95
130
  const createMissingNetworkConfigError = (apiName: "init" | "wrap") => {
96
131
  if (apiName === "init") {
97
132
  return new Error(
98
- `[HotUpdater] baseURL must be provided.\n\n` +
99
- `Configure HotUpdater before calling update APIs:\n\n` +
133
+ `[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
134
+ `Configure HotUpdater before calling update APIs with the standard baseURL setup:\n\n` +
100
135
  ` HotUpdater.init({\n` +
101
136
  ` baseURL: "<your-update-server-url>",\n` +
102
137
  ` });\n\n` +
103
- `For more information, visit: https://hot-updater.dev/docs/react-native-api/init`,
138
+ `For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
104
139
  );
105
140
  }
106
141
 
107
142
  const baseURLExample =
108
143
  ` export default HotUpdater.wrap({\n` +
109
144
  ` baseURL: "<your-update-server-url>",\n` +
110
- ` updateStrategy: "appVersion",\n` +
111
- ` updateMode: "auto"\n` +
112
- ` })(App);\n\n`;
113
-
114
- const resolverExample =
115
- ` export default HotUpdater.wrap({\n` +
116
- ` resolver: {\n` +
117
- ` checkUpdate: async (params) => { /* custom logic */ },\n` +
118
- ` notifyAppReady: async (params) => { /* custom logic */ }\n` +
119
- ` },\n` +
120
- ` updateMode: "manual"\n` +
145
+ ` updateStrategy: "appVersion"\n` +
121
146
  ` })(App);\n\n`;
122
147
 
123
148
  return new Error(
124
149
  `[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
125
- `Option 1: Using baseURL (recommended for most cases)\n` +
150
+ `Configure HotUpdater.wrap with the standard baseURL setup:\n\n` +
126
151
  baseURLExample +
127
- `Option 2: Using custom resolver (advanced)\n` +
128
- resolverExample +
129
- `For more information, visit: https://hot-updater.dev/docs/react-native-api/${apiName}`,
152
+ `For manual update flows, use HotUpdater.init() and visit: ` +
153
+ `https://hot-updater.dev/docs/guides/custom-update`,
130
154
  );
131
155
  };
132
156
 
133
157
  const normalizeOptions = (
134
158
  options: HotUpdaterOptions,
135
159
  ): InternalWrapOptions => {
136
- if ("baseURL" in options && options.baseURL) {
137
- const { baseURL, ...rest } = options;
160
+ if (isManualWrapOptions(options)) {
161
+ if ("baseURL" in options && options.baseURL) {
162
+ const { baseURL, ...rest } = options;
163
+ return {
164
+ ...rest,
165
+ resolver: createDefaultResolver(baseURL),
166
+ updateMode: "manual",
167
+ };
168
+ }
169
+
170
+ if ("resolver" in options && options.resolver) {
171
+ return {
172
+ ...options,
173
+ updateMode: "manual",
174
+ };
175
+ }
176
+ }
177
+
178
+ const autoOptions = options as AutoUpdateOptions;
179
+
180
+ if ("baseURL" in autoOptions && autoOptions.baseURL) {
181
+ const { baseURL, ...rest } = autoOptions;
138
182
  return {
139
183
  ...rest,
140
184
  resolver: createDefaultResolver(baseURL),
185
+ updateMode: "auto",
141
186
  };
142
187
  }
143
188
 
144
- if ("resolver" in options && options.resolver) {
145
- return options;
189
+ if ("resolver" in autoOptions && autoOptions.resolver) {
190
+ return {
191
+ ...autoOptions,
192
+ updateMode: "auto",
193
+ };
146
194
  }
147
195
 
148
196
  throw createMissingNetworkConfigError("wrap");
@@ -151,21 +199,23 @@ function createHotUpdaterClient() {
151
199
  const normalizeInitOptions = (
152
200
  options: HotUpdaterInitOptions,
153
201
  ): InternalInitOptions => {
154
- const {
155
- baseURL,
156
- updateMode: _updateMode,
157
- ...rest
158
- } = options as HotUpdaterInitOptions & {
159
- updateMode?: unknown;
160
- };
161
-
162
- if (baseURL) {
202
+ const { updateMode: _updateMode, ...rest } =
203
+ options as HotUpdaterInitOptions & {
204
+ updateMode?: unknown;
205
+ };
206
+
207
+ if ("baseURL" in rest && rest.baseURL) {
208
+ const { baseURL, ...baseURLRest } = rest;
163
209
  return {
164
- ...rest,
210
+ ...baseURLRest,
165
211
  resolver: createDefaultResolver(baseURL),
166
212
  };
167
213
  }
168
214
 
215
+ if ("resolver" in rest && rest.resolver) {
216
+ return rest;
217
+ }
218
+
169
219
  throw createMissingNetworkConfigError("init");
170
220
  };
171
221
 
@@ -176,6 +226,7 @@ function createHotUpdaterClient() {
176
226
  globalConfig.resolver = normalizedOptions.resolver;
177
227
  globalConfig.requestHeaders = options.requestHeaders;
178
228
  globalConfig.requestTimeout = options.requestTimeout;
229
+ globalConfig.onError = options.onError;
179
230
  };
180
231
 
181
232
  const ensureGlobalResolver = (methodName: string) => {
@@ -186,15 +237,13 @@ function createHotUpdaterClient() {
186
237
  `Option 1: With HotUpdater.wrap()\n` +
187
238
  ` export default HotUpdater.wrap({\n` +
188
239
  ` baseURL: "<your-update-server-url>",\n` +
189
- ` updateStrategy: "appVersion",\n` +
190
- ` updateMode: "auto"\n` +
240
+ ` updateStrategy: "appVersion"\n` +
191
241
  ` })(App);\n\n` +
192
242
  `Option 2: With HotUpdater.init() for custom runtimes\n` +
193
243
  ` HotUpdater.init({\n` +
194
244
  ` baseURL: "<your-update-server-url>",\n` +
195
245
  ` });\n\n` +
196
- `For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap ` +
197
- `or https://hot-updater.dev/docs/react-native-api/init`,
246
+ `For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
198
247
  );
199
248
  }
200
249
  return globalConfig.resolver;
@@ -218,7 +267,6 @@ function createHotUpdaterClient() {
218
267
  * export default HotUpdater.wrap({
219
268
  * baseURL: "<your-update-server-url>",
220
269
  * updateStrategy: "appVersion",
221
- * updateMode: "auto",
222
270
  * requestHeaders: {
223
271
  * "Authorization": "Bearer <your-access-token>",
224
272
  * },
@@ -236,8 +284,8 @@ function createHotUpdaterClient() {
236
284
  * Initializes HotUpdater without wrapping a React component.
237
285
  *
238
286
  * Use this for manual update flows in runtimes where a root component HOC
239
- * is not convenient. It has the same initialization effect as
240
- * `HotUpdater.wrap({ updateMode: "manual" })(App)`.
287
+ * is not convenient. Replace deprecated manual
288
+ * `HotUpdater.wrap({ updateMode: "manual" })(App)` usage with this API.
241
289
  *
242
290
  * @example
243
291
  * ```tsx
@@ -419,6 +467,7 @@ function createHotUpdaterClient() {
419
467
  ...config.requestHeaders,
420
468
  },
421
469
  requestTimeout: config.requestTimeout ?? globalConfig.requestTimeout,
470
+ onError: config.onError ?? globalConfig.onError,
422
471
  };
423
472
 
424
473
  return checkForUpdate(mergedConfig);
@@ -471,8 +520,12 @@ function createHotUpdaterClient() {
471
520
  const ok = await resetChannel();
472
521
  if (ok) {
473
522
  hotUpdaterStore.setState({
523
+ artifactType: null,
524
+ details: null,
525
+ downloadedBytes: undefined,
474
526
  isUpdateDownloaded: false,
475
527
  progress: 0,
528
+ totalBytes: undefined,
476
529
  });
477
530
  }
478
531
  return ok;
@@ -521,4 +574,11 @@ function createHotUpdaterClient() {
521
574
  };
522
575
  }
523
576
 
524
- export const HotUpdater = createHotUpdaterClient();
577
+ type HotUpdaterClient = Omit<
578
+ ReturnType<typeof createHotUpdaterClient>,
579
+ "wrap"
580
+ > & {
581
+ wrap: HotUpdaterWrap;
582
+ };
583
+
584
+ export const HotUpdater: HotUpdaterClient = createHotUpdaterClient();
@@ -167,6 +167,7 @@ describe("notifyAppReady", () => {
167
167
  assets: {
168
168
  "assets/logo.png": {
169
169
  fileHash: "hash-logo",
170
+ signature: "sig-logo",
170
171
  },
171
172
  "index.android.bundle": {
172
173
  fileHash: "hash-bundle",
@@ -181,6 +182,7 @@ describe("notifyAppReady", () => {
181
182
  assets: {
182
183
  "assets/logo.png": {
183
184
  fileHash: "hash-logo",
185
+ signature: "sig-logo",
184
186
  },
185
187
  "index.android.bundle": {
186
188
  fileHash: "hash-bundle",
@@ -337,11 +339,147 @@ describe("notifyAppReady", () => {
337
339
  bundleId: "bundle-123",
338
340
  });
339
341
  expect(getBaseURL()).toBe("file:///bundle-123");
340
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(2);
342
+ expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(3);
341
343
  expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
342
344
  expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
343
345
  });
344
346
 
347
+ it("reinstalls a session-installed bundle after native resets to built-in", async () => {
348
+ nativeModuleMock.getConstants.mockReturnValue({
349
+ APP_VERSION: null,
350
+ CHANNEL: "production",
351
+ DEFAULT_CHANNEL: "production",
352
+ FINGERPRINT_HASH: null,
353
+ MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
354
+ });
355
+ nativeModuleMock.getBundleId.mockReturnValue(
356
+ "00000000-0000-0000-0000-000000000002",
357
+ );
358
+ nativeModuleMock.updateBundle.mockResolvedValue(true);
359
+
360
+ const { getBundleId, updateBundle } = await import("./native");
361
+
362
+ await updateBundle({
363
+ bundleId: "00000000-0000-0000-0000-000000000003",
364
+ fileHash: null,
365
+ fileUrl: "https://example.com/bundle.zip",
366
+ status: "UPDATE",
367
+ });
368
+
369
+ nativeModuleMock.getBundleId.mockReturnValue(
370
+ "00000000-0000-0000-0000-000000000003",
371
+ );
372
+ expect(getBundleId()).toBe("00000000-0000-0000-0000-000000000003");
373
+
374
+ nativeModuleMock.getBundleId.mockReturnValue(null);
375
+
376
+ await expect(
377
+ updateBundle({
378
+ bundleId: "00000000-0000-0000-0000-000000000003",
379
+ fileHash: null,
380
+ fileUrl: "https://example.com/bundle.zip",
381
+ status: "UPDATE",
382
+ }),
383
+ ).resolves.toBe(true);
384
+
385
+ expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
386
+ });
387
+
388
+ it("skips a session-installed bundle when native still reports it active", async () => {
389
+ nativeModuleMock.getConstants.mockReturnValue({
390
+ APP_VERSION: null,
391
+ CHANNEL: "production",
392
+ DEFAULT_CHANNEL: "production",
393
+ FINGERPRINT_HASH: null,
394
+ MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
395
+ });
396
+ nativeModuleMock.getBundleId.mockReturnValue(
397
+ "00000000-0000-0000-0000-000000000002",
398
+ );
399
+ nativeModuleMock.updateBundle.mockResolvedValue(true);
400
+
401
+ const { updateBundle } = await import("./native");
402
+
403
+ await updateBundle({
404
+ bundleId: "00000000-0000-0000-0000-000000000003",
405
+ fileHash: null,
406
+ fileUrl: "https://example.com/bundle.zip",
407
+ status: "UPDATE",
408
+ });
409
+
410
+ nativeModuleMock.getBundleId.mockReturnValue(
411
+ "00000000-0000-0000-0000-000000000003",
412
+ );
413
+
414
+ await expect(
415
+ updateBundle({
416
+ bundleId: "00000000-0000-0000-0000-000000000003",
417
+ fileHash: null,
418
+ fileUrl: "https://example.com/bundle.zip",
419
+ status: "UPDATE",
420
+ }),
421
+ ).resolves.toBe(true);
422
+
423
+ expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(1);
424
+ });
425
+
426
+ it("forwards manifest artifact parameters to native updateBundle", async () => {
427
+ nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
428
+ nativeModuleMock.updateBundle.mockResolvedValue(true);
429
+
430
+ const { updateBundle } = await import("./native");
431
+
432
+ await updateBundle({
433
+ bundleId: "bundle-789",
434
+ changedAssets: {
435
+ "index.ios.bundle": {
436
+ file: {
437
+ compression: "br",
438
+ url: "https://example.com/files/index.ios.bundle.br",
439
+ },
440
+ fileHash: "hash-next",
441
+ patch: {
442
+ algorithm: "bsdiff",
443
+ baseBundleId: "bundle-123",
444
+ baseFileHash: "hash-prev",
445
+ patchFileHash: "hash-patch",
446
+ patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
447
+ },
448
+ },
449
+ },
450
+ fileHash: "sig:archive",
451
+ fileUrl: "https://example.com/bundle.zip",
452
+ manifestFileHash: "sig:manifest",
453
+ manifestUrl: "https://example.com/manifest.json",
454
+ status: "UPDATE",
455
+ });
456
+
457
+ expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
458
+ bundleId: "bundle-789",
459
+ changedAssets: {
460
+ "index.ios.bundle": {
461
+ file: {
462
+ compression: "br",
463
+ url: "https://example.com/files/index.ios.bundle.br",
464
+ },
465
+ fileHash: "hash-next",
466
+ patch: {
467
+ algorithm: "bsdiff",
468
+ baseBundleId: "bundle-123",
469
+ baseFileHash: "hash-prev",
470
+ patchFileHash: "hash-patch",
471
+ patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
472
+ },
473
+ },
474
+ },
475
+ channel: undefined,
476
+ fileHash: "sig:archive",
477
+ fileUrl: "https://example.com/bundle.zip",
478
+ manifestFileHash: "sig:manifest",
479
+ manifestUrl: "https://example.com/manifest.json",
480
+ });
481
+ });
482
+
345
483
  it("invalidates cached bundle getters after resetChannel succeeds", async () => {
346
484
  nativeModuleMock.getConstants.mockReturnValue({
347
485
  APP_VERSION: null,