@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
@@ -6,10 +6,14 @@ const nativeModuleMock = vi.hoisted(() => {
6
6
  const getCrashHistory = vi.fn<() => string[] | string>(() => []);
7
7
 
8
8
  return {
9
+ getActiveUpdateState: vi.fn<() => unknown>(),
9
10
  clearCrashHistory: vi.fn(() => true),
10
11
  getBaseURL: vi.fn<() => string | null>(() => null),
11
12
  getBundleId: vi.fn<() => string | null>(() => "bundle-id"),
12
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),
13
17
  getManifest,
14
18
  getCrashHistory,
15
19
  getConstants: vi.fn(() => ({
@@ -23,6 +27,7 @@ const nativeModuleMock = vi.hoisted(() => {
23
27
  reload: vi.fn(),
24
28
  resetChannel: vi.fn(),
25
29
  setCohort: vi.fn(),
30
+ setUser: vi.fn(),
26
31
  setBundleURL: vi.fn(),
27
32
  switchChannel: vi.fn(),
28
33
  updateBundle: vi.fn(),
@@ -63,6 +68,8 @@ describe("notifyAppReady", () => {
63
68
  nativeModuleMock.getCrashHistory.mockReturnValue([]);
64
69
  nativeModuleMock.getCohort.mockReset();
65
70
  nativeModuleMock.getCohort.mockReturnValue("123");
71
+ nativeModuleMock.getInstallId.mockReset();
72
+ nativeModuleMock.getInstallId.mockReturnValue("install-id");
66
73
  nativeModuleMock.getManifest.mockReset();
67
74
  nativeModuleMock.getManifest.mockReturnValue({
68
75
  assets: {
@@ -74,40 +81,154 @@ describe("notifyAppReady", () => {
74
81
  });
75
82
  nativeModuleMock.resetChannel.mockReset();
76
83
  nativeModuleMock.setCohort.mockReset();
84
+ nativeModuleMock.setUser.mockReset();
77
85
  nativeModuleMock.updateBundle.mockReset();
78
86
  });
79
87
 
80
- it("normalizes legacy PROMOTED launch reports to STABLE", async () => {
88
+ it("exposes active Release state without internal Catalog identity or history", async () => {
89
+ const receipt = {
90
+ bundleId: "bundle-123",
91
+ releaseId: "release-123",
92
+ kind: "BUNDLE",
93
+ channel: "production",
94
+ catalogId: "private-catalog",
95
+ scopeKey: "private-scope",
96
+ generation: 10,
97
+ catalogHash: "private-hash",
98
+ selectionContextHash: "private-context",
99
+ };
100
+ nativeModuleMock.getActiveUpdateState.mockReturnValue({
101
+ activeSelection: receipt,
102
+ stableSelection: receipt,
103
+ verificationPending: true,
104
+ highestSeenCatalogs: {
105
+ "private-catalog|private-scope": {
106
+ generation: 10,
107
+ catalogHash: "private-hash",
108
+ },
109
+ },
110
+ });
111
+ const { getPublicActiveUpdateState } = await import("./native");
112
+ const selection = {
113
+ bundleId: "bundle-123",
114
+ releaseId: "release-123",
115
+ kind: "BUNDLE",
116
+ channel: "production",
117
+ };
118
+ expect(getPublicActiveUpdateState()).toEqual({
119
+ activeSelection: selection,
120
+ stableSelection: selection,
121
+ verificationPending: true,
122
+ });
123
+ });
124
+
125
+ it("normalizes legacy PROMOTED launch reports to UPDATE_APPLIED", async () => {
81
126
  nativeModuleMock.notifyAppReady.mockReturnValue(
82
- JSON.stringify({ status: "PROMOTED" }),
127
+ JSON.stringify({
128
+ fromBundleId: "bundle-123",
129
+ status: "PROMOTED",
130
+ toBundleId: "bundle-456",
131
+ }),
83
132
  );
84
133
 
85
134
  const { notifyAppReady } = await import("./native");
86
135
 
87
- expect(notifyAppReady()).toEqual({ status: "STABLE" });
136
+ expect(notifyAppReady()).toEqual({
137
+ fromBundleId: "bundle-123",
138
+ status: "UPDATE_APPLIED",
139
+ toBundleId: "bundle-456",
140
+ });
88
141
  expect(nativeModuleMock.notifyAppReady).toHaveBeenCalledWith();
89
142
  });
90
143
 
91
- it("returns RECOVERED launch reports unchanged", async () => {
144
+ it("returns RECOVERED launch reports with directional ids", async () => {
92
145
  nativeModuleMock.notifyAppReady.mockReturnValue({
93
- crashedBundleId: "bundle-123",
146
+ fromBundleId: "bundle-123",
94
147
  status: "RECOVERED",
148
+ toBundleId: "bundle-122",
149
+ updateStrategy: "appVersion",
95
150
  });
96
151
 
97
- const { notifyAppReady } = await import("./native");
152
+ const { notifyAppReady, readNotifyAppReady } = await import("./native");
98
153
 
99
154
  expect(notifyAppReady()).toEqual({
100
- crashedBundleId: "bundle-123",
155
+ fromBundleId: "bundle-123",
156
+ status: "RECOVERED",
157
+ toBundleId: "bundle-122",
158
+ });
159
+ expect(readNotifyAppReady()).toEqual({
160
+ analyticsEvent: {
161
+ fromBundleId: "bundle-123",
162
+ toBundleId: "bundle-122",
163
+ type: "RECOVERED",
164
+ updateStrategy: "appVersion",
165
+ },
166
+ pending: false,
167
+ result: {
168
+ fromBundleId: "bundle-123",
169
+ status: "RECOVERED",
170
+ toBundleId: "bundle-122",
171
+ },
172
+ });
173
+ });
174
+
175
+ it("returns UNCHANGED for incomplete recovery payloads", async () => {
176
+ nativeModuleMock.notifyAppReady.mockReturnValue({
177
+ fromBundleId: "bundle-123",
101
178
  status: "RECOVERED",
179
+ toBundleId: null,
180
+ updateStrategy: "appVersion",
181
+ });
182
+
183
+ const { notifyAppReady, readNotifyAppReady } = await import("./native");
184
+
185
+ expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
186
+ expect(readNotifyAppReady()).toEqual({
187
+ analyticsEvent: null,
188
+ pending: false,
189
+ result: { status: "UNCHANGED" },
102
190
  });
103
191
  });
104
192
 
105
- it("falls back to STABLE for malformed old-arch payloads", async () => {
193
+ it("returns UNCHANGED when automatic analytics metadata is incomplete", async () => {
194
+ nativeModuleMock.notifyAppReady.mockReturnValue({
195
+ status: "UPDATE_APPLIED",
196
+ });
197
+
198
+ const { notifyAppReady, readNotifyAppReady } = await import("./native");
199
+
200
+ expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
201
+ expect(readNotifyAppReady()).toEqual({
202
+ analyticsEvent: null,
203
+ pending: false,
204
+ result: { status: "UNCHANGED" },
205
+ });
206
+ });
207
+
208
+ it("normalizes malformed old-arch notifyAppReady payloads to UNCHANGED", async () => {
106
209
  nativeModuleMock.notifyAppReady.mockReturnValue("{");
107
210
 
108
- const { notifyAppReady } = await import("./native");
211
+ const { notifyAppReady, readNotifyAppReady } = await import("./native");
212
+
213
+ expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
214
+ expect(readNotifyAppReady()).toEqual({
215
+ analyticsEvent: null,
216
+ pending: false,
217
+ result: { status: "UNCHANGED" },
218
+ });
219
+ });
109
220
 
110
- expect(notifyAppReady()).toEqual({ status: "STABLE" });
221
+ it("keeps the internal pending state out of the public result", async () => {
222
+ nativeModuleMock.notifyAppReady.mockReturnValue({ status: "PENDING" });
223
+
224
+ const { notifyAppReady, readNotifyAppReady } = await import("./native");
225
+
226
+ expect(notifyAppReady()).toEqual({ status: "UNCHANGED" });
227
+ expect(readNotifyAppReady()).toEqual({
228
+ analyticsEvent: null,
229
+ pending: true,
230
+ result: { status: "UNCHANGED" },
231
+ });
111
232
  });
112
233
 
113
234
  it("returns the native bundle id when available", async () => {
@@ -599,6 +720,101 @@ describe("notifyAppReady", () => {
599
720
  expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
600
721
  });
601
722
 
723
+ it("returns the install id reported by native", async () => {
724
+ nativeModuleMock.getInstallId.mockReturnValue("install-123");
725
+
726
+ const { getInstallId } = await import("./native");
727
+
728
+ expect(getInstallId()).toBe("install-123");
729
+ });
730
+
731
+ it("throws when native SDK does not expose getInstallId", async () => {
732
+ const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
733
+ getInstallId?: typeof nativeModuleMock.getInstallId;
734
+ };
735
+ const originalGetInstallId = nativeModule.getInstallId;
736
+ nativeModule.getInstallId = null as unknown as Mock<() => string>;
737
+
738
+ try {
739
+ const { getInstallId } = await import("./native");
740
+
741
+ expect(() => getInstallId()).toThrow(
742
+ "Native module is missing 'getInstallId()'",
743
+ );
744
+ } finally {
745
+ nativeModule.getInstallId = originalGetInstallId;
746
+ }
747
+ });
748
+
749
+ it("passes nullable user identity through to native", async () => {
750
+ const { setUser } = await import("./native");
751
+
752
+ setUser({ userId: "user-123", username: "alice" });
753
+ setUser({ userId: 42, username: "bob" });
754
+ setUser({});
755
+ setUser(null);
756
+
757
+ expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(
758
+ 1,
759
+ "user-123",
760
+ "alice",
761
+ );
762
+ expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(2, "42", "bob");
763
+ expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(3, null, null);
764
+ expect(nativeModuleMock.setUser).toHaveBeenNthCalledWith(4, null, null);
765
+ });
766
+
767
+ it("reads persisted user identity from native when available", async () => {
768
+ nativeModuleMock.getUserId.mockReturnValue("user-123");
769
+ nativeModuleMock.getUsername.mockReturnValue("alice");
770
+
771
+ const { getPersistedUserIdentity } = await import("./native");
772
+
773
+ expect(getPersistedUserIdentity()).toEqual({
774
+ userId: "user-123",
775
+ username: "alice",
776
+ });
777
+ });
778
+
779
+ it("throws when native SDK does not expose persisted user identity getters", async () => {
780
+ const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
781
+ getUserId?: typeof nativeModuleMock.getUserId;
782
+ getUsername?: typeof nativeModuleMock.getUsername;
783
+ };
784
+ const originalGetUserId = nativeModule.getUserId;
785
+ nativeModule.getUserId = null as unknown as Mock<() => string | null>;
786
+
787
+ try {
788
+ const { getPersistedUserIdentity } = await import("./native");
789
+
790
+ expect(() => getPersistedUserIdentity()).toThrow(
791
+ "Native module is missing 'getUserId()' or 'getUsername()'",
792
+ );
793
+ } finally {
794
+ nativeModule.getUserId = originalGetUserId;
795
+ }
796
+ });
797
+
798
+ it("throws when native SDK does not expose setUser", async () => {
799
+ const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
800
+ setUser?: typeof nativeModuleMock.setUser;
801
+ };
802
+ const originalSetUser = nativeModule.setUser;
803
+ nativeModule.setUser = null as unknown as Mock<
804
+ (userId: string | null, username: string | null) => void
805
+ >;
806
+
807
+ try {
808
+ const { setUser } = await import("./native");
809
+
810
+ expect(() => setUser({ userId: "user-123" })).toThrow(
811
+ "Native module is missing 'setUser()'",
812
+ );
813
+ } finally {
814
+ nativeModule.setUser = originalSetUser;
815
+ }
816
+ });
817
+
602
818
  it("returns the cohort reported by native", async () => {
603
819
  nativeModuleMock.getCohort.mockReturnValue("qa-group");
604
820