@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/wrap.spec.ts CHANGED
@@ -1,13 +1,49 @@
1
1
  import type React from "react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
 
4
+ import type {
5
+ NotifyAppReadyAnalyticsEvent,
6
+ NotifyAppReadyResult,
7
+ } from "./native";
4
8
  import type { HotUpdaterOptions } from "./wrap";
5
9
 
10
+ vi.mock("react-native", () => ({
11
+ Platform: {
12
+ OS: "ios",
13
+ },
14
+ }));
15
+
16
+ const createNotifyReadResult = (
17
+ result: NotifyAppReadyResult = { status: "UNCHANGED" },
18
+ analyticsEvent: NotifyAppReadyAnalyticsEvent | null = null,
19
+ pending = false,
20
+ ): {
21
+ analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
22
+ pending: boolean;
23
+ result: NotifyAppReadyResult;
24
+ } => ({
25
+ analyticsEvent,
26
+ pending,
27
+ result,
28
+ });
29
+
6
30
  const mocks = vi.hoisted(() => ({
7
31
  addListener: vi.fn(() => () => {}),
8
32
  checkForUpdate: vi.fn(),
33
+ getAppVersion: vi.fn(() => "1.0.0"),
9
34
  getBundleId: vi.fn(() => "bundle-id"),
10
- notifyAppReady: vi.fn(() => ({ status: "STABLE" })),
35
+ getChannel: vi.fn(() => "production"),
36
+ getCohort: vi.fn(() => "123"),
37
+ getFingerprintHash: vi.fn(() => "fingerprint-hash"),
38
+ getInstallId: vi.fn(() => "install-id"),
39
+ getPersistedUserIdentity: vi.fn(() => ({})),
40
+ readNotifyAppReady: vi.fn<
41
+ () => {
42
+ analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
43
+ pending: boolean;
44
+ result: NotifyAppReadyResult;
45
+ }
46
+ >(() => createNotifyReadResult()),
11
47
  reload: vi.fn(),
12
48
  }));
13
49
 
@@ -17,11 +53,30 @@ vi.mock("./checkForUpdate", () => ({
17
53
 
18
54
  vi.mock("./native", () => ({
19
55
  addListener: mocks.addListener,
56
+ getAppVersion: mocks.getAppVersion,
20
57
  getBundleId: mocks.getBundleId,
21
- notifyAppReady: mocks.notifyAppReady,
58
+ getChannel: mocks.getChannel,
59
+ getCohort: mocks.getCohort,
60
+ getFingerprintHash: mocks.getFingerprintHash,
61
+ getInstallId: mocks.getInstallId,
62
+ getPersistedUserIdentity: mocks.getPersistedUserIdentity,
63
+ readNotifyAppReady: mocks.readNotifyAppReady,
22
64
  reload: mocks.reload,
23
65
  }));
24
66
 
67
+ const createClient = (
68
+ sendAnalyticsEvent = vi.fn().mockResolvedValue(undefined),
69
+ ) => ({
70
+ client: {
71
+ createSession: vi.fn(async () => ({
72
+ fetchReleaseCatalog: vi.fn(),
73
+ resolveArtifact: vi.fn(),
74
+ sendAnalyticsEvent,
75
+ })),
76
+ },
77
+ sendAnalyticsEvent,
78
+ });
79
+
25
80
  describe("HotUpdater wrap initialization", () => {
26
81
  beforeEach(() => {
27
82
  vi.resetModules();
@@ -34,8 +89,14 @@ describe("HotUpdater wrap initialization", () => {
34
89
 
35
90
  mocks.checkForUpdate.mockResolvedValue(null);
36
91
  mocks.addListener.mockReturnValue(() => {});
92
+ mocks.getAppVersion.mockReturnValue("1.0.0");
93
+ mocks.getPersistedUserIdentity.mockReturnValue({});
37
94
  mocks.getBundleId.mockReturnValue("bundle-id");
38
- mocks.notifyAppReady.mockReturnValue({ status: "STABLE" });
95
+ mocks.getChannel.mockReturnValue("production");
96
+ mocks.getCohort.mockReturnValue("123");
97
+ mocks.getFingerprintHash.mockReturnValue("fingerprint-hash");
98
+ mocks.getInstallId.mockReturnValue("install-id");
99
+ mocks.readNotifyAppReady.mockReturnValue(createNotifyReadResult());
39
100
  });
40
101
 
41
102
  it("returns void from init and defers notifyAppReady to the next frame", async () => {
@@ -49,14 +110,11 @@ describe("HotUpdater wrap initialization", () => {
49
110
  );
50
111
  vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
51
112
 
52
- const resolver = {
53
- checkUpdate: vi.fn(),
54
- notifyAppReady: vi.fn().mockResolvedValue(undefined),
55
- };
113
+ const { client, sendAnalyticsEvent } = createClient();
56
114
  const { init } = await import("./wrap");
57
115
 
58
116
  const result = init({
59
- resolver,
117
+ client,
60
118
  requestHeaders: {
61
119
  Authorization: "Bearer token",
62
120
  },
@@ -64,72 +122,224 @@ describe("HotUpdater wrap initialization", () => {
64
122
  });
65
123
 
66
124
  expect(result).toBeUndefined();
67
- expect(mocks.notifyAppReady).not.toHaveBeenCalled();
68
- expect(resolver.notifyAppReady).not.toHaveBeenCalled();
125
+ expect(mocks.readNotifyAppReady).not.toHaveBeenCalled();
126
+ expect(sendAnalyticsEvent).not.toHaveBeenCalled();
69
127
 
70
128
  expect(requestAnimationFrame).toHaveBeenCalled();
71
129
 
72
130
  await vi.runOnlyPendingTimersAsync();
73
131
 
74
- expect(mocks.notifyAppReady).toHaveBeenCalledWith();
75
- expect(resolver.notifyAppReady).toHaveBeenCalledWith({
76
- status: "STABLE",
77
- crashedBundleId: undefined,
132
+ expect(mocks.readNotifyAppReady).toHaveBeenCalledWith();
133
+ expect(sendAnalyticsEvent).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it("waits for native launch verification before sending analytics", async () => {
137
+ vi.useFakeTimers();
138
+
139
+ vi.stubGlobal(
140
+ "requestAnimationFrame",
141
+ vi.fn((callback: (timestamp: number) => void) => {
142
+ setTimeout(() => callback(0), 0);
143
+ return 1;
144
+ }),
145
+ );
146
+
147
+ mocks.readNotifyAppReady
148
+ .mockReturnValueOnce(createNotifyReadResult(undefined, null, true))
149
+ .mockReturnValueOnce(
150
+ createNotifyReadResult(
151
+ {
152
+ fromBundleId: "bundle-a",
153
+ status: "UPDATE_APPLIED",
154
+ toBundleId: "bundle-b",
155
+ },
156
+ {
157
+ fromBundleId: "bundle-a",
158
+ toBundleId: "bundle-b",
159
+ type: "UPDATE_APPLIED",
160
+ updateStrategy: "appVersion",
161
+ },
162
+ ),
163
+ );
164
+
165
+ const { client, sendAnalyticsEvent } = createClient();
166
+ const { init } = await import("./wrap");
167
+
168
+ init({ analytics: true, client });
169
+
170
+ await vi.runAllTimersAsync();
171
+
172
+ expect(mocks.readNotifyAppReady).toHaveBeenCalledTimes(2);
173
+ expect(sendAnalyticsEvent).toHaveBeenCalledTimes(1);
174
+ });
175
+
176
+ it("sends automatic analytics only from init when enabled", async () => {
177
+ vi.useFakeTimers();
178
+
179
+ vi.stubGlobal(
180
+ "requestAnimationFrame",
181
+ vi.fn((callback: (timestamp: number) => void) => {
182
+ setTimeout(() => callback(0), 0);
183
+ return 1;
184
+ }),
185
+ );
186
+
187
+ mocks.readNotifyAppReady.mockReturnValue(
188
+ createNotifyReadResult(
189
+ {
190
+ fromBundleId: "bundle-a",
191
+ status: "UPDATE_APPLIED",
192
+ toBundleId: "bundle-b",
193
+ },
194
+ {
195
+ fromBundleId: "bundle-a",
196
+ toBundleId: "bundle-b",
197
+ type: "UPDATE_APPLIED",
198
+ updateStrategy: "fingerprint",
199
+ },
200
+ ),
201
+ );
202
+
203
+ mocks.getPersistedUserIdentity.mockReturnValue({
204
+ userId: "user-123",
205
+ username: "alice",
206
+ });
207
+
208
+ const { client, sendAnalyticsEvent } = createClient();
209
+ const { init } = await import("./wrap");
210
+
211
+ init({
212
+ analytics: true,
78
213
  requestHeaders: {
79
214
  Authorization: "Bearer token",
80
215
  },
81
216
  requestTimeout: 1000,
217
+ client,
218
+ });
219
+
220
+ await vi.runOnlyPendingTimersAsync();
221
+
222
+ expect(sendAnalyticsEvent).toHaveBeenCalledWith({
223
+ appVersion: "1.0.0",
224
+ channel: "production",
225
+ cohort: "123",
226
+ fingerprintHash: "fingerprint-hash",
227
+ fromBundleId: "bundle-a",
228
+ fromReleaseId: null,
229
+ installId: "install-id",
230
+ platform: "ios",
231
+ requestHeaders: {
232
+ Authorization: "Bearer token",
233
+ },
234
+ requestTimeout: 1000,
235
+ toBundleId: "bundle-b",
236
+ toReleaseId: null,
237
+ type: "UPDATE_APPLIED",
238
+ updateStrategy: "fingerprint",
239
+ userId: "user-123",
240
+ username: "alice",
82
241
  });
83
242
  });
84
243
 
85
- it("calls init onError when app-ready notification fails", async () => {
244
+ it("guards automatic analytics to a single delivery attempt per runtime", async () => {
245
+ vi.useFakeTimers();
246
+
247
+ vi.stubGlobal(
248
+ "requestAnimationFrame",
249
+ vi.fn((callback: (timestamp: number) => void) => {
250
+ setTimeout(() => callback(0), 0);
251
+ return 1;
252
+ }),
253
+ );
254
+
255
+ mocks.readNotifyAppReady.mockReturnValue(
256
+ createNotifyReadResult(
257
+ {
258
+ fromBundleId: "bundle-a",
259
+ status: "RECOVERED",
260
+ toBundleId: "bundle-b",
261
+ },
262
+ {
263
+ fromBundleId: "bundle-a",
264
+ toBundleId: "bundle-b",
265
+ type: "RECOVERED",
266
+ updateStrategy: "appVersion",
267
+ },
268
+ ),
269
+ );
270
+
271
+ const { client, sendAnalyticsEvent } = createClient();
272
+ const { init } = await import("./wrap");
273
+
274
+ init({ analytics: true, client });
275
+ init({ analytics: true, client });
276
+
277
+ await vi.runOnlyPendingTimersAsync();
278
+
279
+ expect(sendAnalyticsEvent).toHaveBeenCalledTimes(1);
280
+ });
281
+
282
+ it("warns without interrupting app readiness when analytics transport fails", async () => {
86
283
  vi.useFakeTimers();
87
284
 
88
285
  const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
89
- const error = new Error("notify failed");
286
+ const error = new Error("Expected HTTP 204 from /events, received 404");
90
287
  const onError = vi.fn();
91
- const requestAnimationFrame = vi.fn(
92
- (callback: (timestamp: number) => void) => {
288
+ const onNotifyAppReady = vi.fn();
289
+ vi.stubGlobal(
290
+ "requestAnimationFrame",
291
+ vi.fn((callback: (timestamp: number) => void) => {
93
292
  setTimeout(() => callback(0), 0);
94
293
  return 1;
95
- },
294
+ }),
96
295
  );
97
- vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
98
- const resolver = {
99
- checkUpdate: vi.fn(),
100
- notifyAppReady: vi.fn().mockRejectedValue(error),
101
- };
296
+ mocks.readNotifyAppReady.mockReturnValue(
297
+ createNotifyReadResult(
298
+ {
299
+ fromBundleId: "bundle-a",
300
+ status: "UPDATE_APPLIED",
301
+ toBundleId: "bundle-b",
302
+ },
303
+ {
304
+ fromBundleId: "bundle-a",
305
+ toBundleId: "bundle-b",
306
+ type: "UPDATE_APPLIED",
307
+ updateStrategy: "appVersion",
308
+ },
309
+ ),
310
+ );
311
+ const { client } = createClient(vi.fn().mockRejectedValue(error));
102
312
  const { init } = await import("./wrap");
103
313
 
104
314
  init({
105
- resolver,
315
+ analytics: true,
106
316
  onError,
317
+ onNotifyAppReady,
318
+ client,
107
319
  });
108
320
 
109
321
  await vi.runOnlyPendingTimersAsync();
110
322
 
111
- expect(onError).toHaveBeenCalledWith(error);
323
+ expect(onError).not.toHaveBeenCalled();
324
+ expect(warn).toHaveBeenCalledWith(
325
+ "[HotUpdater] Automatic notifyAppReady analytics failed:",
326
+ error,
327
+ );
328
+ expect(onNotifyAppReady).toHaveBeenCalledWith({
329
+ fromBundleId: "bundle-a",
330
+ status: "UPDATE_APPLIED",
331
+ toBundleId: "bundle-b",
332
+ });
112
333
  warn.mockRestore();
113
334
  });
114
335
 
115
- it("warns when the deprecated manual wrap HOC is used", async () => {
116
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
336
+ it("does not accept a manual wrap HOC", async () => {
117
337
  const { wrap } = await import("./wrap");
118
338
 
119
339
  wrap({
120
- resolver: {
121
- checkUpdate: vi.fn(),
122
- notifyAppReady: vi.fn(),
123
- },
124
- updateMode: "manual",
340
+ client: createClient().client,
341
+ updateStrategy: "appVersion",
125
342
  });
126
-
127
- expect(warn).toHaveBeenCalledWith(
128
- expect.stringContaining(
129
- 'HotUpdater.wrap({ updateMode: "manual" }) is deprecated',
130
- ),
131
- );
132
- warn.mockRestore();
133
343
  });
134
344
 
135
345
  it("preserves wrapped component prop inference", async () => {
@@ -137,11 +347,7 @@ describe("HotUpdater wrap initialization", () => {
137
347
  const Component: React.ComponentType<{ title: string }> = () => null;
138
348
 
139
349
  const WrappedComponent = wrap({
140
- resolver: {
141
- checkUpdate: vi.fn(),
142
- notifyAppReady: vi.fn(),
143
- },
144
- updateMode: "auto",
350
+ client: createClient().client,
145
351
  updateStrategy: "appVersion",
146
352
  })(Component);
147
353
 
@@ -155,18 +361,7 @@ describe("HotUpdater wrap initialization", () => {
155
361
  baseURL: "https://updates.example.com",
156
362
  updateStrategy: "appVersion",
157
363
  } satisfies HotUpdaterOptions;
158
- const explicitAutoOptions = {
159
- baseURL: "https://updates.example.com",
160
- updateMode: "auto",
161
- updateStrategy: "appVersion",
162
- } satisfies HotUpdaterOptions;
163
- const manualOptions = {
164
- baseURL: "https://updates.example.com",
165
- updateMode: "manual",
166
- } satisfies HotUpdaterOptions;
167
364
 
168
365
  expect(autoOptions.updateStrategy).toBe("appVersion");
169
- expect(explicitAutoOptions.updateMode).toBe("auto");
170
- expect(manualOptions.updateMode).toBe("manual");
171
366
  });
172
367
  });