@hot-updater/react-native 0.36.6 → 1.0.0-rc.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 (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 +48 -71
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/index.spec.js +117 -161
  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 -55
  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 +49 -66
  90. package/lib/module/index.js.map +1 -1
  91. package/lib/module/index.spec.js +117 -161
  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 -51
  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 +32 -41
  137. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/native.d.ts +83 -18
  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 +32 -41
  167. package/lib/typescript/module/index.d.ts.map +1 -1
  168. package/lib/typescript/module/native.d.ts +83 -18
  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 +368 -97
  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 +110 -203
  196. package/src/index.ts +70 -93
  197. package/src/iosInterop.spec.ts +30 -0
  198. package/src/native.spec.ts +226 -10
  199. package/src/native.ts +385 -83
  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)) {
@@ -211,7 +215,7 @@ const getNativeBundleId = (): string | null => {
211
215
  };
212
216
 
213
217
  const resolveBundleId = (bundleId: string | null): string => {
214
- return !bundleId || bundleId === NIL_UUID ? getMinBundleId() : bundleId;
218
+ return !bundleId || bundleId === NIL_UUID ? getMinimumReleaseId() : bundleId;
215
219
  };
216
220
 
217
221
  const getFreshBundleId = (): string => {
@@ -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,18 @@ 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 build-time minimum Release id.
520
592
  *
521
- * @returns {string} Resolves with the minimum bundle id or null if not available.
593
+ * @returns {string} The minimum Release id.
522
594
  */
523
- export const getMinBundleId = (): string => {
595
+ export const getMinimumReleaseId = (): string => {
524
596
  const constants = HotUpdaterNative.getConstants();
525
597
  return constants.MIN_BUNDLE_ID;
526
598
  };
527
599
 
600
+ export const getReleaseId = async (): Promise<string | null> =>
601
+ getActiveUpdateState().activeSelection?.releaseId ?? null;
602
+
528
603
  /**
529
604
  * Fetches the current bundle version id.
530
605
  *
@@ -613,12 +688,264 @@ export const getFingerprintHash = (): string | null => {
613
688
  return constants.FINGERPRINT_HASH;
614
689
  };
615
690
 
691
+ export const getInstallId = (): string => {
692
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
693
+ getInstallId?: () => string;
694
+ };
695
+
696
+ if (typeof nativeModule.getInstallId !== "function") {
697
+ throw new Error(
698
+ "[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.",
699
+ );
700
+ }
701
+
702
+ return nativeModule.getInstallId();
703
+ };
704
+
705
+ export type PersistedUserIdentity = {
706
+ userId?: string;
707
+ username?: string;
708
+ };
709
+
710
+ export const getPersistedUserIdentity = (): PersistedUserIdentity => {
711
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
712
+ getUserId?: () => string | null;
713
+ getUsername?: () => string | null;
714
+ };
715
+
716
+ if (
717
+ typeof nativeModule.getUserId !== "function" ||
718
+ typeof nativeModule.getUsername !== "function"
719
+ ) {
720
+ throw new Error(
721
+ "[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.",
722
+ );
723
+ }
724
+
725
+ const userId = nativeModule.getUserId();
726
+ const username = nativeModule.getUsername();
727
+
728
+ return {
729
+ ...(userId != null ? { userId } : {}),
730
+ ...(username != null ? { username } : {}),
731
+ };
732
+ };
733
+
734
+ export type SetUserParams = {
735
+ userId?: string | number | null;
736
+ username?: string | null;
737
+ };
738
+
739
+ export function setUser(params: SetUserParams): void;
740
+ export function setUser(params: null): void;
741
+ export function setUser(params: SetUserParams | null): void {
742
+ const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
743
+ setUser?: (userId: string | null, username: string | null) => void;
744
+ };
745
+
746
+ if (typeof nativeModule.setUser !== "function") {
747
+ throw new Error(
748
+ "[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.",
749
+ );
750
+ }
751
+
752
+ if (params === null) {
753
+ nativeModule.setUser(null, null);
754
+ return;
755
+ }
756
+
757
+ const normalizedUserId =
758
+ params.userId === null || params.userId === undefined
759
+ ? null
760
+ : String(params.userId);
761
+ nativeModule.setUser(normalizedUserId, params.username ?? null);
762
+ }
763
+
616
764
  /**
617
765
  * Result returned by notifyAppReady()
618
766
  */
619
- export type NotifyAppReadyResult = {
620
- status: "RECOVERED" | "STABLE";
767
+ export type NotifyAppReadyResult =
768
+ | { status: "UNCHANGED" }
769
+ | {
770
+ status: "UPDATE_APPLIED";
771
+ fromBundleId: string;
772
+ toBundleId: string;
773
+ fromReleaseId?: string;
774
+ toReleaseId?: string;
775
+ }
776
+ | {
777
+ status: "RECOVERED";
778
+ fromBundleId: string;
779
+ toBundleId: string;
780
+ fromReleaseId?: string;
781
+ toReleaseId?: string;
782
+ };
783
+
784
+ export type NotifyAppReadyAnalyticsEvent = {
785
+ type: "UPDATE_APPLIED" | "RECOVERED";
786
+ fromBundleId: string;
787
+ toBundleId: string;
788
+ fromReleaseId?: string | null;
789
+ toReleaseId?: string | null;
790
+ updateStrategy: PersistedUpdateStrategy;
791
+ };
792
+
793
+ type RawNotifyAppReadyResult = {
794
+ status?: string;
621
795
  crashedBundleId?: string;
796
+ fromReleaseId?: string;
797
+ fromBundleId?: string;
798
+ toReleaseId?: string;
799
+ toBundleId?: string;
800
+ updateStrategy?: PersistedUpdateStrategy;
801
+ };
802
+
803
+ const isPersistedUpdateStrategy = (
804
+ value: unknown,
805
+ ): value is PersistedUpdateStrategy => {
806
+ return value === "appVersion" || value === "fingerprint";
807
+ };
808
+
809
+ const readRawNotifyAppReadyResult = (): RawNotifyAppReadyResult => {
810
+ const result = HotUpdaterNative.notifyAppReady();
811
+
812
+ if (typeof result === "string") {
813
+ try {
814
+ const parsed = JSON.parse(result) as unknown;
815
+ if (
816
+ typeof parsed !== "object" ||
817
+ parsed === null ||
818
+ Array.isArray(parsed)
819
+ ) {
820
+ return {};
821
+ }
822
+ return parsed as RawNotifyAppReadyResult;
823
+ } catch {
824
+ return {};
825
+ }
826
+ }
827
+
828
+ if (typeof result !== "object" || result === null || Array.isArray(result)) {
829
+ return {};
830
+ }
831
+
832
+ return result as RawNotifyAppReadyResult;
833
+ };
834
+
835
+ const readDirectionalBundleId = (bundleId: unknown): string | null => {
836
+ if (typeof bundleId !== "string") {
837
+ return null;
838
+ }
839
+ const normalizedBundleId = bundleId.trim();
840
+ if (normalizedBundleId.length === 0) {
841
+ return null;
842
+ }
843
+ return resolveBundleId(normalizedBundleId);
844
+ };
845
+
846
+ const readDirectionalReleaseId = (releaseId: unknown): string | null => {
847
+ if (typeof releaseId !== "string") return null;
848
+ const normalizedReleaseId = releaseId.trim();
849
+ return normalizedReleaseId.length === 0 ? null : normalizedReleaseId;
850
+ };
851
+
852
+ const getNotifyAppReadyTransition = (
853
+ result: RawNotifyAppReadyResult,
854
+ ): {
855
+ status: "UPDATE_APPLIED" | "RECOVERED";
856
+ fromBundleId: string;
857
+ toBundleId: string;
858
+ fromReleaseId: string | null;
859
+ toReleaseId: string | null;
860
+ } | null => {
861
+ if (result.status === "UPDATE_APPLIED" || result.status === "PROMOTED") {
862
+ const fromBundleId = readDirectionalBundleId(result.fromBundleId);
863
+ const toBundleId = readDirectionalBundleId(result.toBundleId);
864
+ if (fromBundleId && toBundleId) {
865
+ return {
866
+ status: "UPDATE_APPLIED",
867
+ fromBundleId,
868
+ toBundleId,
869
+ fromReleaseId: readDirectionalReleaseId(result.fromReleaseId),
870
+ toReleaseId: readDirectionalReleaseId(result.toReleaseId),
871
+ };
872
+ }
873
+ }
874
+
875
+ if (result.status === "RECOVERED") {
876
+ const fromBundleId = readDirectionalBundleId(result.fromBundleId);
877
+ const toBundleId = readDirectionalBundleId(result.toBundleId);
878
+ if (fromBundleId && toBundleId) {
879
+ return {
880
+ status: "RECOVERED",
881
+ fromBundleId,
882
+ toBundleId,
883
+ fromReleaseId: readDirectionalReleaseId(result.fromReleaseId),
884
+ toReleaseId: readDirectionalReleaseId(result.toReleaseId),
885
+ };
886
+ }
887
+ }
888
+
889
+ return null;
890
+ };
891
+
892
+ const getNotifyAppReadyAnalyticsEvent = (
893
+ result: RawNotifyAppReadyResult,
894
+ ): NotifyAppReadyAnalyticsEvent | null => {
895
+ const transition = getNotifyAppReadyTransition(result);
896
+
897
+ if (!transition || !isPersistedUpdateStrategy(result.updateStrategy)) {
898
+ return null;
899
+ }
900
+
901
+ return {
902
+ type: transition.status,
903
+ fromBundleId: transition.fromBundleId,
904
+ toBundleId: transition.toBundleId,
905
+ ...(transition.fromReleaseId === null
906
+ ? {}
907
+ : { fromReleaseId: transition.fromReleaseId }),
908
+ ...(transition.toReleaseId === null
909
+ ? {}
910
+ : { toReleaseId: transition.toReleaseId }),
911
+ updateStrategy: result.updateStrategy,
912
+ };
913
+ };
914
+
915
+ const normalizeNotifyAppReadyResult = (
916
+ result: RawNotifyAppReadyResult,
917
+ ): NotifyAppReadyResult => {
918
+ const transition = getNotifyAppReadyTransition(result);
919
+
920
+ if (transition) {
921
+ return {
922
+ status: transition.status,
923
+ fromBundleId: transition.fromBundleId,
924
+ toBundleId: transition.toBundleId,
925
+ ...(transition.fromReleaseId === null
926
+ ? {}
927
+ : { fromReleaseId: transition.fromReleaseId }),
928
+ ...(transition.toReleaseId === null
929
+ ? {}
930
+ : { toReleaseId: transition.toReleaseId }),
931
+ };
932
+ }
933
+
934
+ return { status: "UNCHANGED" };
935
+ };
936
+
937
+ export const readNotifyAppReady = (): {
938
+ result: NotifyAppReadyResult;
939
+ analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
940
+ pending: boolean;
941
+ } => {
942
+ const rawResult = readRawNotifyAppReadyResult();
943
+
944
+ return {
945
+ result: normalizeNotifyAppReadyResult(rawResult),
946
+ analyticsEvent: getNotifyAppReadyAnalyticsEvent(rawResult),
947
+ pending: rawResult.status === "PENDING",
948
+ };
622
949
  };
623
950
 
624
951
  /**
@@ -627,46 +954,21 @@ export type NotifyAppReadyResult = {
627
954
  * This function is called automatically after the app has rendered.
628
955
  *
629
956
  * @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"
957
+ * - `status: "UNCHANGED"` - No qualifying OTA transition to report
958
+ * - `status: "UPDATE_APPLIED"` - A pending update became active
959
+ * - `status: "RECOVERED"` - Crash recovery rolled back to another bundle
633
960
  *
634
961
  * @example
635
962
  * ```ts
636
963
  * const result = HotUpdater.notifyAppReady();
637
964
  *
638
965
  * if (result.status === "RECOVERED") {
639
- * // Send ROLLBACK analytics event
640
- * analytics.track("bundle_rollback", {
641
- * crashedBundleId: result.crashedBundleId,
642
- * });
966
+ * console.log(result.fromBundleId, result.toBundleId);
643
967
  * }
644
968
  * ```
645
969
  */
646
970
  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" };
971
+ return readNotifyAppReady().result;
670
972
  };
671
973
 
672
974
  const createEmptyManifest = (): Manifest => ({