@teardown/react-native 2.0.1 → 2.0.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teardown/react-native",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,15 +18,20 @@
18
18
  "import": "./src/exports/index.ts",
19
19
  "default": "./src/exports/index.ts"
20
20
  },
21
- "./expo": {
22
- "types": "./src/exports/expo.ts",
23
- "import": "./src/exports/expo.ts",
24
- "default": "./src/exports/expo.ts"
21
+ "./adapters/expo": {
22
+ "types": "./src/exports/adapters/expo.ts",
23
+ "import": "./src/exports/adapters/expo.ts",
24
+ "default": "./src/exports/adapters/expo.ts"
25
25
  },
26
- "./mmkv": {
27
- "types": "./src/exports/mmkv.ts",
28
- "import": "./src/exports/mmkv.ts",
29
- "default": "./src/exports/mmkv.ts"
26
+ "./adapters/mmkv": {
27
+ "types": "./src/exports/adapters/mmkv.ts",
28
+ "import": "./src/exports/adapters/mmkv.ts",
29
+ "default": "./src/exports/adapters/mmkv.ts"
30
+ },
31
+ "./adapters/async-storage": {
32
+ "types": "./src/exports/adapters/async-storage.ts",
33
+ "import": "./src/exports/adapters/async-storage.ts",
34
+ "default": "./src/exports/adapters/async-storage.ts"
30
35
  }
31
36
  },
32
37
  "scripts": {
@@ -40,11 +45,13 @@
40
45
  "nuke": "cd ../../ && bun run nuke"
41
46
  },
42
47
  "dependencies": {
43
- "@teardown/ingest-api": "0.1.36",
44
- "@teardown/schemas": "0.1.36",
45
- "@teardown/types": "0.1.36",
48
+ "@teardown/ingest-api": "0.1.41",
49
+ "@teardown/schemas": "0.1.41",
50
+ "@teardown/types": "0.1.41",
46
51
  "eventemitter3": "^5.0.1",
47
- "uuid": "^13.0.0"
52
+ "react-native-get-random-values": "^2.0.0",
53
+ "uuid": "^13.0.0",
54
+ "zod": "^4.1.13"
48
55
  },
49
56
  "devDependencies": {
50
57
  "@elysiajs/eden": "^1.4.5",
@@ -55,13 +62,34 @@
55
62
  "expo-updates": "^29.0.12"
56
63
  },
57
64
  "peerDependencies": {
65
+ "@react-native-async-storage/async-storage": "^1.21.0 || ^2.0.0",
58
66
  "react": "*",
59
67
  "react-native": "*",
60
68
  "typescript": "*",
61
- "expo-application": "^7.0",
62
- "expo-device": "^8.0",
63
- "expo-notifications": "^0.29",
64
- "react-native-mmkv": "^3.0",
65
- "zod": "^4"
69
+ "expo-application": "*",
70
+ "expo-device": "*",
71
+ "expo-notifications": "*",
72
+ "expo-secure-store": "*",
73
+ "react-native-mmkv": "*"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "@react-native-async-storage/async-storage": {
77
+ "optional": true
78
+ },
79
+ "react-native-mmkv": {
80
+ "optional": true
81
+ },
82
+ "expo-application": {
83
+ "optional": true
84
+ },
85
+ "expo-device": {
86
+ "optional": true
87
+ },
88
+ "expo-notifications": {
89
+ "optional": true
90
+ },
91
+ "expo-secure-store": {
92
+ "optional": true
93
+ }
66
94
  }
67
95
  }
@@ -1,9 +1,9 @@
1
- import * as Eden from "@elysiajs/eden";
1
+ import type * as Eden from "@elysiajs/eden";
2
2
  import * as IngestApi from "@teardown/ingest-api";
3
3
  import type { LoggingClient } from "../logging";
4
4
  import type { StorageClient } from "../storage";
5
5
 
6
- export { Eden, IngestApi };
6
+ export type { Eden, IngestApi };
7
7
 
8
8
  const TEARDOWN_INGEST_URL = "https://ingest.teardown.dev";
9
9
  const TEARDOWN_API_KEY_HEADER = "td-api-key";
@@ -9,7 +9,7 @@ import * as Device from "expo-device";
9
9
  import { Platform } from "react-native";
10
10
 
11
11
  import { DeviceInfoAdapter } from "./device.adpater-interface";
12
- import { DevicePlatformEnum, NotificationPlatformEnum } from "./device.client";
12
+ import { DevicePlatformEnum, NotificationPlatformEnum } from "../device.client";
13
13
 
14
14
  /**
15
15
  * Maps expo-device DeviceType to a string representation
@@ -50,7 +50,7 @@ export abstract class DeviceInfoAdapter {
50
50
  application: this.applicationInfo,
51
51
  hardware: this.hardwareInfo,
52
52
  os: this.osInfo,
53
- notifications: this.notificationsInfo,
53
+ notifications: null,
54
54
  update: null,
55
55
  });
56
56
  }
@@ -0,0 +1,90 @@
1
+ import type {
2
+ ApplicationInfo,
3
+ HardwareInfo,
4
+ NotificationsInfo,
5
+ OSInfo,
6
+ } from "@teardown/schemas";
7
+ import * as Application from "expo-application";
8
+ import * as Device from "expo-device";
9
+ import { Platform } from "react-native";
10
+
11
+ import { DeviceInfoAdapter } from "./device.adpater-interface";
12
+ import { DevicePlatformEnum, NotificationPlatformEnum } from "../device.client";
13
+
14
+ /**
15
+ * Maps expo-device DeviceType to a string representation
16
+ */
17
+ function mapDeviceType(deviceType: Device.DeviceType | null): string {
18
+ switch (deviceType) {
19
+ case Device.DeviceType.PHONE:
20
+ return "phone";
21
+ case Device.DeviceType.TABLET:
22
+ return "tablet";
23
+ case Device.DeviceType.DESKTOP:
24
+ return "desktop";
25
+ case Device.DeviceType.TV:
26
+ return "tv";
27
+ default:
28
+ return "unknown";
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Maps React Native Platform.OS to DevicePlatformEnum
34
+ */
35
+ function mapPlatform(platform: typeof Platform.OS): DevicePlatformEnum {
36
+ switch (platform) {
37
+ case "ios":
38
+ return DevicePlatformEnum.IOS;
39
+ case "android":
40
+ return DevicePlatformEnum.ANDROID;
41
+ case "web":
42
+ return DevicePlatformEnum.WEB;
43
+ case "macos":
44
+ return DevicePlatformEnum.MACOS;
45
+ case "windows":
46
+ return DevicePlatformEnum.WINDOWS;
47
+ default:
48
+ return DevicePlatformEnum.UNKNOWN;
49
+ }
50
+ }
51
+
52
+ export class ExpoDeviceAdapter extends DeviceInfoAdapter {
53
+ get applicationInfo(): ApplicationInfo {
54
+ return {
55
+ version: Application.nativeApplicationVersion ?? "0.0.0",
56
+ build_number: Number.parseInt(
57
+ Application.nativeBuildVersion ?? "0",
58
+ 10
59
+ ),
60
+ };
61
+ }
62
+
63
+ get hardwareInfo(): HardwareInfo {
64
+ return {
65
+ device_name: Device.deviceName ?? "Unknown Device",
66
+ device_brand: Device.brand ?? "Unknown Brand",
67
+ device_type: mapDeviceType(Device.deviceType),
68
+ };
69
+ }
70
+
71
+ get osInfo(): OSInfo {
72
+ return {
73
+ platform: mapPlatform(Platform.OS),
74
+ name: Device.osName ?? Platform.OS,
75
+ version: Device.osVersion ?? "0.0.0",
76
+ };
77
+ }
78
+
79
+ get notificationsInfo(): NotificationsInfo {
80
+ return {
81
+ push: {
82
+ enabled: false,
83
+ granted: false,
84
+ token: null,
85
+ platform: NotificationPlatformEnum.EXPO,
86
+ },
87
+ };
88
+ }
89
+
90
+ }
@@ -1,6 +1,6 @@
1
1
  import type { DeviceInfo } from "@teardown/schemas";
2
2
  import { describe, expect, test } from "bun:test";
3
- import type { DeviceInfoAdapter } from "./device.adpater-interface";
3
+ import type { DeviceInfoAdapter } from "./adapters/device.adpater-interface";
4
4
  import { DeviceClient } from "./device.client";
5
5
 
6
6
  function createMockLoggingClient() {
@@ -2,7 +2,7 @@ import type { DeviceInfo } from "@teardown/schemas";
2
2
  import type { Logger, LoggingClient } from "../logging/";
3
3
  import type { StorageClient, SupportedStorage } from "../storage";
4
4
  import type { UtilsClient } from "../utils/utils.client";
5
- import type { DeviceInfoAdapter } from "./device.adpater-interface";
5
+ import type { DeviceInfoAdapter } from "./adapters/device.adpater-interface";
6
6
 
7
7
  // TODO: sort out why importing these enuims from schemas is not working - @teardown/schemas
8
8
  export enum NotificationPlatformEnum {
@@ -54,11 +54,13 @@ export class DeviceClient {
54
54
 
55
55
  this.logger.debug("Getting device ID");
56
56
  const deviceId = this.storage.getItem("deviceId");
57
+ this.logger.debug(`Device ID found in storage: ${deviceId}`);
57
58
  if (deviceId) {
58
59
  return deviceId;
59
60
  }
60
61
 
61
62
  const newDeviceId = await this.utils.generateRandomUUID();
63
+ await this.storage.setItem("deviceId", newDeviceId);
62
64
 
63
65
  return newDeviceId;
64
66
  }
@@ -66,4 +68,6 @@ export class DeviceClient {
66
68
  async getDeviceInfo(): Promise<DeviceInfo> {
67
69
  return this.options.adapter.getDeviceInfo();
68
70
  }
71
+
72
+
69
73
  }
@@ -1,4 +1,4 @@
1
1
  // export * from "./adapters/device-info.adapter";
2
2
  // export * from "./adapters/expo-device.adapter";
3
- export * from "./device.adpater-interface";
3
+ export * from "./adapters/device.adpater-interface";
4
4
  export * from "./device.client";
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect, beforeEach, mock } from "bun:test";
2
2
  import { EventEmitter } from "eventemitter3";
3
- import { ForceUpdateClient, IdentifyVersionStatusEnum } from "./force-update.client";
3
+ import { ForceUpdateClient, IdentifyVersionStatusEnum, VERSION_STATUS_STORAGE_KEY } from "./force-update.client";
4
4
 
5
5
  // Must mock react-native BEFORE any imports that use it
6
6
  const mockAppStateListeners: ((state: string) => void)[] = [];
@@ -17,10 +17,11 @@ type IdentifyState = import("../identity").IdentifyState;
17
17
  type IdentifyStateChangeEvents = import("../identity").IdentifyStateChangeEvents;
18
18
  type VersionStatus = import("./force-update.client").VersionStatus;
19
19
 
20
- function createMockIdentityClient() {
20
+ function createMockIdentityClient(initialState?: IdentifyState) {
21
21
  const emitter = new EventEmitter<IdentifyStateChangeEvents>();
22
22
  let identifyCallCount = 0;
23
- let nextIdentifyResult: { success: boolean; data?: { version_info: { status: IdentifyVersionStatusEnum } } } = {
23
+ let currentState: IdentifyState = initialState ?? { type: "unidentified" };
24
+ let nextIdentifyResult: { success: boolean; data?: { version_info: { status: IdentifyVersionStatusEnum } } } | null = {
24
25
  success: true,
25
26
  data: { version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE } },
26
27
  };
@@ -31,14 +32,20 @@ function createMockIdentityClient() {
31
32
  emitter.addListener("IDENTIFY_STATE_CHANGED", listener);
32
33
  return () => emitter.removeListener("IDENTIFY_STATE_CHANGED", listener);
33
34
  },
35
+ getIdentifyState: () => currentState,
34
36
  identify: async () => {
35
37
  identifyCallCount++;
36
- emitter.emit("IDENTIFY_STATE_CHANGED", { type: "identifying" });
37
- emitter.emit("IDENTIFY_STATE_CHANGED", {
38
+ if (nextIdentifyResult === null) {
39
+ return null;
40
+ }
41
+ currentState = { type: "identifying" };
42
+ emitter.emit("IDENTIFY_STATE_CHANGED", currentState);
43
+ currentState = {
38
44
  type: "identified",
39
45
  session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
40
46
  version_info: { status: nextIdentifyResult.data?.version_info.status ?? IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
41
- });
47
+ };
48
+ emitter.emit("IDENTIFY_STATE_CHANGED", currentState);
42
49
  return nextIdentifyResult;
43
50
  },
44
51
  getIdentifyCallCount: () => identifyCallCount,
@@ -67,6 +74,7 @@ function createMockStorageClient() {
67
74
  setItem: (key: string, value: string) => storage.set(key, value),
68
75
  removeItem: (key: string) => storage.delete(key),
69
76
  }),
77
+ getStorage: () => storage,
70
78
  };
71
79
  }
72
80
 
@@ -75,6 +83,83 @@ describe("ForceUpdateClient", () => {
75
83
  mockAppStateListeners.length = 0;
76
84
  });
77
85
 
86
+ describe("initialization from current identity state", () => {
87
+ test("initializes version status when identity is already identified", () => {
88
+ const mockIdentity = createMockIdentityClient({
89
+ type: "identified",
90
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
91
+ version_info: { status: IdentifyVersionStatusEnum.UPDATE_REQUIRED, update: null },
92
+ });
93
+ const mockLogging = createMockLoggingClient();
94
+ const mockStorage = createMockStorageClient();
95
+
96
+ const client = new ForceUpdateClient(
97
+ mockLogging as never,
98
+ mockStorage as never,
99
+ mockIdentity as never
100
+ );
101
+ client.initialize();
102
+
103
+ // Should immediately have update_required status from initialization
104
+ expect(client.getVersionStatus().type).toBe("update_required");
105
+
106
+ client.shutdown();
107
+ });
108
+
109
+ test("stays in initializing when identity is unidentified", () => {
110
+ const mockIdentity = createMockIdentityClient({ type: "unidentified" });
111
+ const mockLogging = createMockLoggingClient();
112
+ const mockStorage = createMockStorageClient();
113
+
114
+ const client = new ForceUpdateClient(
115
+ mockLogging as never,
116
+ mockStorage as never,
117
+ mockIdentity as never
118
+ );
119
+ client.initialize();
120
+
121
+ // Should stay in initializing since not yet identified
122
+ expect(client.getVersionStatus().type).toBe("initializing");
123
+
124
+ client.shutdown();
125
+ });
126
+
127
+ test("emits status change during initialization when already identified", () => {
128
+ const mockIdentity = createMockIdentityClient({
129
+ type: "identified",
130
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
131
+ version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
132
+ });
133
+ const mockLogging = createMockLoggingClient();
134
+ const mockStorage = createMockStorageClient();
135
+
136
+ const statusChanges: VersionStatus[] = [];
137
+
138
+ const client = new ForceUpdateClient(
139
+ mockLogging as never,
140
+ mockStorage as never,
141
+ mockIdentity as never
142
+ );
143
+ client.initialize();
144
+
145
+ // Subscribe after construction to verify initial status was set
146
+ client.onVersionStatusChange((status) => statusChanges.push(status));
147
+
148
+ // Trigger another identify to verify no duplicate
149
+ mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", {
150
+ type: "identified",
151
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
152
+ version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
153
+ });
154
+
155
+ // Should only have one change from the second emit (initial was before subscription)
156
+ expect(statusChanges).toHaveLength(1);
157
+ expect(client.getVersionStatus().type).toBe("up_to_date");
158
+
159
+ client.shutdown();
160
+ });
161
+ });
162
+
78
163
  describe("updateFromVersionStatus single emission", () => {
79
164
  test("emits VERSION_STATUS_CHANGED exactly once per identify cycle", async () => {
80
165
  const mockIdentity = createMockIdentityClient();
@@ -87,6 +172,7 @@ describe("ForceUpdateClient", () => {
87
172
  mockIdentity as never,
88
173
  { throttleMs: 0, checkCooldownMs: 0 }
89
174
  );
175
+ client.initialize();
90
176
 
91
177
  const statusChanges: VersionStatus[] = [];
92
178
  client.onVersionStatusChange((status) => statusChanges.push(status));
@@ -123,6 +209,7 @@ describe("ForceUpdateClient", () => {
123
209
  mockIdentity as never,
124
210
  { throttleMs: 0, checkCooldownMs: 0 }
125
211
  );
212
+ client.initialize();
126
213
 
127
214
  const statusChanges: VersionStatus[] = [];
128
215
  client.onVersionStatusChange((status) => statusChanges.push(status));
@@ -156,6 +243,7 @@ describe("ForceUpdateClient", () => {
156
243
  mockStorage as never,
157
244
  mockIdentity as never
158
245
  );
246
+ client.initialize();
159
247
 
160
248
  const statusChanges: VersionStatus[] = [];
161
249
  client.onVersionStatusChange((status) => statusChanges.push(status));
@@ -183,6 +271,7 @@ describe("ForceUpdateClient", () => {
183
271
  mockStorage as never,
184
272
  mockIdentity as never
185
273
  );
274
+ client.initialize();
186
275
 
187
276
  expect(mockAppStateListeners).toHaveLength(1);
188
277
 
@@ -193,6 +282,60 @@ describe("ForceUpdateClient", () => {
193
282
  });
194
283
 
195
284
  describe("throttle and cooldown", () => {
285
+ test("skips version check when identify returns null", async () => {
286
+ const mockIdentity = createMockIdentityClient();
287
+ const mockLogging = createMockLoggingClient();
288
+ const mockStorage = createMockStorageClient();
289
+
290
+ mockIdentity.setNextIdentifyResult(null);
291
+
292
+ const client = new ForceUpdateClient(
293
+ mockLogging as never,
294
+ mockStorage as never,
295
+ mockIdentity as never,
296
+ { throttleMs: 0, checkCooldownMs: 0 }
297
+ );
298
+ client.initialize();
299
+
300
+ const statusChanges: VersionStatus[] = [];
301
+ client.onVersionStatusChange((status) => statusChanges.push(status));
302
+
303
+ const foregroundHandler = mockAppStateListeners[0];
304
+ await foregroundHandler("active");
305
+ await new Promise((r) => setTimeout(r, 10));
306
+
307
+ // Should have called identify but no status changes (null result)
308
+ expect(mockIdentity.getIdentifyCallCount()).toBe(1);
309
+ expect(statusChanges).toHaveLength(0);
310
+
311
+ client.shutdown();
312
+ });
313
+
314
+ test("checkCooldownMs -1 disables version checking entirely", async () => {
315
+ const mockIdentity = createMockIdentityClient();
316
+ const mockLogging = createMockLoggingClient();
317
+ const mockStorage = createMockStorageClient();
318
+
319
+ const client = new ForceUpdateClient(
320
+ mockLogging as never,
321
+ mockStorage as never,
322
+ mockIdentity as never,
323
+ { throttleMs: 0, checkCooldownMs: -1 }
324
+ );
325
+ client.initialize();
326
+
327
+ const foregroundHandler = mockAppStateListeners[0];
328
+
329
+ // Trigger foreground
330
+ await foregroundHandler("active");
331
+ await new Promise((r) => setTimeout(r, 10));
332
+
333
+ // Should not have called identify at all
334
+ expect(mockIdentity.getIdentifyCallCount()).toBe(0);
335
+
336
+ client.shutdown();
337
+ });
338
+
196
339
  test("throttle prevents rapid foreground checks", async () => {
197
340
  const mockIdentity = createMockIdentityClient();
198
341
  const mockLogging = createMockLoggingClient();
@@ -204,6 +347,7 @@ describe("ForceUpdateClient", () => {
204
347
  mockIdentity as never,
205
348
  { throttleMs: 1000, checkCooldownMs: 0 }
206
349
  );
350
+ client.initialize();
207
351
 
208
352
  const foregroundHandler = mockAppStateListeners[0];
209
353
 
@@ -237,6 +381,7 @@ describe("ForceUpdateClient", () => {
237
381
  mockIdentity as never,
238
382
  { throttleMs: 0, checkCooldownMs: 5000 }
239
383
  );
384
+ client.initialize();
240
385
 
241
386
  const foregroundHandler = mockAppStateListeners[0];
242
387
 
@@ -260,6 +405,92 @@ describe("ForceUpdateClient", () => {
260
405
  });
261
406
  });
262
407
 
408
+ describe("stale storage state reset", () => {
409
+ test("resets stale 'checking' state from storage to initializing", () => {
410
+ const mockIdentity = createMockIdentityClient({ type: "unidentified" });
411
+ const mockLogging = createMockLoggingClient();
412
+ const mockStorage = createMockStorageClient();
413
+
414
+ // Pre-populate storage with stale "checking" state
415
+ mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "checking" }));
416
+
417
+ const client = new ForceUpdateClient(
418
+ mockLogging as never,
419
+ mockStorage as never,
420
+ mockIdentity as never
421
+ );
422
+ client.initialize();
423
+
424
+ // Should reset to initializing, not stay in checking
425
+ expect(client.getVersionStatus().type).toBe("initializing");
426
+
427
+ client.shutdown();
428
+ });
429
+
430
+ test("resets stale 'initializing' state from storage to initializing", () => {
431
+ const mockIdentity = createMockIdentityClient({ type: "unidentified" });
432
+ const mockLogging = createMockLoggingClient();
433
+ const mockStorage = createMockStorageClient();
434
+
435
+ // Pre-populate storage with stale "initializing" state
436
+ mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "initializing" }));
437
+
438
+ const client = new ForceUpdateClient(
439
+ mockLogging as never,
440
+ mockStorage as never,
441
+ mockIdentity as never
442
+ );
443
+ client.initialize();
444
+
445
+ // Should reset to initializing (which it already is, but storage should be cleared)
446
+ expect(client.getVersionStatus().type).toBe("initializing");
447
+
448
+ client.shutdown();
449
+ });
450
+
451
+ test("preserves valid 'up_to_date' state from storage", () => {
452
+ const mockIdentity = createMockIdentityClient({ type: "unidentified" });
453
+ const mockLogging = createMockLoggingClient();
454
+ const mockStorage = createMockStorageClient();
455
+
456
+ // Pre-populate storage with valid state
457
+ mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "up_to_date" }));
458
+
459
+ const client = new ForceUpdateClient(
460
+ mockLogging as never,
461
+ mockStorage as never,
462
+ mockIdentity as never
463
+ );
464
+ client.initialize();
465
+
466
+ // Should keep up_to_date from storage
467
+ expect(client.getVersionStatus().type).toBe("up_to_date");
468
+
469
+ client.shutdown();
470
+ });
471
+
472
+ test("preserves valid 'update_required' state from storage", () => {
473
+ const mockIdentity = createMockIdentityClient({ type: "unidentified" });
474
+ const mockLogging = createMockLoggingClient();
475
+ const mockStorage = createMockStorageClient();
476
+
477
+ // Pre-populate storage with valid state
478
+ mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "update_required" }));
479
+
480
+ const client = new ForceUpdateClient(
481
+ mockLogging as never,
482
+ mockStorage as never,
483
+ mockIdentity as never
484
+ );
485
+ client.initialize();
486
+
487
+ // Should keep update_required from storage
488
+ expect(client.getVersionStatus().type).toBe("update_required");
489
+
490
+ client.shutdown();
491
+ });
492
+ });
493
+
263
494
  describe("version status mapping", () => {
264
495
  test.each([
265
496
  [IdentifyVersionStatusEnum.UP_TO_DATE, "up_to_date"],
@@ -277,6 +508,7 @@ describe("ForceUpdateClient", () => {
277
508
  mockStorage as never,
278
509
  mockIdentity as never
279
510
  );
511
+ client.initialize();
280
512
 
281
513
  const statusChanges: VersionStatus[] = [];
282
514
  client.onVersionStatusChange((status) => statusChanges.push(status));