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

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 (306) hide show
  1. package/android/build.gradle +13 -2
  2. package/android/proguard-rules.pro +8 -2
  3. package/android/src/bridge/BridgeReactInstance.kt +38 -0
  4. package/android/src/bridgeless/BridgeReactInstance.kt +24 -0
  5. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +94 -0
  6. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
  7. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1064 -844
  8. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +50 -11
  9. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  10. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
  11. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +46 -30
  12. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +75 -192
  13. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  14. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  15. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  16. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  17. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  18. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
  19. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  20. package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
  21. package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
  22. package/android/src/newarch/HotUpdater.kt +7 -9
  23. package/android/src/newarch/HotUpdaterModule.kt +109 -84
  24. package/android/src/newarch/JavaScriptExceptionHooks.kt +48 -0
  25. package/android/src/newarch/ReactIntegrationManager.kt +6 -32
  26. package/android/src/oldarch/HotUpdater.kt +7 -9
  27. package/android/src/oldarch/HotUpdaterModule.kt +98 -74
  28. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  29. package/android/src/oldarch/JavaScriptExceptionHooks.kt +71 -0
  30. package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
  31. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
  32. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1032 -1116
  33. package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -54
  34. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  35. package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
  36. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
  37. package/ios/HotUpdater/Internal/HotUpdater.mm +32 -67
  38. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +229 -73
  39. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  40. package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
  41. package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
  42. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
  43. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  44. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  45. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  46. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  47. package/lib/commonjs/appReady.js +102 -0
  48. package/lib/commonjs/appReady.js.map +1 -0
  49. package/lib/commonjs/checkForUpdate.js +120 -68
  50. package/lib/commonjs/checkForUpdate.js.map +1 -1
  51. package/lib/commonjs/clientPlugin.js +13 -0
  52. package/lib/commonjs/clientPlugin.js.map +1 -0
  53. package/lib/commonjs/error.js +4 -13
  54. package/lib/commonjs/error.js.map +1 -1
  55. package/lib/commonjs/fetchJSON.js +16 -27
  56. package/lib/commonjs/fetchJSON.js.map +1 -1
  57. package/lib/commonjs/fetchUpdateResponse.js +60 -0
  58. package/lib/commonjs/fetchUpdateResponse.js.map +1 -0
  59. package/lib/commonjs/httpClient.js +63 -94
  60. package/lib/commonjs/httpClient.js.map +1 -1
  61. package/lib/commonjs/index.js +71 -18
  62. package/lib/commonjs/index.js.map +1 -1
  63. package/lib/commonjs/native.js +74 -60
  64. package/lib/commonjs/native.js.map +1 -1
  65. package/lib/commonjs/pluginHost.js +45 -0
  66. package/lib/commonjs/pluginHost.js.map +1 -0
  67. package/lib/commonjs/releaseCatalogCache.js +36 -87
  68. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  69. package/lib/commonjs/sdkVersion.js +1 -1
  70. package/lib/commonjs/sdkVersion.js.map +1 -1
  71. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  72. package/lib/commonjs/store.js +3 -10
  73. package/lib/commonjs/store.js.map +1 -1
  74. package/lib/commonjs/updateError.js +145 -0
  75. package/lib/commonjs/updateError.js.map +1 -0
  76. package/lib/commonjs/wrap.js +43 -26
  77. package/lib/commonjs/wrap.js.map +1 -1
  78. package/lib/module/appReady.js +95 -0
  79. package/lib/module/appReady.js.map +1 -0
  80. package/lib/module/checkForUpdate.js +113 -63
  81. package/lib/module/checkForUpdate.js.map +1 -1
  82. package/lib/module/clientPlugin.js +4 -0
  83. package/lib/module/clientPlugin.js.map +1 -0
  84. package/lib/module/error.js +4 -13
  85. package/lib/module/error.js.map +1 -1
  86. package/lib/module/fetchJSON.js +14 -26
  87. package/lib/module/fetchJSON.js.map +1 -1
  88. package/lib/module/fetchUpdateResponse.js +55 -0
  89. package/lib/module/fetchUpdateResponse.js.map +1 -0
  90. package/lib/module/httpClient.js +62 -93
  91. package/lib/module/httpClient.js.map +1 -1
  92. package/lib/module/index.js +58 -20
  93. package/lib/module/index.js.map +1 -1
  94. package/lib/module/native.js +67 -53
  95. package/lib/module/native.js.map +1 -1
  96. package/lib/module/pluginHost.js +36 -0
  97. package/lib/module/pluginHost.js.map +1 -0
  98. package/lib/module/releaseCatalogCache.js +32 -82
  99. package/lib/module/releaseCatalogCache.js.map +1 -1
  100. package/lib/module/sdkVersion.js +1 -1
  101. package/lib/module/sdkVersion.js.map +1 -1
  102. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  103. package/lib/module/store.js +3 -10
  104. package/lib/module/store.js.map +1 -1
  105. package/lib/module/updateError.js +139 -0
  106. package/lib/module/updateError.js.map +1 -0
  107. package/lib/module/wrap.js +43 -26
  108. package/lib/module/wrap.js.map +1 -1
  109. package/lib/typescript/commonjs/appReady.d.ts +21 -0
  110. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
  112. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
  114. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/error.d.ts +4 -13
  116. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/fetchJSON.d.ts +5 -5
  118. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +14 -0
  120. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/httpClient.d.ts +4 -33
  122. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/index.d.ts +4 -8
  124. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/native.d.ts +29 -22
  126. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
  128. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +8 -14
  130. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  132. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +35 -47
  134. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/store.d.ts +1 -3
  136. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/types.d.ts +1 -1
  138. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  140. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
  143. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  144. package/lib/typescript/module/appReady.d.ts +21 -0
  145. package/lib/typescript/module/appReady.d.ts.map +1 -0
  146. package/lib/typescript/module/checkForUpdate.d.ts +16 -4
  147. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  148. package/lib/typescript/module/clientPlugin.d.ts +2 -0
  149. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  150. package/lib/typescript/module/error.d.ts +4 -13
  151. package/lib/typescript/module/error.d.ts.map +1 -1
  152. package/lib/typescript/module/fetchJSON.d.ts +5 -5
  153. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  154. package/lib/typescript/module/fetchUpdateResponse.d.ts +14 -0
  155. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -0
  156. package/lib/typescript/module/httpClient.d.ts +4 -33
  157. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  158. package/lib/typescript/module/index.d.ts +4 -8
  159. package/lib/typescript/module/index.d.ts.map +1 -1
  160. package/lib/typescript/module/native.d.ts +29 -22
  161. package/lib/typescript/module/native.d.ts.map +1 -1
  162. package/lib/typescript/module/pluginHost.d.ts +4 -0
  163. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  164. package/lib/typescript/module/releaseCatalogCache.d.ts +8 -14
  165. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  166. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  167. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  168. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +35 -47
  169. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  170. package/lib/typescript/module/store.d.ts +1 -3
  171. package/lib/typescript/module/store.d.ts.map +1 -1
  172. package/lib/typescript/module/types.d.ts +1 -1
  173. package/lib/typescript/module/types.d.ts.map +1 -1
  174. package/lib/typescript/module/updateError.d.ts +26 -0
  175. package/lib/typescript/module/updateError.d.ts.map +1 -0
  176. package/lib/typescript/module/wrap.d.ts.map +1 -1
  177. package/lib/typescript/module/wrap.types.d.ts +6 -7
  178. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  179. package/package.json +7 -4
  180. package/src/appReady.ts +141 -0
  181. package/src/checkForUpdate.ts +145 -86
  182. package/src/clientPlugin.ts +18 -0
  183. package/src/error.ts +4 -14
  184. package/src/fetchJSON.ts +14 -30
  185. package/src/fetchUpdateResponse.ts +60 -0
  186. package/src/httpClient.ts +88 -159
  187. package/src/index.ts +90 -24
  188. package/src/native.ts +98 -102
  189. package/src/pluginHost.ts +47 -0
  190. package/src/releaseCatalogCache.ts +43 -172
  191. package/src/sdkVersion.ts +1 -1
  192. package/src/specs/NativeHotUpdater.ts +34 -48
  193. package/src/store.ts +2 -23
  194. package/src/types.ts +1 -1
  195. package/src/updateError.ts +167 -0
  196. package/src/wrap.tsx +57 -26
  197. package/src/wrap.types.ts +6 -7
  198. package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
  199. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
  200. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
  201. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
  202. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
  203. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  204. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
  205. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
  206. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  207. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
  208. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  209. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  210. package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
  211. package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
  212. package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
  213. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
  214. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
  215. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
  216. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
  217. package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
  218. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
  219. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  220. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  221. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -616
  222. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  223. package/lib/commonjs/httpClient.spec.js +0 -102
  224. package/lib/commonjs/httpClient.spec.js.map +0 -1
  225. package/lib/commonjs/index.spec.js +0 -302
  226. package/lib/commonjs/index.spec.js.map +0 -1
  227. package/lib/commonjs/iosInterop.spec.js +0 -18
  228. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  229. package/lib/commonjs/native.spec.js +0 -883
  230. package/lib/commonjs/native.spec.js.map +0 -1
  231. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
  232. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  233. package/lib/commonjs/notifyAppReadyInsights.js +0 -133
  234. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  235. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
  236. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  237. package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
  238. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  239. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
  240. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  241. package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
  242. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  243. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  244. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  245. package/lib/commonjs/store.notification.spec.js +0 -76
  246. package/lib/commonjs/store.notification.spec.js.map +0 -1
  247. package/lib/commonjs/store.spec.js +0 -316
  248. package/lib/commonjs/store.spec.js.map +0 -1
  249. package/lib/commonjs/wrap.spec.js +0 -329
  250. package/lib/commonjs/wrap.spec.js.map +0 -1
  251. package/lib/module/catalogCacheNative.spec.js +0 -31
  252. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  253. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -615
  254. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  255. package/lib/module/httpClient.spec.js +0 -102
  256. package/lib/module/httpClient.spec.js.map +0 -1
  257. package/lib/module/index.spec.js +0 -301
  258. package/lib/module/index.spec.js.map +0 -1
  259. package/lib/module/iosInterop.spec.js +0 -18
  260. package/lib/module/iosInterop.spec.js.map +0 -1
  261. package/lib/module/native.spec.js +0 -882
  262. package/lib/module/native.spec.js.map +0 -1
  263. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
  264. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  265. package/lib/module/notifyAppReadyInsights.js +0 -128
  266. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  267. package/lib/module/notifyAppReadyInsights.spec.js +0 -245
  268. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  269. package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
  270. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  271. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
  272. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  273. package/lib/module/releaseCatalogCache.spec.js +0 -195
  274. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  275. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  276. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  277. package/lib/module/store.notification.spec.js +0 -77
  278. package/lib/module/store.notification.spec.js.map +0 -1
  279. package/lib/module/store.spec.js +0 -315
  280. package/lib/module/store.spec.js.map +0 -1
  281. package/lib/module/wrap.spec.js +0 -330
  282. package/lib/module/wrap.spec.js.map +0 -1
  283. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
  284. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  285. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  286. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  287. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
  288. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  289. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  290. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  291. package/src/catalogCacheNative.spec.ts +0 -63
  292. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -612
  293. package/src/httpClient.spec.ts +0 -132
  294. package/src/index.spec.ts +0 -337
  295. package/src/iosInterop.spec.ts +0 -30
  296. package/src/native.spec.ts +0 -945
  297. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
  298. package/src/notifyAppReadyInsights.spec.ts +0 -243
  299. package/src/notifyAppReadyInsights.test-utils.ts +0 -31
  300. package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
  301. package/src/notifyAppReadyInsights.ts +0 -194
  302. package/src/releaseCatalogCache.spec.ts +0 -282
  303. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  304. package/src/store.notification.spec.ts +0 -92
  305. package/src/store.spec.ts +0 -372
  306. package/src/wrap.spec.ts +0 -406
@@ -1,945 +0,0 @@
1
- import { INVALID_COHORT_ERROR_MESSAGE } from "@hot-updater/core";
2
- import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
3
-
4
- const nativeModuleMock = vi.hoisted(() => {
5
- const getManifest = vi.fn<() => Record<string, unknown> | string>();
6
- const getCrashHistory = vi.fn<() => string[] | string>(() => []);
7
-
8
- return {
9
- getActiveUpdateState: vi.fn<() => unknown>(),
10
- clearCrashHistory: vi.fn(() => true),
11
- getBaseURL: vi.fn<() => string | null>(() => null),
12
- getBundleId: vi.fn<() => string | null>(() => "bundle-id"),
13
- getCohort: vi.fn<() => string>(() => "123"),
14
- getInstallId: vi.fn<() => string>(() => "install-id"),
15
- getUserId: vi.fn<() => string | null>(() => null),
16
- getUsername: vi.fn<() => string | null>(() => null),
17
- getManifest,
18
- getCrashHistory,
19
- getConstants: vi.fn(() => ({
20
- APP_VERSION: null,
21
- CHANNEL: "production",
22
- DEFAULT_CHANNEL: "production",
23
- FINGERPRINT_HASH: null,
24
- MIN_BUNDLE_ID: "min-bundle-id",
25
- })),
26
- notifyAppReady: vi.fn(),
27
- reload: vi.fn(),
28
- resetChannel: vi.fn(),
29
- setCohort: vi.fn(),
30
- setUser: vi.fn(),
31
- setBundleURL: vi.fn(),
32
- switchChannel: vi.fn(),
33
- updateBundle: vi.fn(),
34
- };
35
- });
36
-
37
- vi.mock("react-native", () => ({
38
- NativeEventEmitter: class {
39
- addListener() {
40
- return { remove: () => {} };
41
- }
42
- },
43
- Platform: {
44
- OS: "ios",
45
- },
46
- }));
47
-
48
- vi.mock("./specs/NativeHotUpdater", () => ({
49
- default: nativeModuleMock,
50
- }));
51
-
52
- describe("notifyAppReady", () => {
53
- beforeEach(() => {
54
- vi.resetModules();
55
- nativeModuleMock.getActiveUpdateState.mockReset();
56
- nativeModuleMock.getActiveUpdateState.mockReturnValue({
57
- activeSelection: null,
58
- stableSelection: null,
59
- verificationPending: false,
60
- highestSeenCatalogs: {},
61
- });
62
- nativeModuleMock.notifyAppReady.mockReset();
63
- nativeModuleMock.getBaseURL.mockReset();
64
- nativeModuleMock.getBundleId.mockReset();
65
- nativeModuleMock.getCrashHistory.mockReset();
66
- nativeModuleMock.getConstants.mockReturnValue({
67
- APP_VERSION: null,
68
- CHANNEL: "production",
69
- DEFAULT_CHANNEL: "production",
70
- FINGERPRINT_HASH: null,
71
- MIN_BUNDLE_ID: "min-bundle-id",
72
- });
73
- nativeModuleMock.getBundleId.mockReturnValue("bundle-id");
74
- nativeModuleMock.getBaseURL.mockReturnValue(null);
75
- nativeModuleMock.getCrashHistory.mockReturnValue([]);
76
- nativeModuleMock.getCohort.mockReset();
77
- nativeModuleMock.getCohort.mockReturnValue("123");
78
- nativeModuleMock.getInstallId.mockReset();
79
- nativeModuleMock.getInstallId.mockReturnValue("install-id");
80
- nativeModuleMock.getManifest.mockReset();
81
- nativeModuleMock.getManifest.mockReturnValue({
82
- assets: {
83
- "index.android.bundle": {
84
- fileHash: "hash-123",
85
- },
86
- },
87
- bundleId: "bundle-id",
88
- });
89
- nativeModuleMock.resetChannel.mockReset();
90
- nativeModuleMock.setCohort.mockReset();
91
- nativeModuleMock.setUser.mockReset();
92
- nativeModuleMock.updateBundle.mockReset();
93
- });
94
-
95
- it("exposes active Release state without internal Catalog identity or history", async () => {
96
- const receipt = {
97
- bundleId: "bundle-123",
98
- releaseId: "release-123",
99
- kind: "BUNDLE",
100
- channel: "production",
101
- catalogId: "private-catalog",
102
- scopeKey: "private-scope",
103
- generation: 10,
104
- catalogHash: "private-hash",
105
- selectionContextHash: "private-context",
106
- };
107
- nativeModuleMock.getActiveUpdateState.mockReturnValue({
108
- activeSelection: receipt,
109
- stableSelection: receipt,
110
- verificationPending: true,
111
- highestSeenCatalogs: {
112
- "private-catalog|private-scope": {
113
- generation: 10,
114
- catalogHash: "private-hash",
115
- },
116
- },
117
- });
118
- const { getPublicActiveUpdateState } = await import("./native");
119
- const selection = {
120
- bundleId: "bundle-123",
121
- releaseId: "release-123",
122
- kind: "BUNDLE",
123
- channel: "production",
124
- };
125
- expect(getPublicActiveUpdateState()).toEqual({
126
- activeSelection: selection,
127
- stableSelection: selection,
128
- verificationPending: true,
129
- });
130
- });
131
-
132
- it("tracks same-file promotions without changing artifact or crash identities", async () => {
133
- const state = {
134
- activeSelection: {
135
- kind: "BUNDLE",
136
- bundleId: "bundle-123",
137
- releaseId: "release-1",
138
- },
139
- };
140
- nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
141
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
142
- nativeModuleMock.getManifest.mockReturnValue({ assets: {} });
143
- nativeModuleMock.getCrashHistory.mockReturnValue(["bundle-failed"]);
144
- const { getUpdateId, getBundleId, getManifest, getCrashHistory } =
145
- await import("./native");
146
-
147
- expect(getUpdateId()).toBe("release-1");
148
- state.activeSelection.releaseId = "release-2";
149
- expect(getUpdateId()).toBe("release-2");
150
- expect(getBundleId()).toBe("bundle-123");
151
- expect(getManifest().bundleId).toBe("bundle-123");
152
- expect(getCrashHistory()).toEqual(["bundle-failed"]);
153
- });
154
-
155
- it.each([
156
- [null, "min-bundle-id"],
157
- ["legacy-bundle", "legacy-bundle"],
158
- ])(
159
- "preserves the fallback ID for native bundle %s",
160
- async (bundleId, expected) => {
161
- nativeModuleMock.getBundleId.mockReturnValue(bundleId);
162
- const { getUpdateId } = await import("./native");
163
-
164
- expect(getUpdateId()).toBe(expected);
165
- },
166
- );
167
-
168
- it("reports a staged update while the manifest still identifies the running file", async () => {
169
- const runningSelection = {
170
- kind: "BUNDLE",
171
- bundleId: "running-file",
172
- releaseId: "running-update",
173
- };
174
- const state = {
175
- activeSelection: runningSelection,
176
- stableSelection: runningSelection,
177
- };
178
- nativeModuleMock.getActiveUpdateState.mockImplementation(() => state);
179
- nativeModuleMock.getBundleId.mockReturnValue("running-file");
180
- nativeModuleMock.getManifest.mockReturnValue({
181
- assets: {},
182
- bundleId: "running-file",
183
- });
184
- const { getUpdateId, getBundleId, getManifest } = await import("./native");
185
-
186
- expect(getUpdateId()).toBe("running-update");
187
- state.activeSelection = {
188
- kind: "BUNDLE",
189
- bundleId: "staged-file",
190
- releaseId: "staged-update",
191
- };
192
-
193
- expect(getUpdateId()).toBe("staged-update");
194
- expect(getBundleId()).toBe("running-file");
195
- expect(getManifest().bundleId).toBe("running-file");
196
- });
197
-
198
- it("reports an explicit built-in rollback by its update ID", async () => {
199
- nativeModuleMock.getActiveUpdateState.mockReturnValue(
200
- JSON.stringify({
201
- activeSelection: {
202
- kind: "EMBEDDED",
203
- releaseId: "rollback-1",
204
- bundleId: "min-bundle-id",
205
- },
206
- }),
207
- );
208
- nativeModuleMock.getBundleId.mockReturnValue(null);
209
- const { getUpdateId, getBundleId } = await import("./native");
210
-
211
- expect(getUpdateId()).toBe("rollback-1");
212
- expect(getBundleId()).toBe("min-bundle-id");
213
- });
214
-
215
- it("normalizes legacy PROMOTED launch reports to UPDATE_APPLIED", async () => {
216
- nativeModuleMock.notifyAppReady.mockReturnValue(
217
- JSON.stringify({
218
- fromBundleId: "bundle-123",
219
- status: "PROMOTED",
220
- toBundleId: "bundle-456",
221
- }),
222
- );
223
-
224
- const { notifyAppReady } = await import("./native");
225
-
226
- expect(notifyAppReady()).toEqual({
227
- fromBundleId: "bundle-123",
228
- status: "UPDATE_APPLIED",
229
- toBundleId: "bundle-456",
230
- });
231
- expect(nativeModuleMock.notifyAppReady).toHaveBeenCalledWith();
232
- });
233
-
234
- it("returns RECOVERED launch reports with directional ids", async () => {
235
- nativeModuleMock.notifyAppReady.mockReturnValue({
236
- fromBundleId: "bundle-123",
237
- status: "RECOVERED",
238
- toBundleId: "bundle-122",
239
- updateStrategy: "appVersion",
240
- });
241
-
242
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
243
-
244
- expect(notifyAppReady()).toEqual({
245
- fromBundleId: "bundle-123",
246
- status: "RECOVERED",
247
- toBundleId: "bundle-122",
248
- });
249
- expect(readNotifyAppReady()).toEqual({
250
- insightsEvent: {
251
- fromBundleId: "bundle-123",
252
- toBundleId: "bundle-122",
253
- type: "RECOVERED",
254
- updateStrategy: "appVersion",
255
- },
256
- pending: false,
257
- result: {
258
- fromBundleId: "bundle-123",
259
- status: "RECOVERED",
260
- toBundleId: "bundle-122",
261
- },
262
- });
263
- });
264
-
265
- it("returns UNCHANGED for incomplete recovery payloads", async () => {
266
- nativeModuleMock.notifyAppReady.mockReturnValue({
267
- fromBundleId: "bundle-123",
268
- status: "RECOVERED",
269
- toBundleId: null,
270
- updateStrategy: "appVersion",
271
- });
272
-
273
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
274
-
275
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
276
- expect(readNotifyAppReady()).toEqual({
277
- insightsEvent: null,
278
- pending: false,
279
- result: { status: "UNCHANGED" },
280
- });
281
- });
282
-
283
- it("returns UNCHANGED when automatic insights metadata is incomplete", async () => {
284
- nativeModuleMock.notifyAppReady.mockReturnValue({
285
- status: "UPDATE_APPLIED",
286
- });
287
-
288
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
289
-
290
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
291
- expect(readNotifyAppReady()).toEqual({
292
- insightsEvent: null,
293
- pending: false,
294
- result: { status: "UNCHANGED" },
295
- });
296
- });
297
-
298
- it("normalizes malformed old-arch notifyAppReady payloads to UNCHANGED", async () => {
299
- nativeModuleMock.notifyAppReady.mockReturnValue("{");
300
-
301
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
302
-
303
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
304
- expect(readNotifyAppReady()).toEqual({
305
- insightsEvent: null,
306
- pending: false,
307
- result: { status: "UNCHANGED" },
308
- });
309
- });
310
-
311
- it("keeps the internal pending state out of the public result", async () => {
312
- nativeModuleMock.notifyAppReady.mockReturnValue({ status: "PENDING" });
313
-
314
- const { notifyAppReady, readNotifyAppReady } = await import("./native");
315
-
316
- expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
317
- expect(readNotifyAppReady()).toEqual({
318
- insightsEvent: null,
319
- pending: true,
320
- result: { status: "UNCHANGED" },
321
- });
322
- });
323
-
324
- it("returns the native bundle id when available", async () => {
325
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
326
-
327
- const { getBundleId } = await import("./native");
328
-
329
- expect(getBundleId()).toBe("bundle-123");
330
- });
331
-
332
- it("throws when native SDK does not expose getBundleId", async () => {
333
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
334
- getBundleId?: typeof nativeModuleMock.getBundleId;
335
- };
336
- const originalGetBundleId = nativeModule.getBundleId;
337
- nativeModule.getBundleId = null as unknown as Mock<() => string | null>;
338
-
339
- try {
340
- const { getBundleId } = await import("./native");
341
-
342
- expect(() => getBundleId()).toThrow(
343
- "Native module is missing 'getBundleId()'",
344
- );
345
- } finally {
346
- nativeModule.getBundleId = originalGetBundleId;
347
- }
348
- });
349
-
350
- it("falls back to MIN_BUNDLE_ID when native reports an empty bundle id", async () => {
351
- nativeModuleMock.getBundleId.mockReturnValue("");
352
-
353
- const { getBundleId } = await import("./native");
354
-
355
- expect(getBundleId()).toBe("min-bundle-id");
356
- });
357
-
358
- it("falls back to MIN_BUNDLE_ID when native bundle id is null", async () => {
359
- nativeModuleMock.getBundleId.mockReturnValue(null);
360
-
361
- const { getBundleId } = await import("./native");
362
-
363
- expect(getBundleId()).toBe("min-bundle-id");
364
- });
365
-
366
- it("falls back to MIN_BUNDLE_ID for legacy NIL_UUID bundle ids", async () => {
367
- nativeModuleMock.getBundleId.mockReturnValue(
368
- "00000000-0000-0000-0000-000000000000",
369
- );
370
-
371
- const { getBundleId } = await import("./native");
372
-
373
- expect(getBundleId()).toBe("min-bundle-id");
374
- });
375
-
376
- it("returns manifest from native objects", async () => {
377
- nativeModuleMock.getManifest.mockReturnValue({
378
- assets: {
379
- "assets/logo.png": {
380
- fileHash: "hash-logo",
381
- signature: "sig-logo",
382
- },
383
- "index.android.bundle": {
384
- fileHash: "hash-bundle",
385
- },
386
- },
387
- bundleId: "bundle-123",
388
- });
389
-
390
- const { getManifest } = await import("./native");
391
-
392
- expect(getManifest()).toEqual({
393
- assets: {
394
- "assets/logo.png": {
395
- fileHash: "hash-logo",
396
- signature: "sig-logo",
397
- },
398
- "index.android.bundle": {
399
- fileHash: "hash-bundle",
400
- },
401
- },
402
- bundleId: "bundle-123",
403
- });
404
- });
405
-
406
- it("normalizes legacy manifest asset entries from native objects", async () => {
407
- nativeModuleMock.getManifest.mockReturnValue({
408
- assets: {
409
- "assets/logo.png": "hash-logo",
410
- },
411
- bundleId: "bundle-123",
412
- });
413
-
414
- const { getManifest } = await import("./native");
415
-
416
- expect(getManifest()).toEqual({
417
- assets: {
418
- "assets/logo.png": {
419
- fileHash: "hash-logo",
420
- },
421
- },
422
- bundleId: "bundle-123",
423
- });
424
- });
425
-
426
- it("parses manifest from old-arch JSON payloads", async () => {
427
- nativeModuleMock.getManifest.mockReturnValue(
428
- JSON.stringify({
429
- assets: {
430
- "assets/logo.png": {
431
- fileHash: "hash-logo",
432
- },
433
- },
434
- bundleId: "bundle-123",
435
- }),
436
- );
437
-
438
- const { getManifest } = await import("./native");
439
-
440
- expect(getManifest()).toEqual({
441
- assets: {
442
- "assets/logo.png": {
443
- fileHash: "hash-logo",
444
- },
445
- },
446
- bundleId: "bundle-123",
447
- });
448
- });
449
-
450
- it("normalizes legacy manifest asset entries from old-arch JSON payloads", async () => {
451
- nativeModuleMock.getManifest.mockReturnValue(
452
- JSON.stringify({
453
- assets: {
454
- "assets/logo.png": "hash-logo",
455
- },
456
- bundleId: "bundle-123",
457
- }),
458
- );
459
-
460
- const { getManifest } = await import("./native");
461
-
462
- expect(getManifest()).toEqual({
463
- assets: {
464
- "assets/logo.png": {
465
- fileHash: "hash-logo",
466
- },
467
- },
468
- bundleId: "bundle-123",
469
- });
470
- });
471
-
472
- it("returns an empty-assets manifest for malformed payloads", async () => {
473
- nativeModuleMock.getManifest.mockReturnValue("{");
474
-
475
- const { getManifest } = await import("./native");
476
-
477
- expect(getManifest()).toEqual({
478
- assets: {},
479
- bundleId: "bundle-id",
480
- });
481
- });
482
-
483
- it("caches active bundle getters within a JS runtime", async () => {
484
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
485
- nativeModuleMock.getManifest.mockReturnValue({
486
- assets: {
487
- "assets/logo.png": {
488
- fileHash: "hash-logo",
489
- },
490
- },
491
- bundleId: "bundle-123",
492
- });
493
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
494
-
495
- const { getBaseURL, getBundleId, getManifest } = await import("./native");
496
-
497
- expect(getBundleId()).toBe("bundle-123");
498
- expect(getBundleId()).toBe("bundle-123");
499
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(1);
500
-
501
- const firstManifest = getManifest();
502
- firstManifest.assets["assets/logo.png"] = {
503
- fileHash: "mutated-hash",
504
- };
505
-
506
- expect(getManifest()).toEqual({
507
- assets: {
508
- "assets/logo.png": {
509
- fileHash: "hash-logo",
510
- },
511
- },
512
- bundleId: "bundle-123",
513
- });
514
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(1);
515
-
516
- expect(getBaseURL()).toBe("file:///bundle-123");
517
- expect(getBaseURL()).toBe("file:///bundle-123");
518
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(1);
519
- });
520
-
521
- it("uses the launched bundle reported by native after updateBundle succeeds", async () => {
522
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
523
- nativeModuleMock.getManifest.mockReturnValue({
524
- assets: {},
525
- bundleId: "bundle-123",
526
- });
527
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
528
- nativeModuleMock.updateBundle.mockResolvedValue(true);
529
-
530
- const { getBaseURL, getBundleId, getManifest, updateBundle } =
531
- await import("./native");
532
-
533
- expect(getBundleId()).toBe("bundle-123");
534
- expect(getManifest()).toEqual({
535
- assets: {},
536
- bundleId: "bundle-123",
537
- });
538
- expect(getBaseURL()).toBe("file:///bundle-123");
539
-
540
- await updateBundle({
541
- bundleId: "bundle-456",
542
- fileHash: null,
543
- fileUrl: "https://example.com/bundle.zip",
544
- status: "UPDATE",
545
- });
546
-
547
- expect(getBundleId()).toBe("bundle-123");
548
- expect(getManifest()).toEqual({
549
- assets: {},
550
- bundleId: "bundle-123",
551
- });
552
- expect(getBaseURL()).toBe("file:///bundle-123");
553
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(3);
554
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
555
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
556
- });
557
-
558
- it("reinstalls a session-installed bundle after native resets to built-in", async () => {
559
- nativeModuleMock.getConstants.mockReturnValue({
560
- APP_VERSION: null,
561
- CHANNEL: "production",
562
- DEFAULT_CHANNEL: "production",
563
- FINGERPRINT_HASH: null,
564
- MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
565
- });
566
- nativeModuleMock.getBundleId.mockReturnValue(
567
- "00000000-0000-0000-0000-000000000002",
568
- );
569
- nativeModuleMock.updateBundle.mockResolvedValue(true);
570
-
571
- const { getBundleId, updateBundle } = await import("./native");
572
-
573
- await updateBundle({
574
- bundleId: "00000000-0000-0000-0000-000000000003",
575
- fileHash: null,
576
- fileUrl: "https://example.com/bundle.zip",
577
- status: "UPDATE",
578
- });
579
-
580
- nativeModuleMock.getBundleId.mockReturnValue(
581
- "00000000-0000-0000-0000-000000000003",
582
- );
583
- expect(getBundleId()).toBe("00000000-0000-0000-0000-000000000003");
584
-
585
- nativeModuleMock.getBundleId.mockReturnValue(null);
586
-
587
- await expect(
588
- updateBundle({
589
- bundleId: "00000000-0000-0000-0000-000000000003",
590
- fileHash: null,
591
- fileUrl: "https://example.com/bundle.zip",
592
- status: "UPDATE",
593
- }),
594
- ).resolves.toBe(true);
595
-
596
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
597
- });
598
-
599
- it("skips a session-installed bundle when native still reports it active", async () => {
600
- nativeModuleMock.getConstants.mockReturnValue({
601
- APP_VERSION: null,
602
- CHANNEL: "production",
603
- DEFAULT_CHANNEL: "production",
604
- FINGERPRINT_HASH: null,
605
- MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
606
- });
607
- nativeModuleMock.getBundleId.mockReturnValue(
608
- "00000000-0000-0000-0000-000000000002",
609
- );
610
- nativeModuleMock.updateBundle.mockResolvedValue(true);
611
-
612
- const { updateBundle } = await import("./native");
613
-
614
- await updateBundle({
615
- bundleId: "00000000-0000-0000-0000-000000000003",
616
- fileHash: null,
617
- fileUrl: "https://example.com/bundle.zip",
618
- status: "UPDATE",
619
- });
620
-
621
- nativeModuleMock.getBundleId.mockReturnValue(
622
- "00000000-0000-0000-0000-000000000003",
623
- );
624
-
625
- await expect(
626
- updateBundle({
627
- bundleId: "00000000-0000-0000-0000-000000000003",
628
- fileHash: null,
629
- fileUrl: "https://example.com/bundle.zip",
630
- status: "UPDATE",
631
- }),
632
- ).resolves.toBe(true);
633
-
634
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(1);
635
- });
636
-
637
- it("forwards manifest artifact parameters to native updateBundle", async () => {
638
- nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
639
- nativeModuleMock.updateBundle.mockResolvedValue(true);
640
-
641
- const { updateBundle } = await import("./native");
642
-
643
- await updateBundle({
644
- bundleId: "bundle-789",
645
- changedAssets: {
646
- "index.ios.bundle": {
647
- file: {
648
- compression: "br",
649
- url: "https://example.com/files/index.ios.bundle.br",
650
- },
651
- fileHash: "hash-next",
652
- patch: {
653
- algorithm: "bsdiff",
654
- baseBundleId: "bundle-123",
655
- baseFileHash: "hash-prev",
656
- patchFileHash: "hash-patch",
657
- patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
658
- },
659
- },
660
- },
661
- fileHash: "sig:archive",
662
- fileUrl: "https://example.com/bundle.zip",
663
- manifestFileHash: "sig:manifest",
664
- manifestUrl: "https://example.com/manifest.json",
665
- status: "UPDATE",
666
- });
667
-
668
- expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
669
- bundleId: "bundle-789",
670
- changedAssets: {
671
- "index.ios.bundle": {
672
- file: {
673
- compression: "br",
674
- url: "https://example.com/files/index.ios.bundle.br",
675
- },
676
- fileHash: "hash-next",
677
- patch: {
678
- algorithm: "bsdiff",
679
- baseBundleId: "bundle-123",
680
- baseFileHash: "hash-prev",
681
- patchFileHash: "hash-patch",
682
- patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
683
- },
684
- },
685
- },
686
- channel: undefined,
687
- fileHash: "sig:archive",
688
- fileUrl: "https://example.com/bundle.zip",
689
- manifestFileHash: "sig:manifest",
690
- manifestUrl: "https://example.com/manifest.json",
691
- });
692
- });
693
-
694
- it("invalidates cached bundle getters after resetChannel succeeds", async () => {
695
- nativeModuleMock.getConstants.mockReturnValue({
696
- APP_VERSION: null,
697
- CHANNEL: "beta",
698
- DEFAULT_CHANNEL: "production",
699
- FINGERPRINT_HASH: null,
700
- MIN_BUNDLE_ID: "min-bundle-id",
701
- });
702
- nativeModuleMock.getBundleId.mockReturnValue("bundle-beta");
703
- nativeModuleMock.getManifest.mockReturnValue({
704
- assets: {},
705
- bundleId: "bundle-beta",
706
- });
707
- nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-beta");
708
- nativeModuleMock.resetChannel.mockResolvedValue(true);
709
-
710
- const { getBaseURL, getBundleId, getManifest, resetChannel } =
711
- await import("./native");
712
-
713
- expect(getBundleId()).toBe("bundle-beta");
714
- expect(getManifest()).toEqual({
715
- assets: {},
716
- bundleId: "bundle-beta",
717
- });
718
- expect(getBaseURL()).toBe("file:///bundle-beta");
719
-
720
- nativeModuleMock.getBundleId.mockReturnValue(null);
721
- nativeModuleMock.getManifest.mockReturnValue({});
722
- nativeModuleMock.getBaseURL.mockReturnValue("");
723
-
724
- await expect(resetChannel()).resolves.toBe(true);
725
-
726
- expect(getBundleId()).toBe("min-bundle-id");
727
- expect(getManifest()).toEqual({
728
- assets: {},
729
- bundleId: "min-bundle-id",
730
- });
731
- expect(getBaseURL()).toBeNull();
732
- expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(2);
733
- expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
734
- expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
735
- });
736
-
737
- it("delegates resetChannel to native even when exported channel constants already match", async () => {
738
- nativeModuleMock.getConstants.mockReturnValue({
739
- APP_VERSION: null,
740
- CHANNEL: "beta",
741
- DEFAULT_CHANNEL: "beta",
742
- FINGERPRINT_HASH: null,
743
- MIN_BUNDLE_ID: "min-bundle-id",
744
- });
745
- nativeModuleMock.resetChannel.mockResolvedValue(true);
746
-
747
- const { resetChannel } = await import("./native");
748
-
749
- await expect(resetChannel()).resolves.toBe(true);
750
- expect(nativeModuleMock.resetChannel).toHaveBeenCalledTimes(1);
751
- });
752
-
753
- it("parses crash history from legacy JSON payloads", async () => {
754
- nativeModuleMock.getCrashHistory.mockReturnValue(
755
- JSON.stringify(["bundle-1", "bundle-2"]),
756
- );
757
-
758
- const { getCrashHistory } = await import("./native");
759
-
760
- expect(getCrashHistory()).toEqual(["bundle-1", "bundle-2"]);
761
- });
762
-
763
- it("falls back to an empty crash history for malformed payloads", async () => {
764
- nativeModuleMock.getCrashHistory.mockReturnValue("{");
765
-
766
- const { getCrashHistory } = await import("./native");
767
-
768
- expect(getCrashHistory()).toEqual([]);
769
- });
770
-
771
- it("passes normalized cohort overrides to native", async () => {
772
- const { setCohort } = await import("./native");
773
-
774
- setCohort(" QA-Group ");
775
-
776
- expect(nativeModuleMock.setCohort).toHaveBeenCalledWith("qa-group");
777
- });
778
-
779
- it("returns the most recently set cohort before native reads catch up", async () => {
780
- nativeModuleMock.getCohort.mockReturnValue("123");
781
-
782
- const { getCohort, setCohort } = await import("./native");
783
-
784
- setCohort(" QA-Group ");
785
-
786
- expect(getCohort()).toBe("qa-group");
787
- expect(nativeModuleMock.getCohort).not.toHaveBeenCalled();
788
- });
789
-
790
- it("throws when attempting to clear the cohort with an empty value", async () => {
791
- const { setCohort } = await import("./native");
792
-
793
- expect(() => setCohort("")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
794
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
795
- });
796
-
797
- it("throws for invalid cohort overrides", async () => {
798
- const { setCohort } = await import("./native");
799
-
800
- expect(() => setCohort("Bad Cohort")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
801
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
802
- });
803
-
804
- it("throws for cohort overrides longer than the limit", async () => {
805
- const { setCohort } = await import("./native");
806
-
807
- expect(() => setCohort("a".repeat(65))).toThrow(
808
- INVALID_COHORT_ERROR_MESSAGE,
809
- );
810
- expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
811
- });
812
-
813
- it("returns the install id reported by native", async () => {
814
- nativeModuleMock.getInstallId.mockReturnValue("install-123");
815
-
816
- const { getInstallId } = await import("./native");
817
-
818
- expect(getInstallId()).toBe("install-123");
819
- });
820
-
821
- it("normalizes an omitted native fingerprint constant to null", async () => {
822
- nativeModuleMock.getConstants.mockReturnValue({
823
- APP_VERSION: null,
824
- CHANNEL: "production",
825
- DEFAULT_CHANNEL: "production",
826
- FINGERPRINT_HASH: undefined as unknown as null,
827
- MIN_BUNDLE_ID: "min-bundle-id",
828
- });
829
-
830
- const { getFingerprintHash } = await import("./native");
831
-
832
- expect(getFingerprintHash()).toBeNull();
833
- });
834
-
835
- it("throws when native SDK does not expose getInstallId", async () => {
836
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
837
- getInstallId?: typeof nativeModuleMock.getInstallId;
838
- };
839
- const originalGetInstallId = nativeModule.getInstallId;
840
- nativeModule.getInstallId = null as unknown as Mock<() => string>;
841
-
842
- try {
843
- const { getInstallId } = await import("./native");
844
-
845
- expect(() => getInstallId()).toThrow(
846
- "Native module is missing 'getInstallId()'",
847
- );
848
- } finally {
849
- nativeModule.getInstallId = originalGetInstallId;
850
- }
851
- });
852
-
853
- it("passes nullable user identity through to native", async () => {
854
- const { setUser } = await import("./native");
855
-
856
- setUser({ userId: "user-123", username: "alice" });
857
- setUser({ userId: 42, username: "bob" });
858
- setUser({});
859
- setUser(null);
860
-
861
- expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(
862
- 1,
863
- "user-123",
864
- "alice",
865
- );
866
- expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(2, "42", "bob");
867
- expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(3, null, null);
868
- expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(4, null, null);
869
- });
870
-
871
- it("reads persisted user identity from native when available", async () => {
872
- nativeModuleMock.getUserId.mockReturnValue("user-123");
873
- nativeModuleMock.getUsername.mockReturnValue("alice");
874
-
875
- const { getPersistedUserIdentity } = await import("./native");
876
-
877
- expect(getPersistedUserIdentity()).toEqual({
878
- userId: "user-123",
879
- username: "alice",
880
- });
881
- });
882
-
883
- it("throws when native SDK does not expose persisted user identity getters", async () => {
884
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
885
- getUserId?: typeof nativeModuleMock.getUserId;
886
- getUsername?: typeof nativeModuleMock.getUsername;
887
- };
888
- const originalGetUserId = nativeModule.getUserId;
889
- nativeModule.getUserId = null as unknown as Mock<() => string | null>;
890
-
891
- try {
892
- const { getPersistedUserIdentity } = await import("./native");
893
-
894
- expect(() => getPersistedUserIdentity()).toThrow(
895
- "Native module is missing 'getUserId()' or 'getUsername()'",
896
- );
897
- } finally {
898
- nativeModule.getUserId = originalGetUserId;
899
- }
900
- });
901
-
902
- it("throws when native SDK does not expose setUser", async () => {
903
- const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
904
- setUser?: typeof nativeModuleMock.setUser;
905
- };
906
- const originalSetUser = nativeModule.setUser;
907
- nativeModule.setUser = null as unknown as Mock<
908
- (userId: string | null, username: string | null) => void
909
- >;
910
-
911
- try {
912
- const { setUser } = await import("./native");
913
-
914
- expect(() => setUser({ userId: "user-123" })).toThrow(
915
- "Native module is missing 'setUser()'",
916
- );
917
- } finally {
918
- nativeModule.setUser = originalSetUser;
919
- }
920
- });
921
-
922
- it("returns the cohort reported by native", async () => {
923
- nativeModuleMock.getCohort.mockReturnValue("qa-group");
924
-
925
- const { getCohort } = await import("./native");
926
-
927
- expect(getCohort()).toBe("qa-group");
928
- });
929
-
930
- it("normalizes the cohort reported by native", async () => {
931
- nativeModuleMock.getCohort.mockReturnValue(" QA-GROUP ");
932
-
933
- const { getCohort } = await import("./native");
934
-
935
- expect(getCohort()).toBe("qa-group");
936
- });
937
-
938
- it("throws when native reports an invalid cohort", async () => {
939
- nativeModuleMock.getCohort.mockReturnValue("1001");
940
-
941
- const { getCohort } = await import("./native");
942
-
943
- expect(() => getCohort()).toThrow(INVALID_COHORT_ERROR_MESSAGE);
944
- });
945
- });