@hot-updater/react-native 1.0.0-rc.40 → 1.0.0-rc.42

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 (88) hide show
  1. package/lib/commonjs/appReady.js +25 -31
  2. package/lib/commonjs/appReady.js.map +1 -1
  3. package/lib/commonjs/checkForUpdate.js +9 -9
  4. package/lib/commonjs/checkForUpdate.js.map +1 -1
  5. package/lib/commonjs/error.js +1 -1
  6. package/lib/commonjs/httpClient.js +1 -1
  7. package/lib/commonjs/index.js +304 -326
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/init.types.js +6 -0
  10. package/lib/commonjs/init.types.js.map +1 -0
  11. package/lib/commonjs/native.js +3 -3
  12. package/lib/commonjs/pluginHost.js +11 -15
  13. package/lib/commonjs/pluginHost.js.map +1 -1
  14. package/lib/commonjs/sdkVersion.js +1 -1
  15. package/lib/commonjs/wrap.js +7 -16
  16. package/lib/commonjs/wrap.js.map +1 -1
  17. package/lib/module/appReady.js +23 -28
  18. package/lib/module/appReady.js.map +1 -1
  19. package/lib/module/checkForUpdate.js +10 -10
  20. package/lib/module/checkForUpdate.js.map +1 -1
  21. package/lib/module/clientPlugin.js.map +1 -1
  22. package/lib/module/error.js +1 -1
  23. package/lib/module/httpClient.js +1 -1
  24. package/lib/module/index.js +299 -329
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/init.types.js +4 -0
  27. package/lib/module/init.types.js.map +1 -0
  28. package/lib/module/native.js +3 -3
  29. package/lib/module/pluginHost.js +9 -9
  30. package/lib/module/pluginHost.js.map +1 -1
  31. package/lib/module/sdkVersion.js +1 -1
  32. package/lib/module/wrap.js +7 -15
  33. package/lib/module/wrap.js.map +1 -1
  34. package/lib/typescript/commonjs/appReady.d.ts +18 -9
  35. package/lib/typescript/commonjs/appReady.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/checkForUpdate.d.ts +5 -3
  37. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/clientPlugin.d.ts +1 -1
  39. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/error.d.ts +1 -1
  41. package/lib/typescript/commonjs/httpClient.d.ts +1 -1
  42. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/index.d.ts +122 -110
  44. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/init.types.d.ts +21 -0
  46. package/lib/typescript/commonjs/init.types.d.ts.map +1 -0
  47. package/lib/typescript/commonjs/native.d.ts +3 -3
  48. package/lib/typescript/commonjs/pluginHost.d.ts +7 -3
  49. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  51. package/lib/typescript/commonjs/wrap.d.ts +2 -3
  52. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/wrap.types.d.ts +13 -40
  54. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  55. package/lib/typescript/module/appReady.d.ts +18 -9
  56. package/lib/typescript/module/appReady.d.ts.map +1 -1
  57. package/lib/typescript/module/checkForUpdate.d.ts +5 -3
  58. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  59. package/lib/typescript/module/clientPlugin.d.ts +1 -1
  60. package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
  61. package/lib/typescript/module/error.d.ts +1 -1
  62. package/lib/typescript/module/httpClient.d.ts +1 -1
  63. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  64. package/lib/typescript/module/index.d.ts +122 -110
  65. package/lib/typescript/module/index.d.ts.map +1 -1
  66. package/lib/typescript/module/init.types.d.ts +21 -0
  67. package/lib/typescript/module/init.types.d.ts.map +1 -0
  68. package/lib/typescript/module/native.d.ts +3 -3
  69. package/lib/typescript/module/pluginHost.d.ts +7 -3
  70. package/lib/typescript/module/pluginHost.d.ts.map +1 -1
  71. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  72. package/lib/typescript/module/wrap.d.ts +2 -3
  73. package/lib/typescript/module/wrap.d.ts.map +1 -1
  74. package/lib/typescript/module/wrap.types.d.ts +13 -40
  75. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  76. package/package.json +4 -3
  77. package/src/appReady.ts +43 -37
  78. package/src/checkForUpdate.ts +19 -11
  79. package/src/clientPlugin.ts +3 -0
  80. package/src/error.ts +1 -1
  81. package/src/httpClient.ts +1 -1
  82. package/src/index.ts +344 -422
  83. package/src/init.types.ts +24 -0
  84. package/src/native.ts +3 -3
  85. package/src/pluginHost.ts +16 -11
  86. package/src/sdkVersion.ts +1 -1
  87. package/src/wrap.tsx +6 -20
  88. package/src/wrap.types.ts +18 -45
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { emitAfterAppReady } from "./appReady";
1
+ import { createLaunchReporter, type LaunchReporter } from "./appReady";
2
2
  import {
3
3
  type CheckForUpdateOptions,
4
4
  checkForUpdate,
5
- type InternalCheckForUpdateOptions,
6
5
  reportUpdateError,
7
6
  } from "./checkForUpdate";
7
+ import type { ClientPluginApis, HotUpdaterClientPlugin } from "./clientPlugin";
8
8
  import { createHttpClient, type HotUpdaterHttpClient } from "./httpClient";
9
+ import type { HotUpdaterInitOptions } from "./init.types";
9
10
  import {
10
11
  addListener,
11
12
  getPublicActiveUpdateState,
@@ -32,25 +33,20 @@ import {
32
33
  stageBundle,
33
34
  type UpdateParams,
34
35
  } from "./native";
35
- import { configurePlugins } from "./pluginHost";
36
+ import { createAppPluginHost } from "./pluginHost";
36
37
  import { hotUpdaterStore } from "./store";
37
- import {
38
- type AutoUpdateOptions,
39
- type HotUpdaterInitOptions,
40
- type HotUpdaterOptions,
41
- init,
42
- type InternalInitOptions,
43
- type InternalWrapOptions,
44
- wrap,
45
- } from "./wrap";
38
+ import { type HotUpdaterWrapOptions, wrap } from "./wrap";
46
39
 
47
40
  export {
48
41
  defineClientPlugin,
49
42
  type AppReadyResult,
50
43
  type BundleDownloadedInfo,
44
+ type ClientPluginApi,
45
+ type ClientPluginApis,
51
46
  type HotUpdaterClientContext,
52
47
  type HotUpdaterClientHooks,
53
48
  type HotUpdaterClientPlugin,
49
+ type HotUpdaterClientSetup,
54
50
  type HotUpdaterClientStorage,
55
51
  type ReleaseTransitionKind,
56
52
  type UpdateCheckResult,
@@ -63,10 +59,25 @@ export {
63
59
  // The built-in Insights client plugin, for HotUpdater.init({ plugins }).
64
60
  export {
65
61
  insights,
62
+ type InsightsClient,
66
63
  type InsightsOptions,
67
64
  type InsightsPlugin,
68
65
  type InsightsUser,
69
66
  } from "@hot-updater/plugin-insights/client";
67
+ // The built-in Remote Config client plugin, for HotUpdater.init({ plugins }).
68
+ export {
69
+ remoteConfig,
70
+ type RemoteConfigClient,
71
+ type RemoteConfigDefaults,
72
+ type RemoteConfigDefaultValue,
73
+ type RemoteConfigFetchOptions,
74
+ type RemoteConfigFetchStatus,
75
+ type RemoteConfigKey,
76
+ type RemoteConfigOptions,
77
+ type RemoteConfigPlugin,
78
+ type RemoteConfigValue,
79
+ type RemoteConfigValueSource,
80
+ } from "@hot-updater/plugin-remote-config/client";
70
81
  export type {
71
82
  CustomReloadHandler,
72
83
  HotUpdaterEvent,
@@ -86,10 +97,10 @@ export {
86
97
  isSignatureVerificationError,
87
98
  type SignatureVerificationFailure,
88
99
  } from "./types";
100
+ export type { HotUpdaterInitOptions } from "./init.types";
89
101
  export type {
90
102
  HotUpdaterFallbackComponentProps,
91
- HotUpdaterInitOptions,
92
- HotUpdaterOptions,
103
+ HotUpdaterWrapOptions,
93
104
  RunUpdateProcessResponse,
94
105
  } from "./wrap";
95
106
 
@@ -111,343 +122,240 @@ const registerGlobalGetBaseURL = () => {
111
122
  // Call registration immediately on module load
112
123
  registerGlobalGetBaseURL();
113
124
 
114
- type HotUpdaterWrap = {
115
- (options: AutoUpdateOptions): ReturnType<typeof wrap>;
116
- (options: HotUpdaterOptions): ReturnType<typeof wrap>;
117
- };
118
-
119
- /**
120
- * Creates a HotUpdater client instance with all update management methods.
121
- * This function is called once on module initialization to create a singleton instance.
122
- */
123
- function createHotUpdaterClient() {
124
- let configurationAPI: "init" | "wrap" | null = null;
125
- let mixedConfigurationReported = false;
126
-
127
- // Global configuration stored from wrap
128
- const globalConfig: {
129
- client: HotUpdaterHttpClient | null;
130
- requestHeaders?: Record<string, string>;
131
- requestTimeout?: number;
132
- onError?: (error: unknown) => void;
133
- } = {
134
- client: null,
135
- };
136
-
137
- const createMissingNetworkConfigError = (apiName: "init" | "wrap") => {
138
- if (apiName === "init") {
139
- return new Error(
140
- `[HotUpdater] baseURL must be provided.\n\n` +
141
- `Configure HotUpdater before calling update APIs with the standard baseURL setup:\n\n` +
142
- ` HotUpdater.init({\n` +
143
- ` baseURL: "<your-update-server-url>",\n` +
144
- ` });\n\n` +
145
- `For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
146
- );
147
- }
148
-
149
- const baseURLExample =
150
- ` export default HotUpdater.wrap({\n` +
125
+ const createMissingNetworkConfigError = () =>
126
+ new Error(
127
+ `[HotUpdater] baseURL must be provided.\n\n` +
128
+ `Configure HotUpdater with the standard baseURL setup:\n\n` +
129
+ ` export const hotUpdater = HotUpdater.init({\n` +
151
130
  ` baseURL: "<your-update-server-url>",\n` +
152
- ` updateStrategy: "appVersion"\n` +
153
- ` })(App);\n\n`;
154
-
155
- return new Error(
156
- `[HotUpdater] baseURL must be provided.\n\n` +
157
- `Configure HotUpdater.wrap with the standard baseURL setup:\n\n` +
158
- baseURLExample +
159
- `For manual update flows, use HotUpdater.init() and visit: ` +
160
- `https://hot-updater.dev/docs/guides/custom-update`,
161
- );
162
- };
163
-
164
- const normalizeOptions = (
165
- options: HotUpdaterOptions,
166
- ): InternalWrapOptions => {
167
- const incoming = options as HotUpdaterOptions & {
168
- updateMode?: unknown;
169
- };
170
- if (incoming.updateMode === "manual") {
171
- throw new Error(
172
- '[HotUpdater] HotUpdater.wrap({ updateMode: "manual" }) was removed. ' +
173
- "Call HotUpdater.init({ ... }) instead, export your root component " +
174
- "directly, and use HotUpdater.checkForUpdate(...) for the manual " +
175
- "update flow. See https://hot-updater.dev/docs/guides/custom-update",
176
- );
177
- }
178
-
179
- const autoOptions = incoming as AutoUpdateOptions;
180
-
181
- if (autoOptions.baseURL) {
182
- const { baseURL, plugins: _plugins, ...rest } = autoOptions;
183
- return {
184
- ...rest,
185
- client: createHttpClient(baseURL, (response) => {
186
- emitAfterAppReady("onHttpResponse", () => response);
187
- }),
188
- };
189
- }
190
-
191
- throw createMissingNetworkConfigError("wrap");
192
- };
193
-
194
- const normalizeInitOptions = (
195
- options: HotUpdaterInitOptions,
196
- ): InternalInitOptions => {
197
- const { updateMode: _updateMode, ...rest } =
198
- options as HotUpdaterInitOptions & {
199
- updateMode?: unknown;
200
- };
201
-
202
- if (rest.baseURL) {
203
- const { baseURL, plugins: _plugins, ...baseURLRest } = rest;
204
- return {
205
- ...baseURLRest,
206
- client: createHttpClient(baseURL, (response) => {
207
- emitAfterAppReady("onHttpResponse", () => response);
208
- }),
209
- };
210
- }
211
-
212
- throw createMissingNetworkConfigError("init");
213
- };
214
-
215
- const configureGlobal = (
216
- normalizedOptions: InternalInitOptions | InternalWrapOptions,
217
- options: HotUpdaterOptions | HotUpdaterInitOptions,
218
- ) => {
219
- // Plugins are set up before init or wrap reads the launch they observe.
220
- configurePlugins(options.plugins, {
221
- baseURL: options.baseURL,
222
- requestHeaders: options.requestHeaders,
223
- requestTimeout: options.requestTimeout,
224
- onError: options.onError,
225
- });
226
- globalConfig.client = normalizedOptions.client;
227
- globalConfig.requestHeaders = options.requestHeaders;
228
- globalConfig.requestTimeout = options.requestTimeout;
229
- globalConfig.onError = options.onError;
230
- };
231
-
232
- const reportMixedConfiguration = (nextAPI: "init" | "wrap") => {
233
- if (
234
- configurationAPI !== null &&
235
- configurationAPI !== nextAPI &&
236
- !mixedConfigurationReported
237
- ) {
238
- mixedConfigurationReported = true;
239
- console.error(
240
- "[HotUpdater] HotUpdater.init() and HotUpdater.wrap() must not be used together. " +
241
- "For custom or manual update flows, use HotUpdater.init() with " +
242
- "HotUpdater.checkForUpdate(). For the automatic HOC flow, use " +
243
- "HotUpdater.wrap().",
244
- );
245
- }
246
- configurationAPI ??= nextAPI;
247
- };
248
-
249
- const ensureGlobalClient = (methodName: string) => {
250
- if (!globalConfig.client) {
251
- throw new Error(
252
- `[HotUpdater] ${methodName} requires HotUpdater.wrap() or HotUpdater.init() to be used.\n\n` +
253
- `To fix this issue, configure HotUpdater before calling ${methodName}:\n\n` +
254
- `Option 1: With HotUpdater.wrap()\n` +
255
- ` export default HotUpdater.wrap({\n` +
256
- ` baseURL: "<your-update-server-url>",\n` +
257
- ` updateStrategy: "appVersion"\n` +
258
- ` })(App);\n\n` +
259
- `Option 2: With HotUpdater.init() for custom runtimes\n` +
260
- ` HotUpdater.init({\n` +
261
- ` baseURL: "<your-update-server-url>",\n` +
262
- ` });\n\n` +
263
- `For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
264
- );
265
- }
266
- return globalConfig.client;
267
- };
131
+ ` });\n\n` +
132
+ `See https://hot-updater.dev/docs/react-native-api/init`,
133
+ );
134
+
135
+ /** The methods that read or change native state, the same on every instance. */
136
+ const nativeMethods = {
137
+ /**
138
+ * Reloads the app.
139
+ */
140
+ reload,
268
141
 
269
- return {
270
- /**
271
- * `HotUpdater.wrap` checks for updates at the entry point, and if there is a bundle to update, it downloads the bundle and applies the update strategy.
272
- *
273
- * @param {object} options - Configuration options
274
- * @param {string} options.source - Update server URL
275
- * @param {object} [options.requestHeaders] - Request headers
276
- * @param {React.ComponentType} [options.fallbackComponent] - Component to display during updates
277
- * @param {boolean} [options.reloadOnForceUpdate=true] - Whether to automatically reload the app on force updates
278
- * @param {Function} [options.onUpdateProcessCompleted] - Callback after update process completes
279
- * @param {Function} [options.onProgress] - Callback to track bundle download progress
280
- * @returns {Function} Higher-order component that wraps the app component
281
- *
282
- * @example
283
- * ```tsx
284
- * export default HotUpdater.wrap({
285
- * baseURL: "<your-update-server-url>",
286
- * updateStrategy: "appVersion",
287
- * requestHeaders: {
288
- * "x-api-key": "<your-api-key>",
289
- * },
290
- * })(App);
291
- * ```
292
- */
293
- wrap: (options: HotUpdaterOptions) => {
294
- const normalizedOptions = normalizeOptions(options);
295
- reportMixedConfiguration("wrap");
296
- configureGlobal(normalizedOptions, options);
142
+ /**
143
+ * Configures how `hotUpdater.reload()` behaves.
144
+ *
145
+ * This can be called unconditionally on both platforms.
146
+ * The default is `processRestart`.
147
+ *
148
+ * - `reload`: built-in React Native reload on both platforms
149
+ * - `processRestart`: Android process restart, iOS behaves like normal reload
150
+ * - `custom`: run a custom JS handler on both platforms
151
+ */
152
+ setReloadBehavior,
297
153
 
298
- return wrap(normalizedOptions);
299
- },
154
+ /**
155
+ * Returns whether an update has finished downloading in this app session.
156
+ *
157
+ * When it returns true, calling `hotUpdater.reload()` (or restarting the app)
158
+ * will apply the downloaded update bundle.
159
+ *
160
+ * - Derived from `progress` reaching 1.0
161
+ * - Resets to false when a new download starts (progress < 1)
162
+ *
163
+ * @returns {boolean} True if a downloaded update is ready to apply
164
+ * @example
165
+ * ```ts
166
+ * if (hotUpdater.isUpdateDownloaded()) {
167
+ * await hotUpdater.reload();
168
+ * }
169
+ * ```
170
+ */
171
+ isUpdateDownloaded: () => hotUpdaterStore.getSnapshot().isUpdateDownloaded,
172
+
173
+ /**
174
+ * Fetches the current app version.
175
+ */
176
+ getAppVersion,
300
177
 
301
- /**
302
- * Initializes HotUpdater without wrapping a React component.
303
- *
304
- * Use this for manual update flows in runtimes where a root component HOC
305
- * is not convenient. Use this instead of wrapping the root component.
306
- *
307
- * @example
308
- * ```tsx
309
- * HotUpdater.init({
310
- * baseURL: "<your-update-server-url>",
311
- * });
312
- *
313
- * export default App;
314
- * ```
315
- */
316
- init: (options: HotUpdaterInitOptions): void => {
317
- const normalizedOptions = normalizeInitOptions(options);
178
+ /** Reads the active and stable Release/Bundle state. */
179
+ getActiveUpdateState: async () => getPublicActiveUpdateState(),
318
180
 
319
- reportMixedConfiguration("init");
320
- configureGlobal(normalizedOptions, options);
181
+ /**
182
+ * Returns the selected update ID, matching the ID shown in the console.
183
+ * A staged update can change this ID before the app reloads.
184
+ */
185
+ getBundleId: getUpdateId,
321
186
 
322
- init(normalizedOptions);
323
- },
187
+ /** Returns the minimum bundle ID based on the native app build time. */
188
+ getMinBundleId,
324
189
 
325
- /**
326
- * Reloads the app.
327
- */
328
- reload,
190
+ /**
191
+ * Fetches the current manifest for the active bundle.
192
+ */
193
+ getManifest,
329
194
 
330
- /**
331
- * Configures how `HotUpdater.reload()` behaves.
332
- *
333
- * This can be called unconditionally on both platforms.
334
- * The default is `processRestart`.
335
- *
336
- * - `reload`: built-in React Native reload on both platforms
337
- * - `processRestart`: Android process restart, iOS behaves like normal reload
338
- * - `custom`: run a custom JS handler on both platforms
339
- */
340
- setReloadBehavior,
195
+ /**
196
+ * Fetches the current channel of the app.
197
+ *
198
+ * If no channel is specified, the app is assigned to the 'production' channel.
199
+ *
200
+ * @returns {string} The current release channel of the app
201
+ * @default "production"
202
+ * @example
203
+ * ```ts
204
+ * const channel = hotUpdater.getChannel();
205
+ * console.log(`Current channel: ${channel}`);
206
+ * ```
207
+ */
208
+ getChannel,
341
209
 
342
- /**
343
- * Returns whether an update has finished downloading in this app session.
344
- *
345
- * When it returns true, calling `HotUpdater.reload()` (or restarting the app)
346
- * will apply the downloaded update bundle.
347
- *
348
- * - Derived from `progress` reaching 1.0
349
- * - Resets to false when a new download starts (progress < 1)
350
- *
351
- * @returns {boolean} True if a downloaded update is ready to apply
352
- * @example
353
- * ```ts
354
- * if (HotUpdater.isUpdateDownloaded()) {
355
- * await HotUpdater.reload();
356
- * }
357
- * ```
358
- */
359
- isUpdateDownloaded: () => hotUpdaterStore.getSnapshot().isUpdateDownloaded,
210
+ /**
211
+ * Fetches the build-time default channel of the app.
212
+ *
213
+ * This value does not change when a runtime channel override is active.
214
+ *
215
+ * @returns {string} The default release channel embedded in the app
216
+ * @example
217
+ * ```ts
218
+ * const defaultChannel = hotUpdater.getDefaultChannel();
219
+ * console.log(`Default channel: ${defaultChannel}`);
220
+ * ```
221
+ */
222
+ getDefaultChannel,
360
223
 
361
- /**
362
- * Fetches the current app version.
363
- */
364
- getAppVersion,
224
+ /**
225
+ * Returns whether the app is currently using a runtime channel override.
226
+ *
227
+ * @returns {boolean} true when a non-default channel has been applied
228
+ */
229
+ isChannelSwitched,
365
230
 
366
- /** Reads the active and stable Release/Bundle state. */
367
- getActiveUpdateState: async () => getPublicActiveUpdateState(),
231
+ /**
232
+ * Sets the persisted cohort used for rollout calculations.
233
+ * Call `getCohort()` first if you need to restore the initial value later.
234
+ */
235
+ setCohort,
368
236
 
369
- /**
370
- * Returns the selected update ID, matching the ID shown in the console.
371
- * A staged update can change this ID before the app reloads.
372
- */
373
- getBundleId: getUpdateId,
237
+ /**
238
+ * Gets the persisted cohort used for rollout calculations.
239
+ */
240
+ getCohort,
374
241
 
375
- /** Returns the minimum bundle ID based on the native app build time. */
376
- getMinBundleId,
242
+ /**
243
+ * Adds a listener to HotUpdater events.
244
+ *
245
+ * @param {keyof HotUpdaterEvent} eventName - The name of the event to listen for
246
+ * @param {(event: HotUpdaterEvent[T]) => void} listener - The callback function to handle the event
247
+ * @returns {() => void} A cleanup function that removes the event listener
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * const unsubscribe = hotUpdater.addListener("onProgress", ({ progress }) => {
252
+ * console.log(`Update progress: ${progress * 100}%`);
253
+ * });
254
+ *
255
+ * // Unsubscribe when no longer needed
256
+ * unsubscribe();
257
+ * ```
258
+ */
259
+ addListener,
377
260
 
378
- /**
379
- * Fetches the current manifest for the active bundle.
380
- */
381
- getManifest,
261
+ /**
262
+ * Clears the runtime channel override and restores the original bundle.
263
+ *
264
+ * @returns {Promise<boolean>} Resolves with true if reset was successful
265
+ */
266
+ resetChannel: async () => {
267
+ const ok = await resetChannel();
268
+ if (ok) {
269
+ hotUpdaterStore.setState({
270
+ artifactType: null,
271
+ details: null,
272
+ isUpdateDownloaded: false,
273
+ progress: 0,
274
+ });
275
+ }
276
+ return ok;
277
+ },
278
+
279
+ /**
280
+ * Fetches the fingerprint of the app.
281
+ *
282
+ * @returns {string} The fingerprint of the app
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * const fingerprint = hotUpdater.getFingerprintHash();
287
+ * console.log(`Fingerprint: ${fingerprint}`);
288
+ * ```
289
+ */
290
+ getFingerprintHash,
382
291
 
383
- /**
384
- * Fetches the current channel of the app.
385
- *
386
- * If no channel is specified, the app is assigned to the 'production' channel.
387
- *
388
- * @returns {string} The current release channel of the app
389
- * @default "production"
390
- * @example
391
- * ```ts
392
- * const channel = HotUpdater.getChannel();
393
- * console.log(`Current channel: ${channel}`);
394
- * ```
395
- */
396
- getChannel,
292
+ /**
293
+ * Fetches the persisted install id for this app installation.
294
+ */
295
+ getInstallId,
397
296
 
398
- /**
399
- * Fetches the build-time default channel of the app.
400
- *
401
- * This value does not change when a runtime channel override is active.
402
- *
403
- * @returns {string} The default release channel embedded in the app
404
- * @example
405
- * ```ts
406
- * const defaultChannel = HotUpdater.getDefaultChannel();
407
- * console.log(`Default channel: ${defaultChannel}`);
408
- * ```
409
- */
410
- getDefaultChannel,
297
+ /**
298
+ * Reads the native launch report for the current process.
299
+ */
300
+ notifyAppReady,
411
301
 
412
- /**
413
- * Returns whether the app is currently using a runtime channel override.
414
- *
415
- * @returns {boolean} true when a non-default channel has been applied
416
- */
417
- isChannelSwitched,
302
+ /**
303
+ * Gets the list of bundle IDs that have been marked as crashed.
304
+ * These bundles will be rejected if attempted to install again.
305
+ *
306
+ * @returns {string[]} Array of crashed bundle IDs
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const crashedBundles = hotUpdater.getCrashHistory();
311
+ * console.log("Crashed bundles:", crashedBundles);
312
+ * ```
313
+ */
314
+ getCrashHistory,
418
315
 
419
- /**
420
- * Sets the persisted cohort used for rollout calculations.
421
- * Call `getCohort()` first if you need to restore the initial value later.
422
- */
423
- setCohort,
316
+ /**
317
+ * Clears the crashed bundle history, allowing previously crashed bundles
318
+ * to be installed again.
319
+ *
320
+ * @returns {boolean} true if clearing was successful
321
+ *
322
+ * @example
323
+ * ```ts
324
+ * // Clear crash history to allow retrying a previously failed bundle
325
+ * hotUpdater.clearCrashHistory();
326
+ * ```
327
+ */
328
+ clearCrashHistory,
329
+ };
424
330
 
425
- /**
426
- * Gets the persisted cohort used for rollout calculations.
427
- */
428
- getCohort,
331
+ /** What an instance's own methods use: its server, settings, and launch. */
332
+ interface InstanceConfig {
333
+ readonly client: HotUpdaterHttpClient;
334
+ readonly requestHeaders?: Record<string, string>;
335
+ readonly requestTimeout?: number;
336
+ readonly onError?: (error: unknown) => void;
337
+ readonly launch: LaunchReporter;
338
+ }
429
339
 
430
- /**
431
- * Adds a listener to HotUpdater events.
432
- *
433
- * @param {keyof HotUpdaterEvent} eventName - The name of the event to listen for
434
- * @param {(event: HotUpdaterEvent[T]) => void} listener - The callback function to handle the event
435
- * @returns {() => void} A cleanup function that removes the event listener
436
- *
437
- * @example
438
- * ```ts
439
- * const unsubscribe = HotUpdater.addListener("onProgress", ({ progress }) => {
440
- * console.log(`Update progress: ${progress * 100}%`);
441
- * });
442
- *
443
- * // Unsubscribe when no longer needed
444
- * unsubscribe();
445
- * ```
446
- */
447
- addListener,
340
+ /** The methods that use the instance's own configuration and plugins. */
341
+ const createConfiguredMethods = (config: InstanceConfig) => {
342
+ const check = (options: CheckForUpdateOptions) =>
343
+ checkForUpdate({
344
+ ...options,
345
+ client: config.client,
346
+ emit: config.launch.emit,
347
+ requestHeaders: {
348
+ ...config.requestHeaders,
349
+ ...options.requestHeaders,
350
+ },
351
+ requestTimeout: options.requestTimeout ?? config.requestTimeout,
352
+ onError: options.onError ?? config.onError,
353
+ });
448
354
 
355
+ return {
449
356
  /**
450
- * Manually checks for updates.
357
+ * Checks for an update, with this instance's server and request
358
+ * settings.
451
359
  *
452
360
  * @param {Object} config - Update check configuration
453
361
  * @param {string} [config.channel] - Optional channel override for this update check
@@ -457,7 +365,7 @@ function createHotUpdaterClient() {
457
365
  *
458
366
  * @example
459
367
  * ```ts
460
- * const updateInfo = await HotUpdater.checkForUpdate({
368
+ * const updateInfo = await hotUpdater.checkForUpdate({
461
369
  * updateStrategy: "appVersion",
462
370
  * requestHeaders: {
463
371
  * "x-api-key": "<your-api-key>",
@@ -471,26 +379,11 @@ function createHotUpdaterClient() {
471
379
  *
472
380
  * await updateInfo.updateBundle();
473
381
  * if (updateInfo.shouldForceUpdate) {
474
- * await HotUpdater.reload();
382
+ * await hotUpdater.reload();
475
383
  * }
476
384
  * ```
477
385
  */
478
- checkForUpdate: (config: CheckForUpdateOptions) => {
479
- const client = ensureGlobalClient("checkForUpdate");
480
-
481
- const mergedConfig: InternalCheckForUpdateOptions = {
482
- ...config,
483
- client,
484
- requestHeaders: {
485
- ...globalConfig.requestHeaders,
486
- ...config.requestHeaders,
487
- },
488
- requestTimeout: config.requestTimeout ?? globalConfig.requestTimeout,
489
- onError: config.onError ?? globalConfig.onError,
490
- };
491
-
492
- return checkForUpdate(mergedConfig);
493
- },
386
+ checkForUpdate: check,
494
387
 
495
388
  /**
496
389
  * Updates the bundle of the app.
@@ -501,7 +394,7 @@ function createHotUpdaterClient() {
501
394
  *
502
395
  * @example
503
396
  * ```ts
504
- * const updateInfo = await HotUpdater.checkForUpdate({
397
+ * const updateInfo = await hotUpdater.checkForUpdate({
505
398
  * updateStrategy: "appVersion",
506
399
  * requestHeaders: {
507
400
  * "x-api-key": "<your-api-key>",
@@ -516,12 +409,11 @@ function createHotUpdaterClient() {
516
409
  *
517
410
  * await updateInfo.updateBundle();
518
411
  * if (updateInfo.shouldForceUpdate) {
519
- * await HotUpdater.reload();
412
+ * await hotUpdater.reload();
520
413
  * }
521
414
  * ```
522
415
  */
523
416
  updateBundle: async (params: UpdateParams) => {
524
- ensureGlobalClient("updateBundle");
525
417
  const fromBundleId = getBundleId();
526
418
  const state = getActiveUpdateState();
527
419
  const active = state.activeSelection;
@@ -538,7 +430,7 @@ function createHotUpdaterClient() {
538
430
  try {
539
431
  delivery = await stageBundle(params);
540
432
  } catch (error) {
541
- reportUpdateError(error, "download", undefined, {
433
+ reportUpdateError(config.launch.emit, error, "download", undefined, {
542
434
  bundleId: fromBundleId,
543
435
  channel,
544
436
  targetBundleId: params.bundleId,
@@ -553,7 +445,7 @@ function createHotUpdaterClient() {
553
445
  throw error;
554
446
  }
555
447
  if (delivery !== null && params.bundleId !== fromBundleId) {
556
- emitAfterAppReady("onBundleDownloaded", () => {
448
+ config.launch.emit("onBundleDownloaded", () => {
557
449
  const selection = getActiveUpdateState().activeSelection;
558
450
  return {
559
451
  channel,
@@ -570,81 +462,111 @@ function createHotUpdaterClient() {
570
462
  },
571
463
 
572
464
  /**
573
- * Clears the runtime channel override and restores the original bundle.
574
- *
575
- * @returns {Promise<boolean>} Resolves with true if reset was successful
576
- */
577
- resetChannel: async () => {
578
- const ok = await resetChannel();
579
- if (ok) {
580
- hotUpdaterStore.setState({
581
- artifactType: null,
582
- details: null,
583
- isUpdateDownloaded: false,
584
- progress: 0,
585
- });
586
- }
587
- return ok;
588
- },
589
-
590
- /**
591
- * Fetches the fingerprint of the app.
592
- *
593
- * @returns {string} The fingerprint of the app
465
+ * Wraps the app's root: when it mounts, it checks for an update with
466
+ * this instance's configuration, downloads one, and reloads for a
467
+ * forced update. A `fallbackComponent` replaces the root until the
468
+ * check and a forced update finish.
594
469
  *
595
470
  * @example
596
- * ```ts
597
- * const fingerprint = HotUpdater.getFingerprintHash();
598
- * console.log(`Fingerprint: ${fingerprint}`);
471
+ * ```tsx
472
+ * export default hotUpdater.wrap({
473
+ * updateStrategy: "appVersion",
474
+ * fallbackComponent: ({ progress }) => <Splash progress={progress} />,
475
+ * })(App);
599
476
  * ```
600
477
  */
601
- getFingerprintHash,
602
-
603
- /**
604
- * Fetches the persisted install id for this app installation.
605
- */
606
- getInstallId,
607
-
608
- /**
609
- * Reads the native launch report for the current process.
610
- */
611
- notifyAppReady,
478
+ wrap: (options: HotUpdaterWrapOptions) =>
479
+ wrap({
480
+ ...options,
481
+ checkForUpdate: check,
482
+ appReady: () => config.launch.appReady,
483
+ onError: (error) => config.onError?.(error),
484
+ }),
485
+ };
486
+ };
612
487
 
613
- /**
614
- * Gets the list of bundle IDs that have been marked as crashed.
615
- * These bundles will be rejected if attempted to install again.
616
- *
617
- * @returns {string[]} Array of crashed bundle IDs
618
- *
619
- * @example
620
- * ```ts
621
- * const crashedBundles = HotUpdater.getCrashHistory();
622
- * console.log("Crashed bundles:", crashedBundles);
623
- * ```
624
- */
625
- getCrashHistory,
488
+ /** The methods of every instance `HotUpdater.init` returns. */
489
+ export type HotUpdaterCore = typeof nativeMethods &
490
+ ReturnType<typeof createConfiguredMethods>;
626
491
 
627
- /**
628
- * Clears the crashed bundle history, allowing previously crashed bundles
629
- * to be installed again.
630
- *
631
- * @returns {boolean} true if clearing was successful
632
- *
633
- * @example
634
- * ```ts
635
- * // Clear crash history to allow retrying a previously failed bundle
636
- * HotUpdater.clearCrashHistory();
637
- * ```
638
- */
639
- clearCrashHistory,
640
- };
641
- }
492
+ /**
493
+ * The instance `HotUpdater.init` returns: HotUpdater's methods, and each
494
+ * plugin's API under the plugin's id.
495
+ */
496
+ export type HotUpdaterInstance<
497
+ TPlugins extends readonly HotUpdaterClientPlugin[] = readonly [],
498
+ > = Readonly<HotUpdaterCore> & ClientPluginApis<TPlugins>;
499
+
500
+ export const HotUpdater = {
501
+ /**
502
+ * Creates the app's HotUpdater instance: the update server, its request
503
+ * settings, and the client plugins. Each call returns an independent
504
+ * instance with its own configuration and plugins, so create one, at the
505
+ * top level of a module, and export it.
506
+ *
507
+ * The instance has every HotUpdater method, such as `wrap`,
508
+ * `checkForUpdate`, and `reload`, and each plugin's API under the
509
+ * plugin's id, such as `hotUpdater.remoteConfig` for `remoteConfig()`.
510
+ *
511
+ * @example
512
+ * ```tsx
513
+ * export const hotUpdater = HotUpdater.init({
514
+ * baseURL: "<your-update-server-url>",
515
+ * plugins: [remoteConfig({ defaults: { welcome_message: "Hi" } })],
516
+ * });
517
+ *
518
+ * hotUpdater.remoteConfig.getString("welcome_message");
519
+ * ```
520
+ */
521
+ init: <
522
+ const TPlugins extends readonly HotUpdaterClientPlugin[] = readonly [],
523
+ >(
524
+ options: HotUpdaterInitOptions<TPlugins>,
525
+ ): HotUpdaterInstance<TPlugins> => {
526
+ const {
527
+ baseURL,
528
+ plugins,
529
+ requestHeaders,
530
+ requestTimeout,
531
+ onError,
532
+ onNotifyAppReady,
533
+ } = options;
534
+ if (!baseURL) throw createMissingNetworkConfigError();
535
+
536
+ const host = createAppPluginHost();
537
+ const launch = createLaunchReporter(host);
538
+ const instance = {
539
+ ...nativeMethods,
540
+ ...createConfiguredMethods({
541
+ client: createHttpClient(baseURL, (response) => {
542
+ launch.emit("onHttpResponse", () => response);
543
+ }),
544
+ requestHeaders,
545
+ requestTimeout,
546
+ onError,
547
+ launch,
548
+ }),
549
+ };
550
+ const reservedIds = new Set([...Object.keys(instance), "init"]);
551
+ for (const plugin of plugins ?? []) {
552
+ if (reservedIds.has(plugin.id)) {
553
+ throw new Error(
554
+ `[HotUpdater] A plugin cannot use the id "${plugin.id}": the HotUpdater instance has its own "${plugin.id}".`,
555
+ );
556
+ }
557
+ }
642
558
 
643
- type HotUpdaterClient = Omit<
644
- ReturnType<typeof createHotUpdaterClient>,
645
- "wrap"
646
- > & {
647
- wrap: HotUpdaterWrap;
559
+ // Plugins are set up before the instance reads the launch they observe.
560
+ const apis = host.configurePlugins(plugins, {
561
+ baseURL,
562
+ requestHeaders,
563
+ requestTimeout,
564
+ onError,
565
+ });
566
+ void launch.read({ onNotifyAppReady, onError });
567
+ return Object.freeze({
568
+ ...instance,
569
+ ...apis,
570
+ }) as HotUpdaterInstance<TPlugins>;
571
+ },
648
572
  };
649
-
650
- export const HotUpdater: HotUpdaterClient = createHotUpdaterClient();