@hot-updater/react-native 0.36.7 → 1.0.0-rc.1

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 (239) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +357 -6
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +268 -34
  3. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +75 -9
  4. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +2 -16
  5. package/android/src/main/java/com/hotupdater/ReleaseCatalogCacheService.kt +166 -0
  6. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +1 -2
  7. package/android/src/newarch/HotUpdater.kt +1 -1
  8. package/android/src/newarch/HotUpdaterModule.kt +116 -0
  9. package/android/src/oldarch/HotUpdater.kt +1 -1
  10. package/android/src/oldarch/HotUpdaterModule.kt +128 -0
  11. package/android/src/oldarch/HotUpdaterSpec.kt +38 -0
  12. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +280 -2
  13. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +57 -2
  14. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +118 -0
  15. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +491 -30
  16. package/ios/HotUpdater/Internal/BundleMetadata.swift +229 -6
  17. package/ios/HotUpdater/Internal/HotUpdater.mm +122 -0
  18. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +176 -7
  19. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +213 -0
  20. package/lib/commonjs/catalogCacheNative.js +44 -0
  21. package/lib/commonjs/catalogCacheNative.js.map +1 -0
  22. package/lib/commonjs/catalogCacheNative.spec.js +31 -0
  23. package/lib/commonjs/catalogCacheNative.spec.js.map +1 -0
  24. package/lib/commonjs/checkForUpdate.js +231 -60
  25. package/lib/commonjs/checkForUpdate.js.map +1 -1
  26. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +604 -0
  27. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  28. package/lib/commonjs/error.js +8 -1
  29. package/lib/commonjs/error.js.map +1 -1
  30. package/lib/commonjs/{fetchUpdateInfo.js → fetchJSON.js} +11 -14
  31. package/lib/commonjs/fetchJSON.js.map +1 -0
  32. package/lib/commonjs/httpClient.js +153 -0
  33. package/lib/commonjs/httpClient.js.map +1 -0
  34. package/lib/commonjs/httpClient.spec.js +102 -0
  35. package/lib/commonjs/httpClient.spec.js.map +1 -0
  36. package/lib/commonjs/index.js +47 -70
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/index.spec.js +115 -159
  39. package/lib/commonjs/index.spec.js.map +1 -1
  40. package/lib/commonjs/iosInterop.spec.js +18 -0
  41. package/lib/commonjs/iosInterop.spec.js.map +1 -0
  42. package/lib/commonjs/native.js +222 -53
  43. package/lib/commonjs/native.js.map +1 -1
  44. package/lib/commonjs/native.spec.js +226 -12
  45. package/lib/commonjs/native.spec.js.map +1 -1
  46. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js +218 -0
  47. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  48. package/lib/commonjs/notifyAppReadyAnalytics.js +133 -0
  49. package/lib/commonjs/notifyAppReadyAnalytics.js.map +1 -0
  50. package/lib/commonjs/notifyAppReadyAnalytics.spec.js +246 -0
  51. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +1 -0
  52. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js +24 -0
  53. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  54. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js +117 -0
  55. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  56. package/lib/commonjs/releaseCatalogCache.js +179 -0
  57. package/lib/commonjs/releaseCatalogCache.js.map +1 -0
  58. package/lib/commonjs/releaseCatalogCache.spec.js +195 -0
  59. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -0
  60. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +64 -0
  61. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  62. package/lib/commonjs/sdkVersion.js +1 -1
  63. package/lib/commonjs/sdkVersion.js.map +1 -1
  64. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  65. package/lib/commonjs/types.js +0 -12
  66. package/lib/commonjs/types.js.map +1 -1
  67. package/lib/commonjs/wrap.js +7 -82
  68. package/lib/commonjs/wrap.js.map +1 -1
  69. package/lib/commonjs/wrap.spec.js +203 -56
  70. package/lib/commonjs/wrap.spec.js.map +1 -1
  71. package/lib/commonjs/wrap.types.js +6 -0
  72. package/lib/commonjs/wrap.types.js.map +1 -0
  73. package/lib/module/catalogCacheNative.js +36 -0
  74. package/lib/module/catalogCacheNative.js.map +1 -0
  75. package/lib/module/catalogCacheNative.spec.js +31 -0
  76. package/lib/module/catalogCacheNative.spec.js.map +1 -0
  77. package/lib/module/checkForUpdate.js +233 -62
  78. package/lib/module/checkForUpdate.js.map +1 -1
  79. package/lib/module/checkForUpdate.releaseCatalog.spec.js +603 -0
  80. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -0
  81. package/lib/module/error.js +6 -0
  82. package/lib/module/error.js.map +1 -1
  83. package/lib/module/{fetchUpdateInfo.js → fetchJSON.js} +9 -12
  84. package/lib/module/fetchJSON.js.map +1 -0
  85. package/lib/module/httpClient.js +148 -0
  86. package/lib/module/httpClient.js.map +1 -0
  87. package/lib/module/httpClient.spec.js +102 -0
  88. package/lib/module/httpClient.spec.js.map +1 -0
  89. package/lib/module/index.js +48 -65
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/index.spec.js +115 -159
  92. package/lib/module/index.spec.js.map +1 -1
  93. package/lib/module/iosInterop.spec.js +18 -0
  94. package/lib/module/iosInterop.spec.js.map +1 -0
  95. package/lib/module/native.js +206 -49
  96. package/lib/module/native.js.map +1 -1
  97. package/lib/module/native.spec.js +226 -12
  98. package/lib/module/native.spec.js.map +1 -1
  99. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js +217 -0
  100. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +1 -0
  101. package/lib/module/notifyAppReadyAnalytics.js +128 -0
  102. package/lib/module/notifyAppReadyAnalytics.js.map +1 -0
  103. package/lib/module/notifyAppReadyAnalytics.spec.js +245 -0
  104. package/lib/module/notifyAppReadyAnalytics.spec.js.map +1 -0
  105. package/lib/module/notifyAppReadyAnalytics.test-utils.js +18 -0
  106. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +1 -0
  107. package/lib/module/notifyAppReadyAnalytics.transport.spec.js +116 -0
  108. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +1 -0
  109. package/lib/module/releaseCatalogCache.js +171 -0
  110. package/lib/module/releaseCatalogCache.js.map +1 -0
  111. package/lib/module/releaseCatalogCache.spec.js +195 -0
  112. package/lib/module/releaseCatalogCache.spec.js.map +1 -0
  113. package/lib/module/releaseCatalogCacheNativeContract.spec.js +64 -0
  114. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +1 -0
  115. package/lib/module/sdkVersion.js +1 -1
  116. package/lib/module/sdkVersion.js.map +1 -1
  117. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  118. package/lib/module/types.js +0 -12
  119. package/lib/module/types.js.map +1 -1
  120. package/lib/module/wrap.js +6 -81
  121. package/lib/module/wrap.js.map +1 -1
  122. package/lib/module/wrap.spec.js +203 -56
  123. package/lib/module/wrap.spec.js.map +1 -1
  124. package/lib/module/wrap.types.js +4 -0
  125. package/lib/module/wrap.types.js.map +1 -0
  126. package/lib/typescript/commonjs/catalogCacheNative.d.ts +4 -0
  127. package/lib/typescript/commonjs/catalogCacheNative.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/checkForUpdate.d.ts +14 -4
  129. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/error.d.ts +3 -0
  131. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/fetchJSON.d.ts +6 -0
  133. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/httpClient.d.ts +56 -0
  135. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/index.d.ts +31 -40
  137. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/native.d.ts +84 -17
  139. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +13 -0
  141. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  143. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +25 -0
  145. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  147. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +40 -5
  149. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/types.d.ts +0 -118
  151. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/wrap.d.ts +2 -191
  153. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/wrap.types.d.ts +65 -0
  155. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -0
  156. package/lib/typescript/module/catalogCacheNative.d.ts +4 -0
  157. package/lib/typescript/module/catalogCacheNative.d.ts.map +1 -0
  158. package/lib/typescript/module/checkForUpdate.d.ts +14 -4
  159. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  160. package/lib/typescript/module/error.d.ts +3 -0
  161. package/lib/typescript/module/error.d.ts.map +1 -1
  162. package/lib/typescript/module/fetchJSON.d.ts +6 -0
  163. package/lib/typescript/module/fetchJSON.d.ts.map +1 -0
  164. package/lib/typescript/module/httpClient.d.ts +56 -0
  165. package/lib/typescript/module/httpClient.d.ts.map +1 -0
  166. package/lib/typescript/module/index.d.ts +31 -40
  167. package/lib/typescript/module/index.d.ts.map +1 -1
  168. package/lib/typescript/module/native.d.ts +84 -17
  169. package/lib/typescript/module/native.d.ts.map +1 -1
  170. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +13 -0
  171. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +1 -0
  172. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +8 -0
  173. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +1 -0
  174. package/lib/typescript/module/releaseCatalogCache.d.ts +25 -0
  175. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -0
  176. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  177. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  178. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +40 -5
  179. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  180. package/lib/typescript/module/types.d.ts +0 -118
  181. package/lib/typescript/module/types.d.ts.map +1 -1
  182. package/lib/typescript/module/wrap.d.ts +2 -191
  183. package/lib/typescript/module/wrap.d.ts.map +1 -1
  184. package/lib/typescript/module/wrap.types.d.ts +65 -0
  185. package/lib/typescript/module/wrap.types.d.ts.map +1 -0
  186. package/package.json +6 -24
  187. package/src/catalogCacheNative.spec.ts +63 -0
  188. package/src/catalogCacheNative.ts +74 -0
  189. package/src/checkForUpdate.releaseCatalog.spec.ts +596 -0
  190. package/src/checkForUpdate.ts +367 -96
  191. package/src/error.ts +7 -0
  192. package/src/{fetchUpdateInfo.ts → fetchJSON.ts} +8 -15
  193. package/src/httpClient.spec.ts +132 -0
  194. package/src/httpClient.ts +261 -0
  195. package/src/index.spec.ts +108 -201
  196. package/src/index.ts +68 -91
  197. package/src/iosInterop.spec.ts +30 -0
  198. package/src/native.spec.ts +226 -10
  199. package/src/native.ts +385 -81
  200. package/src/notifyAppReadyAnalytics.boundary.spec.ts +234 -0
  201. package/src/notifyAppReadyAnalytics.spec.ts +243 -0
  202. package/src/notifyAppReadyAnalytics.test-utils.ts +31 -0
  203. package/src/notifyAppReadyAnalytics.transport.spec.ts +130 -0
  204. package/src/notifyAppReadyAnalytics.ts +194 -0
  205. package/src/releaseCatalogCache.spec.ts +282 -0
  206. package/src/releaseCatalogCache.ts +341 -0
  207. package/src/releaseCatalogCacheNativeContract.spec.ts +80 -0
  208. package/src/sdkVersion.ts +1 -1
  209. package/src/specs/NativeHotUpdater.ts +51 -5
  210. package/src/types.ts +0 -140
  211. package/src/wrap.spec.ts +251 -56
  212. package/src/wrap.tsx +23 -318
  213. package/src/wrap.types.ts +78 -0
  214. package/android/src/main/java/com/hotupdater/StringResourceUtils.kt +0 -45
  215. package/app.plugin.js +0 -1
  216. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +0 -7
  217. package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +0 -4
  218. package/lib/commonjs/DefaultResolver.js +0 -50
  219. package/lib/commonjs/DefaultResolver.js.map +0 -1
  220. package/lib/commonjs/DefaultResolver.spec.js +0 -144
  221. package/lib/commonjs/DefaultResolver.spec.js.map +0 -1
  222. package/lib/commonjs/fetchUpdateInfo.js.map +0 -1
  223. package/lib/module/DefaultResolver.js +0 -46
  224. package/lib/module/DefaultResolver.js.map +0 -1
  225. package/lib/module/DefaultResolver.spec.js +0 -144
  226. package/lib/module/DefaultResolver.spec.js.map +0 -1
  227. package/lib/module/fetchUpdateInfo.js.map +0 -1
  228. package/lib/typescript/commonjs/DefaultResolver.d.ts +0 -11
  229. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +0 -1
  230. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +0 -7
  231. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +0 -1
  232. package/lib/typescript/module/DefaultResolver.d.ts +0 -11
  233. package/lib/typescript/module/DefaultResolver.d.ts.map +0 -1
  234. package/lib/typescript/module/fetchUpdateInfo.d.ts +0 -7
  235. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +0 -1
  236. package/plugin/build/transformers.js +0 -314
  237. package/plugin/build/withHotUpdater.js +0 -228
  238. package/src/DefaultResolver.spec.ts +0 -193
  239. package/src/DefaultResolver.ts +0 -63
package/src/native.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  type ChangedAsset,
3
+ type CatalogHighWater,
3
4
  INVALID_COHORT_ERROR_MESSAGE,
4
5
  isValidCohort,
5
6
  normalizeCohortValue,
6
7
  type UpdateStatus,
8
+ type PersistedSelectionReceipt,
7
9
  } from "@hot-updater/core";
8
10
  import { NativeEventEmitter, Platform } from "react-native";
9
11
 
@@ -15,6 +17,8 @@ import HotUpdaterNative, {
15
17
  export { HotUpdaterErrorCode, isHotUpdaterError };
16
18
 
17
19
  const NIL_UUID = "00000000-0000-0000-0000-000000000000";
20
+ type PersistedUpdateStrategy = "fingerprint" | "appVersion";
21
+
18
22
  const normalizeAndValidateCohort = (cohort: string): string => {
19
23
  const normalized = normalizeCohortValue(cohort);
20
24
  if (!isValidCohort(normalized)) {
@@ -317,6 +321,109 @@ export type UpdateParams = UpdateBundleParams & {
317
321
  shouldSkipCurrentBundleIdCheck?: boolean;
318
322
  };
319
323
 
324
+ export interface ActiveUpdateState {
325
+ readonly activeSelection: ActiveUpdateSelection | null;
326
+ readonly stableSelection: ActiveUpdateSelection | null;
327
+ readonly verificationPending: boolean;
328
+ }
329
+
330
+ type ActiveUpdateSelection = Pick<
331
+ PersistedSelectionReceipt,
332
+ "kind" | "releaseId" | "bundleId" | "channel"
333
+ >;
334
+
335
+ export interface InternalActiveUpdateState {
336
+ readonly activeSelection: PersistedSelectionReceipt | null;
337
+ readonly stableSelection: PersistedSelectionReceipt | null;
338
+ readonly verificationPending: boolean;
339
+ readonly highestSeenCatalogs: Readonly<Record<string, CatalogHighWater>>;
340
+ }
341
+
342
+ export interface ReleaseSelectionGuard {
343
+ readonly catalogId: string;
344
+ readonly scopeKey: string;
345
+ readonly generation: number;
346
+ readonly catalogHash: string;
347
+ readonly selectionContextHash: string;
348
+ readonly channel: string;
349
+ }
350
+
351
+ const requireReleaseCatalogNativeMethod = <T extends (...args: any[]) => any>(
352
+ name: string,
353
+ ): T => {
354
+ const method = (HotUpdaterNative as unknown as Record<string, unknown>)[name];
355
+ if (typeof method !== "function") {
356
+ throw new Error(
357
+ `[HotUpdater] Native module is missing '${name}()'. Rebuild the native app before using Release catalogs.`,
358
+ );
359
+ }
360
+ return method.bind(HotUpdaterNative) as T;
361
+ };
362
+
363
+ export const acceptReleaseCatalog = (input: {
364
+ readonly catalogId: string;
365
+ readonly scopeKey: string;
366
+ readonly generation: number;
367
+ readonly catalogHash: string;
368
+ readonly channel: string;
369
+ readonly selectionContextHash: string;
370
+ }): boolean =>
371
+ requireReleaseCatalogNativeMethod<(params: object) => boolean>(
372
+ "acceptReleaseCatalog",
373
+ )(input);
374
+
375
+ export const getActiveUpdateState = (): InternalActiveUpdateState => {
376
+ const value = requireReleaseCatalogNativeMethod<() => unknown>(
377
+ "getActiveUpdateState",
378
+ )();
379
+ if (typeof value === "string") {
380
+ return JSON.parse(value) as InternalActiveUpdateState;
381
+ }
382
+ return value as InternalActiveUpdateState;
383
+ };
384
+
385
+ export const getPublicActiveUpdateState = (): ActiveUpdateState => {
386
+ const state = getActiveUpdateState();
387
+ const selection = (
388
+ receipt: PersistedSelectionReceipt | null,
389
+ ): ActiveUpdateSelection | null =>
390
+ receipt === null
391
+ ? null
392
+ : {
393
+ kind: receipt.kind,
394
+ releaseId: receipt.releaseId,
395
+ bundleId: receipt.bundleId,
396
+ channel: receipt.channel,
397
+ };
398
+ return {
399
+ activeSelection: selection(state.activeSelection),
400
+ stableSelection: selection(state.stableSelection),
401
+ verificationPending: state.verificationPending,
402
+ };
403
+ };
404
+
405
+ export const isReleaseSelectionCurrent = (
406
+ input: ReleaseSelectionGuard,
407
+ ): boolean =>
408
+ requireReleaseCatalogNativeMethod<(params: object) => boolean>(
409
+ "isReleaseSelectionCurrent",
410
+ )(input);
411
+
412
+ export const commitReleaseSelection = async (input: {
413
+ readonly selection: PersistedSelectionReceipt;
414
+ readonly guard: ReleaseSelectionGuard;
415
+ }): Promise<boolean> => {
416
+ const ok = await requireReleaseCatalogNativeMethod<
417
+ (params: object) => Promise<boolean>
418
+ >("commitReleaseSelection")(input);
419
+ if (ok)
420
+ sessionState.markBundleInstalled(
421
+ input.selection.bundleId,
422
+ input.selection.channel,
423
+ );
424
+ return ok;
425
+ };
426
+
320
427
  /**
321
428
  * Downloads files and applies them to the app.
322
429
  *
@@ -324,30 +431,10 @@ export type UpdateParams = UpdateBundleParams & {
324
431
  * @returns {Promise<boolean>} Resolves with true if download was successful
325
432
  * @throws {Error} Rejects with error.code from HotUpdaterErrorCode enum and error.message
326
433
  */
327
- export async function updateBundle(params: UpdateParams): Promise<boolean>;
328
- /**
329
- * @deprecated Use updateBundle(params: UpdateBundleParamsWithStatus) instead
330
- */
331
- export async function updateBundle(
332
- bundleId: string,
333
- fileUrl: string | null,
334
- ): Promise<boolean>;
335
- export async function updateBundle(
336
- paramsOrBundleId: UpdateParams | string,
337
- fileUrl?: string | null,
338
- ): Promise<boolean> {
339
- const updateBundleId =
340
- typeof paramsOrBundleId === "string"
341
- ? paramsOrBundleId
342
- : paramsOrBundleId.bundleId;
343
-
344
- const status =
345
- typeof paramsOrBundleId === "string" ? "UPDATE" : paramsOrBundleId.status;
346
-
347
- const targetFileUrl =
348
- typeof paramsOrBundleId === "string"
349
- ? (fileUrl ?? null)
350
- : paramsOrBundleId.fileUrl;
434
+ export async function updateBundle(params: UpdateParams): Promise<boolean> {
435
+ const updateBundleId = params.bundleId;
436
+ const status = params.status;
437
+ const targetFileUrl = params.fileUrl;
351
438
 
352
439
  const currentBundleId = status === "UPDATE" ? getFreshBundleId() : undefined;
353
440
 
@@ -363,9 +450,7 @@ export async function updateBundle(
363
450
  }
364
451
 
365
452
  const shouldSkipCurrentBundleIdCheck =
366
- typeof paramsOrBundleId === "string"
367
- ? false
368
- : paramsOrBundleId.shouldSkipCurrentBundleIdCheck === true;
453
+ params.shouldSkipCurrentBundleIdCheck === true;
369
454
 
370
455
  if (
371
456
  !shouldSkipCurrentBundleIdCheck &&
@@ -382,28 +467,15 @@ export async function updateBundle(
382
467
  const existing = sessionState.getInflightUpdate(updateBundleId);
383
468
  if (existing) return existing;
384
469
 
385
- const targetFileHash =
386
- typeof paramsOrBundleId === "string"
387
- ? undefined
388
- : paramsOrBundleId.fileHash;
389
-
390
- const targetChannel =
391
- typeof paramsOrBundleId === "string" ? undefined : paramsOrBundleId.channel;
392
- const targetManifestUrl =
393
- typeof paramsOrBundleId === "string"
394
- ? undefined
395
- : paramsOrBundleId.manifestUrl;
396
- const targetManifestFileHash =
397
- typeof paramsOrBundleId === "string"
398
- ? undefined
399
- : paramsOrBundleId.manifestFileHash;
400
- const targetChangedAssets =
401
- typeof paramsOrBundleId === "string"
402
- ? undefined
403
- : paramsOrBundleId.changedAssets;
470
+ const targetFileHash = params.fileHash;
471
+ const targetChannel = params.channel;
472
+ const targetManifestUrl = params.manifestUrl;
473
+ const targetManifestFileHash = params.manifestFileHash;
474
+ const targetChangedAssets = params.changedAssets;
404
475
 
405
476
  const promise = (async () => {
406
477
  try {
478
+ const selection = params.selection;
407
479
  const ok = await HotUpdaterNative.updateBundle({
408
480
  bundleId: updateBundleId,
409
481
  channel: targetChannel,
@@ -413,6 +485,7 @@ export async function updateBundle(
413
485
  fileHash: targetFileHash ?? null,
414
486
  manifestFileHash: targetManifestFileHash ?? null,
415
487
  manifestUrl: targetManifestUrl ?? null,
488
+ ...(selection === undefined ? {} : { selection }),
416
489
  });
417
490
  if (ok) {
418
491
  sessionState.markBundleInstalled(updateBundleId, targetChannel);
@@ -515,16 +588,20 @@ export function setReloadBehavior(
515
588
  }
516
589
 
517
590
  /**
518
- * Fetches the minimum bundle id, which represents the initial bundle of the app
519
- * since it is created at build time.
591
+ * Fetches the minimum bundle id created from the native app build time.
592
+ *
593
+ * Release Catalog selection uses this value as its minimum Release id.
520
594
  *
521
- * @returns {string} Resolves with the minimum bundle id or null if not available.
595
+ * @returns {string} The minimum bundle id.
522
596
  */
523
597
  export const getMinBundleId = (): string => {
524
598
  const constants = HotUpdaterNative.getConstants();
525
599
  return constants.MIN_BUNDLE_ID;
526
600
  };
527
601
 
602
+ export const getReleaseId = async (): Promise<string | null> =>
603
+ getActiveUpdateState().activeSelection?.releaseId ?? null;
604
+
528
605
  /**
529
606
  * Fetches the current bundle version id.
530
607
  *
@@ -613,12 +690,264 @@ export const getFingerprintHash = (): string | null => {
613
690
  return constants.FINGERPRINT_HASH;
614
691
  };
615
692
 
693
+ export const getInstallId = (): string => {
694
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
695
+ getInstallId?: () => string;
696
+ };
697
+
698
+ if (typeof nativeModule.getInstallId !== "function") {
699
+ throw new Error(
700
+ "[HotUpdater] Native module is missing 'getInstallId()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
701
+ );
702
+ }
703
+
704
+ return nativeModule.getInstallId();
705
+ };
706
+
707
+ export type PersistedUserIdentity = {
708
+ userId?: string;
709
+ username?: string;
710
+ };
711
+
712
+ export const getPersistedUserIdentity = (): PersistedUserIdentity => {
713
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
714
+ getUserId?: () => string | null;
715
+ getUsername?: () => string | null;
716
+ };
717
+
718
+ if (
719
+ typeof nativeModule.getUserId !== "function" ||
720
+ typeof nativeModule.getUsername !== "function"
721
+ ) {
722
+ throw new Error(
723
+ "[HotUpdater] Native module is missing 'getUserId()' or 'getUsername()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
724
+ );
725
+ }
726
+
727
+ const userId = nativeModule.getUserId();
728
+ const username = nativeModule.getUsername();
729
+
730
+ return {
731
+ ...(userId != null ? { userId } : {}),
732
+ ...(username != null ? { username } : {}),
733
+ };
734
+ };
735
+
736
+ export type SetUserParams = {
737
+ userId?: string | number | null;
738
+ username?: string | null;
739
+ };
740
+
741
+ export function setUser(params: SetUserParams): void;
742
+ export function setUser(params: null): void;
743
+ export function setUser(params: SetUserParams | null): void {
744
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
745
+ setUser?: (userId: string | null, username: string | null) => void;
746
+ };
747
+
748
+ if (typeof nativeModule.setUser !== "function") {
749
+ throw new Error(
750
+ "[HotUpdater] Native module is missing 'setUser()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
751
+ );
752
+ }
753
+
754
+ if (params === null) {
755
+ nativeModule.setUser(null, null);
756
+ return;
757
+ }
758
+
759
+ const normalizedUserId =
760
+ params.userId === null || params.userId === undefined
761
+ ? null
762
+ : String(params.userId);
763
+ nativeModule.setUser(normalizedUserId, params.username ?? null);
764
+ }
765
+
616
766
  /**
617
767
  * Result returned by notifyAppReady()
618
768
  */
619
- export type NotifyAppReadyResult = {
620
- status: "RECOVERED" | "STABLE";
769
+ export type NotifyAppReadyResult =
770
+ | { status: "UNCHANGED" }
771
+ | {
772
+ status: "UPDATE_APPLIED";
773
+ fromBundleId: string;
774
+ toBundleId: string;
775
+ fromReleaseId?: string;
776
+ toReleaseId?: string;
777
+ }
778
+ | {
779
+ status: "RECOVERED";
780
+ fromBundleId: string;
781
+ toBundleId: string;
782
+ fromReleaseId?: string;
783
+ toReleaseId?: string;
784
+ };
785
+
786
+ export type NotifyAppReadyAnalyticsEvent = {
787
+ type: "UPDATE_APPLIED" | "RECOVERED";
788
+ fromBundleId: string;
789
+ toBundleId: string;
790
+ fromReleaseId?: string | null;
791
+ toReleaseId?: string | null;
792
+ updateStrategy: PersistedUpdateStrategy;
793
+ };
794
+
795
+ type RawNotifyAppReadyResult = {
796
+ status?: string;
621
797
  crashedBundleId?: string;
798
+ fromReleaseId?: string;
799
+ fromBundleId?: string;
800
+ toReleaseId?: string;
801
+ toBundleId?: string;
802
+ updateStrategy?: PersistedUpdateStrategy;
803
+ };
804
+
805
+ const isPersistedUpdateStrategy = (
806
+ value: unknown,
807
+ ): value is PersistedUpdateStrategy => {
808
+ return value === "appVersion" || value === "fingerprint";
809
+ };
810
+
811
+ const readRawNotifyAppReadyResult = (): RawNotifyAppReadyResult => {
812
+ const result = HotUpdaterNative.notifyAppReady();
813
+
814
+ if (typeof result === "string") {
815
+ try {
816
+ const parsed = JSON.parse(result) as unknown;
817
+ if (
818
+ typeof parsed !== "object" ||
819
+ parsed === null ||
820
+ Array.isArray(parsed)
821
+ ) {
822
+ return {};
823
+ }
824
+ return parsed as RawNotifyAppReadyResult;
825
+ } catch {
826
+ return {};
827
+ }
828
+ }
829
+
830
+ if (typeof result !== "object" || result === null || Array.isArray(result)) {
831
+ return {};
832
+ }
833
+
834
+ return result as RawNotifyAppReadyResult;
835
+ };
836
+
837
+ const readDirectionalBundleId = (bundleId: unknown): string | null => {
838
+ if (typeof bundleId !== "string") {
839
+ return null;
840
+ }
841
+ const normalizedBundleId = bundleId.trim();
842
+ if (normalizedBundleId.length === 0) {
843
+ return null;
844
+ }
845
+ return resolveBundleId(normalizedBundleId);
846
+ };
847
+
848
+ const readDirectionalReleaseId = (releaseId: unknown): string | null => {
849
+ if (typeof releaseId !== "string") return null;
850
+ const normalizedReleaseId = releaseId.trim();
851
+ return normalizedReleaseId.length === 0 ? null : normalizedReleaseId;
852
+ };
853
+
854
+ const getNotifyAppReadyTransition = (
855
+ result: RawNotifyAppReadyResult,
856
+ ): {
857
+ status: "UPDATE_APPLIED" | "RECOVERED";
858
+ fromBundleId: string;
859
+ toBundleId: string;
860
+ fromReleaseId: string | null;
861
+ toReleaseId: string | null;
862
+ } | null => {
863
+ if (result.status === "UPDATE_APPLIED" || result.status === "PROMOTED") {
864
+ const fromBundleId = readDirectionalBundleId(result.fromBundleId);
865
+ const toBundleId = readDirectionalBundleId(result.toBundleId);
866
+ if (fromBundleId && toBundleId) {
867
+ return {
868
+ status: "UPDATE_APPLIED",
869
+ fromBundleId,
870
+ toBundleId,
871
+ fromReleaseId: readDirectionalReleaseId(result.fromReleaseId),
872
+ toReleaseId: readDirectionalReleaseId(result.toReleaseId),
873
+ };
874
+ }
875
+ }
876
+
877
+ if (result.status === "RECOVERED") {
878
+ const fromBundleId = readDirectionalBundleId(result.fromBundleId);
879
+ const toBundleId = readDirectionalBundleId(result.toBundleId);
880
+ if (fromBundleId && toBundleId) {
881
+ return {
882
+ status: "RECOVERED",
883
+ fromBundleId,
884
+ toBundleId,
885
+ fromReleaseId: readDirectionalReleaseId(result.fromReleaseId),
886
+ toReleaseId: readDirectionalReleaseId(result.toReleaseId),
887
+ };
888
+ }
889
+ }
890
+
891
+ return null;
892
+ };
893
+
894
+ const getNotifyAppReadyAnalyticsEvent = (
895
+ result: RawNotifyAppReadyResult,
896
+ ): NotifyAppReadyAnalyticsEvent | null => {
897
+ const transition = getNotifyAppReadyTransition(result);
898
+
899
+ if (!transition || !isPersistedUpdateStrategy(result.updateStrategy)) {
900
+ return null;
901
+ }
902
+
903
+ return {
904
+ type: transition.status,
905
+ fromBundleId: transition.fromBundleId,
906
+ toBundleId: transition.toBundleId,
907
+ ...(transition.fromReleaseId === null
908
+ ? {}
909
+ : { fromReleaseId: transition.fromReleaseId }),
910
+ ...(transition.toReleaseId === null
911
+ ? {}
912
+ : { toReleaseId: transition.toReleaseId }),
913
+ updateStrategy: result.updateStrategy,
914
+ };
915
+ };
916
+
917
+ const normalizeNotifyAppReadyResult = (
918
+ result: RawNotifyAppReadyResult,
919
+ ): NotifyAppReadyResult => {
920
+ const transition = getNotifyAppReadyTransition(result);
921
+
922
+ if (transition) {
923
+ return {
924
+ status: transition.status,
925
+ fromBundleId: transition.fromBundleId,
926
+ toBundleId: transition.toBundleId,
927
+ ...(transition.fromReleaseId === null
928
+ ? {}
929
+ : { fromReleaseId: transition.fromReleaseId }),
930
+ ...(transition.toReleaseId === null
931
+ ? {}
932
+ : { toReleaseId: transition.toReleaseId }),
933
+ };
934
+ }
935
+
936
+ return { status: "UNCHANGED" };
937
+ };
938
+
939
+ export const readNotifyAppReady = (): {
940
+ result: NotifyAppReadyResult;
941
+ analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
942
+ pending: boolean;
943
+ } => {
944
+ const rawResult = readRawNotifyAppReadyResult();
945
+
946
+ return {
947
+ result: normalizeNotifyAppReadyResult(rawResult),
948
+ analyticsEvent: getNotifyAppReadyAnalyticsEvent(rawResult),
949
+ pending: rawResult.status === "PENDING",
950
+ };
622
951
  };
623
952
 
624
953
  /**
@@ -627,46 +956,21 @@ export type NotifyAppReadyResult = {
627
956
  * This function is called automatically after the app has rendered.
628
957
  *
629
958
  * @returns {NotifyAppReadyResult} Bundle state information
630
- * - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
631
- * - `status: "STABLE"` - No changes, already stable
632
- * - `crashedBundleId` - Present only when status is "RECOVERED"
959
+ * - `status: "UNCHANGED"` - No qualifying OTA transition to report
960
+ * - `status: "UPDATE_APPLIED"` - A pending update became active
961
+ * - `status: "RECOVERED"` - Crash recovery rolled back to another bundle
633
962
  *
634
963
  * @example
635
964
  * ```ts
636
965
  * const result = HotUpdater.notifyAppReady();
637
966
  *
638
967
  * if (result.status === "RECOVERED") {
639
- * // Send ROLLBACK analytics event
640
- * analytics.track("bundle_rollback", {
641
- * crashedBundleId: result.crashedBundleId,
642
- * });
968
+ * console.log(result.fromBundleId, result.toBundleId);
643
969
  * }
644
970
  * ```
645
971
  */
646
972
  export const notifyAppReady = (): NotifyAppReadyResult => {
647
- const result = HotUpdaterNative.notifyAppReady();
648
- // Older Android old-arch implementations returned JSON strings.
649
- if (typeof result === "string") {
650
- try {
651
- return normalizeNotifyAppReadyResult(JSON.parse(result));
652
- } catch {
653
- return { status: "STABLE" };
654
- }
655
- }
656
- return normalizeNotifyAppReadyResult(result);
657
- };
658
-
659
- const normalizeNotifyAppReadyResult = (
660
- result: NotifyAppReadyResult | { status?: string; crashedBundleId?: string },
661
- ): NotifyAppReadyResult => {
662
- if (result.status === "RECOVERED") {
663
- return {
664
- status: "RECOVERED",
665
- crashedBundleId: result.crashedBundleId,
666
- };
667
- }
668
-
669
- return { status: "STABLE" };
973
+ return readNotifyAppReady().result;
670
974
  };
671
975
 
672
976
  const createEmptyManifest = (): Manifest => ({