@teardown/react-native 2.0.19 → 2.0.23

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.19",
3
+ "version": "2.0.23",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/.DS_Store ADDED
Binary file
@@ -37,7 +37,6 @@ export type ApiClientOptions = {
37
37
  * @returns The options for the request.
38
38
  */
39
39
  onRequest?: (endpoint: IngestApi.Endpoints, options: IngestApi.RequestOptions) => Promise<IngestApi.RequestOptions>;
40
-
41
40
  /**
42
41
  * The URL of the ingest API.
43
42
  * @default https://ingest.teardown.dev
@@ -31,9 +31,9 @@ export enum DevicePlatformEnum {
31
31
  OTHER = "OTHER",
32
32
  }
33
33
 
34
- export type DeviceClientOptions = {
34
+ export interface DeviceClientOptions {
35
35
  adapter: DeviceInfoAdapter;
36
- };
36
+ }
37
37
  export class DeviceClient {
38
38
  private logger: Logger;
39
39
  private storage: SupportedStorage;
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, beforeEach, mock } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
  import { EventEmitter } from "eventemitter3";
3
3
  import { ForceUpdateClient, IdentifyVersionStatusEnum, VERSION_STATUS_STORAGE_KEY } from "./force-update.client";
4
4
 
@@ -21,10 +21,11 @@ function createMockIdentityClient(initialState?: IdentifyState) {
21
21
  const emitter = new EventEmitter<IdentifyStateChangeEvents>();
22
22
  let identifyCallCount = 0;
23
23
  let currentState: IdentifyState = initialState ?? { type: "unidentified" };
24
- let nextIdentifyResult: { success: boolean; data?: { version_info: { status: IdentifyVersionStatusEnum } } } | null = {
25
- success: true,
26
- data: { version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE } },
27
- };
24
+ let nextIdentifyResult: { success: boolean; data?: { version_info: { status: IdentifyVersionStatusEnum } } } | null =
25
+ {
26
+ success: true,
27
+ data: { version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE } },
28
+ };
28
29
 
29
30
  return {
30
31
  emitter,
@@ -43,7 +44,10 @@ function createMockIdentityClient(initialState?: IdentifyState) {
43
44
  currentState = {
44
45
  type: "identified",
45
46
  session: { session_id: "s1", device_id: "d1", user_id: "p1", token: "t1" },
46
- version_info: { status: nextIdentifyResult.data?.version_info.status ?? IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
47
+ version_info: {
48
+ status: nextIdentifyResult.data?.version_info.status ?? IdentifyVersionStatusEnum.UP_TO_DATE,
49
+ update: null,
50
+ },
47
51
  };
48
52
  emitter.emit("IDENTIFY_STATE_CHANGED", currentState);
49
53
  return nextIdentifyResult;
@@ -58,10 +62,10 @@ function createMockIdentityClient(initialState?: IdentifyState) {
58
62
  function createMockLoggingClient() {
59
63
  return {
60
64
  createLogger: () => ({
61
- info: () => { },
62
- warn: () => { },
63
- error: () => { },
64
- debug: () => { },
65
+ info: () => {},
66
+ warn: () => {},
67
+ error: () => {},
68
+ debug: () => {},
65
69
  }),
66
70
  };
67
71
  }
@@ -93,11 +97,7 @@ describe("ForceUpdateClient", () => {
93
97
  const mockLogging = createMockLoggingClient();
94
98
  const mockStorage = createMockStorageClient();
95
99
 
96
- const client = new ForceUpdateClient(
97
- mockLogging as never,
98
- mockStorage as never,
99
- mockIdentity as never
100
- );
100
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
101
101
  client.initialize();
102
102
 
103
103
  // Should immediately have update_required status from initialization
@@ -111,11 +111,7 @@ describe("ForceUpdateClient", () => {
111
111
  const mockLogging = createMockLoggingClient();
112
112
  const mockStorage = createMockStorageClient();
113
113
 
114
- const client = new ForceUpdateClient(
115
- mockLogging as never,
116
- mockStorage as never,
117
- mockIdentity as never
118
- );
114
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
119
115
  client.initialize();
120
116
 
121
117
  // Should stay in initializing since not yet identified
@@ -135,11 +131,7 @@ describe("ForceUpdateClient", () => {
135
131
 
136
132
  const statusChanges: VersionStatus[] = [];
137
133
 
138
- const client = new ForceUpdateClient(
139
- mockLogging as never,
140
- mockStorage as never,
141
- mockIdentity as never
142
- );
134
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
143
135
  client.initialize();
144
136
 
145
137
  // Subscribe after construction to verify initial status was set
@@ -166,18 +158,15 @@ describe("ForceUpdateClient", () => {
166
158
  const mockLogging = createMockLoggingClient();
167
159
  const mockStorage = createMockStorageClient();
168
160
 
169
- const client = new ForceUpdateClient(
170
- mockLogging as never,
171
- mockStorage as never,
172
- mockIdentity as never,
173
- { throttleMs: 0, checkCooldownMs: 0 }
174
- );
161
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
162
+ checkOnForeground: true,
163
+ });
175
164
  client.initialize();
176
165
 
177
166
  const statusChanges: VersionStatus[] = [];
178
167
  client.onVersionStatusChange((status) => statusChanges.push(status));
179
168
 
180
- // Trigger identify via state change
169
+ // Trigger identify via identify state change
181
170
  mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", { type: "identifying" });
182
171
  mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", {
183
172
  type: "identified",
@@ -203,12 +192,9 @@ describe("ForceUpdateClient", () => {
203
192
  data: { version_info: { status: IdentifyVersionStatusEnum.UPDATE_REQUIRED } },
204
193
  });
205
194
 
206
- const client = new ForceUpdateClient(
207
- mockLogging as never,
208
- mockStorage as never,
209
- mockIdentity as never,
210
- { throttleMs: 0, checkCooldownMs: 0 }
211
- );
195
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
196
+ checkOnForeground: true,
197
+ });
212
198
  client.initialize();
213
199
 
214
200
  const statusChanges: VersionStatus[] = [];
@@ -238,11 +224,7 @@ describe("ForceUpdateClient", () => {
238
224
  const mockLogging = createMockLoggingClient();
239
225
  const mockStorage = createMockStorageClient();
240
226
 
241
- const client = new ForceUpdateClient(
242
- mockLogging as never,
243
- mockStorage as never,
244
- mockIdentity as never
245
- );
227
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
246
228
  client.initialize();
247
229
 
248
230
  const statusChanges: VersionStatus[] = [];
@@ -266,11 +248,7 @@ describe("ForceUpdateClient", () => {
266
248
  const mockLogging = createMockLoggingClient();
267
249
  const mockStorage = createMockStorageClient();
268
250
 
269
- const client = new ForceUpdateClient(
270
- mockLogging as never,
271
- mockStorage as never,
272
- mockIdentity as never
273
- );
251
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
274
252
  client.initialize();
275
253
 
276
254
  expect(mockAppStateListeners).toHaveLength(1);
@@ -289,12 +267,10 @@ describe("ForceUpdateClient", () => {
289
267
 
290
268
  mockIdentity.setNextIdentifyResult(null);
291
269
 
292
- const client = new ForceUpdateClient(
293
- mockLogging as never,
294
- mockStorage as never,
295
- mockIdentity as never,
296
- { throttleMs: 0, checkCooldownMs: 0 }
297
- );
270
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
271
+ throttleMs: 0,
272
+ checkCooldownMs: 0,
273
+ });
298
274
  client.initialize();
299
275
 
300
276
  const statusChanges: VersionStatus[] = [];
@@ -316,12 +292,10 @@ describe("ForceUpdateClient", () => {
316
292
  const mockLogging = createMockLoggingClient();
317
293
  const mockStorage = createMockStorageClient();
318
294
 
319
- const client = new ForceUpdateClient(
320
- mockLogging as never,
321
- mockStorage as never,
322
- mockIdentity as never,
323
- { throttleMs: 0, checkCooldownMs: -1 }
324
- );
295
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
296
+ throttleMs: 0,
297
+ checkCooldownMs: -1,
298
+ });
325
299
  client.initialize();
326
300
 
327
301
  const foregroundHandler = mockAppStateListeners[0];
@@ -341,12 +315,11 @@ describe("ForceUpdateClient", () => {
341
315
  const mockLogging = createMockLoggingClient();
342
316
  const mockStorage = createMockStorageClient();
343
317
 
344
- const client = new ForceUpdateClient(
345
- mockLogging as never,
346
- mockStorage as never,
347
- mockIdentity as never,
348
- { throttleMs: 1000, checkCooldownMs: 0 }
349
- );
318
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
319
+ checkOnForeground: false,
320
+ throttleMs: 100,
321
+ checkCooldownMs: 0,
322
+ });
350
323
  client.initialize();
351
324
 
352
325
  const foregroundHandler = mockAppStateListeners[0];
@@ -363,24 +336,55 @@ describe("ForceUpdateClient", () => {
363
336
 
364
337
  const callsAfterSecond = mockIdentity.getIdentifyCallCount();
365
338
 
366
- // Should only have one identify call due to throttle
339
+ // Only first call should have triggered identify (throttle blocks second)
367
340
  expect(callsAfterFirst).toBe(1);
368
341
  expect(callsAfterSecond).toBe(1);
369
342
 
370
343
  client.shutdown();
371
344
  });
372
345
 
373
- test("cooldown prevents redundant checks after recent success", async () => {
346
+ test("checkOnForeground: true always checks on foreground", async () => {
374
347
  const mockIdentity = createMockIdentityClient();
375
348
  const mockLogging = createMockLoggingClient();
376
349
  const mockStorage = createMockStorageClient();
377
350
 
378
- const client = new ForceUpdateClient(
379
- mockLogging as never,
380
- mockStorage as never,
381
- mockIdentity as never,
382
- { throttleMs: 0, checkCooldownMs: 5000 }
383
- );
351
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
352
+ checkOnForeground: true,
353
+ throttleMs: 100_000,
354
+ });
355
+ client.initialize();
356
+
357
+ const foregroundHandler = mockAppStateListeners[0];
358
+
359
+ // First foreground
360
+ await foregroundHandler("active");
361
+ await new Promise((r) => setTimeout(r, 10));
362
+
363
+ const callsAfterFirst = mockIdentity.getIdentifyCallCount();
364
+
365
+ // Second foreground immediately (within throttle window)
366
+ await foregroundHandler("active");
367
+ await new Promise((r) => setTimeout(r, 10));
368
+
369
+ const callsAfterSecond = mockIdentity.getIdentifyCallCount();
370
+
371
+ // Both should trigger identify calls with checkOnForeground: true
372
+ expect(callsAfterFirst).toBe(1);
373
+ expect(callsAfterSecond).toBe(2);
374
+
375
+ client.shutdown();
376
+ });
377
+
378
+ test("cooldown prevents checks too soon after successful check", async () => {
379
+ const mockIdentity = createMockIdentityClient();
380
+ const mockLogging = createMockLoggingClient();
381
+ const mockStorage = createMockStorageClient();
382
+
383
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
384
+ checkOnForeground: false,
385
+ throttleMs: 0,
386
+ checkCooldownMs: 100_000,
387
+ });
384
388
  client.initialize();
385
389
 
386
390
  const foregroundHandler = mockAppStateListeners[0];
@@ -391,7 +395,7 @@ describe("ForceUpdateClient", () => {
391
395
 
392
396
  const callsAfterFirst = mockIdentity.getIdentifyCallCount();
393
397
 
394
- // Second foreground (within cooldown window)
398
+ // Second foreground immediately (within cooldown window)
395
399
  await foregroundHandler("active");
396
400
  await new Promise((r) => setTimeout(r, 10));
397
401
 
@@ -414,11 +418,7 @@ describe("ForceUpdateClient", () => {
414
418
  // Pre-populate storage with stale "checking" state
415
419
  mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "checking" }));
416
420
 
417
- const client = new ForceUpdateClient(
418
- mockLogging as never,
419
- mockStorage as never,
420
- mockIdentity as never
421
- );
421
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
422
422
  client.initialize();
423
423
 
424
424
  // Should reset to initializing, not stay in checking
@@ -435,11 +435,7 @@ describe("ForceUpdateClient", () => {
435
435
  // Pre-populate storage with stale "initializing" state
436
436
  mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "initializing" }));
437
437
 
438
- const client = new ForceUpdateClient(
439
- mockLogging as never,
440
- mockStorage as never,
441
- mockIdentity as never
442
- );
438
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
443
439
  client.initialize();
444
440
 
445
441
  // Should reset to initializing (which it already is, but storage should be cleared)
@@ -456,11 +452,7 @@ describe("ForceUpdateClient", () => {
456
452
  // Pre-populate storage with valid state
457
453
  mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "up_to_date" }));
458
454
 
459
- const client = new ForceUpdateClient(
460
- mockLogging as never,
461
- mockStorage as never,
462
- mockIdentity as never
463
- );
455
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
464
456
  client.initialize();
465
457
 
466
458
  // Should keep up_to_date from storage
@@ -477,11 +469,7 @@ describe("ForceUpdateClient", () => {
477
469
  // Pre-populate storage with valid state
478
470
  mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "update_required" }));
479
471
 
480
- const client = new ForceUpdateClient(
481
- mockLogging as never,
482
- mockStorage as never,
483
- mockIdentity as never
484
- );
472
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
485
473
  client.initialize();
486
474
 
487
475
  // Should keep update_required from storage
@@ -503,11 +491,7 @@ describe("ForceUpdateClient", () => {
503
491
  const mockLogging = createMockLoggingClient();
504
492
  const mockStorage = createMockStorageClient();
505
493
 
506
- const client = new ForceUpdateClient(
507
- mockLogging as never,
508
- mockStorage as never,
509
- mockIdentity as never
510
- );
494
+ const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
511
495
  client.initialize();
512
496
 
513
497
  const statusChanges: VersionStatus[] = [];
@@ -29,7 +29,6 @@ export enum IdentifyVersionStatusEnum {
29
29
  DISABLED = "DISABLED",
30
30
  }
31
31
 
32
-
33
32
  export const InitializingVersionStatusSchema = z.object({ type: z.literal("initializing") });
34
33
  export const CheckingVersionStatusSchema = z.object({ type: z.literal("checking") });
35
34
  export const UpToDateVersionStatusSchema = z.object({ type: z.literal("up_to_date") });
@@ -64,43 +63,49 @@ export type UpdateRequiredVersionStatus = z.infer<typeof UpdateRequiredVersionSt
64
63
  export type DisabledVersionStatus = z.infer<typeof DisabledVersionStatusSchema>;
65
64
  export type VersionStatus = z.infer<typeof VersionStatusSchema>;
66
65
 
67
- export type VersionStatusChangeEvents = {
66
+ export interface VersionStatusChangeEvents {
68
67
  VERSION_STATUS_CHANGED: (status: VersionStatus) => void;
69
- };
68
+ }
70
69
 
71
70
  export type ForceUpdateClientOptions = {
72
- /**
71
+ /**
73
72
  * Minimum time (ms) between foreground transitions to prevent rapid-fire checks.
74
73
  * Measured from the last time the app came to foreground.
75
74
  * Prevents checking when user quickly switches apps back and forth.
76
75
  * Default: 30000 (30 seconds)
77
- *
76
+ *
78
77
  * Special values:
79
78
  * - -1: Disable throttling, check on every foreground (respects checkCooldownMs)
80
- *
79
+ *
81
80
  * Example: If throttleMs is 30s and user backgrounds then foregrounds the app
82
81
  * twice within 20s, only the first transition triggers a check.
83
82
  */
84
83
  throttleMs?: number;
85
- /**
84
+ /**
86
85
  * Minimum time (ms) since the last successful version check before checking again.
87
86
  * Measured from when the last check completed successfully (not when it started).
88
87
  * Prevents unnecessary API calls after we already have fresh version data.
89
88
  * Default: 300000 (5 minutes)
90
- *
89
+ *
91
90
  * Special values:
92
91
  * - 0: Disable cooldown, check on every foreground (respects throttleMs)
93
92
  * - -1: Disable all automatic version checking
94
- *
93
+ *
95
94
  * Example: If checkCooldownMs is 5min and a check completes at 12:00pm,
96
95
  * no new checks occur until 12:05pm, even if user foregrounds the app multiple times.
97
96
  */
98
97
  checkCooldownMs?: number;
98
+ /** Always check on foreground, ignoring throttle (default: true) */
99
+ checkOnForeground?: boolean;
100
+ /** If true, check version even when not identified by using anonymous device identification (default: false) */
101
+ identifyAnonymousDevice?: boolean;
99
102
  };
100
103
 
101
104
  const DEFAULT_OPTIONS: Required<ForceUpdateClientOptions> = {
102
105
  throttleMs: 30_000, // 30 seconds
103
106
  checkCooldownMs: 300_000, // 5 minutes
107
+ checkOnForeground: true,
108
+ identifyAnonymousDevice: false,
104
109
  };
105
110
 
106
111
  export const VERSION_STATUS_STORAGE_KEY = "version_status";
@@ -149,7 +154,9 @@ export class ForceUpdateClient {
149
154
  const currentState = this.identity.getIdentifyState();
150
155
  this.logger.debug(`Current identity state during init: ${currentState.type}`);
151
156
  if (currentState.type === "identified") {
152
- this.logger.debug(`Identity already identified, syncing version status from: ${currentState.version_info.status}`);
157
+ this.logger.debug(
158
+ `Identity already identified, syncing version status from: ${currentState.version_info.status}`
159
+ );
153
160
  this.updateFromVersionStatus(currentState.version_info.status);
154
161
  } else {
155
162
  this.logger.debug(`Identity not yet identified (${currentState.type}), waiting for identify event`);
@@ -242,19 +249,20 @@ export class ForceUpdateClient {
242
249
 
243
250
  // If throttleMs is -1, disable throttling (always pass)
244
251
  // Otherwise, check if enough time has passed since last foreground
245
- const throttleOk = this.options.throttleMs === -1
246
- || !this.lastForegroundTime
247
- || now - this.lastForegroundTime >= this.options.throttleMs;
252
+ const throttleOk =
253
+ this.options.throttleMs === -1 ||
254
+ !this.lastForegroundTime ||
255
+ now - this.lastForegroundTime >= this.options.throttleMs;
248
256
 
249
257
  // If checkCooldownMs is 0, always allow check (no cooldown)
250
258
  // Otherwise, check if enough time has passed since last successful check
251
- const cooldownOk = this.options.checkCooldownMs === 0
252
- || !this.lastCheckTime
253
- || now - this.lastCheckTime >= this.options.checkCooldownMs;
254
-
255
- this.lastForegroundTime = now;
259
+ const cooldownOk =
260
+ this.options.checkCooldownMs === 0 ||
261
+ !this.lastCheckTime ||
262
+ now - this.lastCheckTime >= this.options.checkCooldownMs;
256
263
 
257
- if (throttleOk && cooldownOk) {
264
+ if (this.options.checkOnForeground || (throttleOk && cooldownOk)) {
265
+ this.lastForegroundTime = now;
258
266
  this.checkVersionOnForeground();
259
267
  }
260
268
  }
@@ -304,5 +312,4 @@ export class ForceUpdateClient {
304
312
  }
305
313
  this.emitter.removeAllListeners("VERSION_STATUS_CHANGED");
306
314
  }
307
-
308
315
  }
@@ -3,7 +3,7 @@ import { describe, expect, mock, test, beforeEach } from "bun:test";
3
3
  // Mock react-native before any imports that use it
4
4
  mock.module("react-native", () => ({
5
5
  AppState: {
6
- addEventListener: () => ({ remove: () => { } }),
6
+ addEventListener: () => ({ remove: () => {} }),
7
7
  },
8
8
  }));
9
9
 
@@ -27,7 +27,9 @@ function createMockLoggingClient() {
27
27
  debug: (message: string, ...args: unknown[]) => logs.push({ level: "debug", message, args }),
28
28
  }),
29
29
  getLogs: () => logs,
30
- clearLogs: () => { logs.length = 0; },
30
+ clearLogs: () => {
31
+ logs.length = 0;
32
+ },
31
33
  };
32
34
  }
33
35
 
@@ -48,19 +50,23 @@ function createMockUtilsClient() {
48
50
  let uuidCounter = 0;
49
51
  return {
50
52
  generateRandomUUID: async () => `mock-uuid-${++uuidCounter}`,
51
- resetCounter: () => { uuidCounter = 0; },
53
+ resetCounter: () => {
54
+ uuidCounter = 0;
55
+ },
52
56
  };
53
57
  }
54
58
 
55
- function createMockDeviceClient(overrides: Partial<{
56
- deviceId: string;
57
- timestamp: string;
58
- application: { name: string; version: string; build: string; bundle_id: string };
59
- hardware: { brand: string; model: string; device_type: string };
60
- os: { name: string; version: string };
61
- notifications: { push_token: string | null; platform: string | null };
62
- update: null;
63
- }> = {}) {
59
+ function createMockDeviceClient(
60
+ overrides: Partial<{
61
+ deviceId: string;
62
+ timestamp: string;
63
+ application: { name: string; version: string; build: string; bundle_id: string };
64
+ hardware: { brand: string; model: string; device_type: string };
65
+ os: { name: string; version: string };
66
+ notifications: { push_token: string | null; platform: string | null };
67
+ update: null;
68
+ }> = {}
69
+ ) {
64
70
  const defaultDeviceInfo = {
65
71
  timestamp: new Date().toISOString(),
66
72
  application: { name: "TestApp", version: "1.0.0", build: "100", bundle_id: "com.test.app" },
@@ -88,17 +94,19 @@ type ApiCallRecord = {
88
94
  };
89
95
  };
90
96
 
91
- function createMockApiClient(options: {
92
- success?: boolean;
93
- versionStatus?: (typeof IdentifyVersionStatusEnum)[keyof typeof IdentifyVersionStatusEnum];
94
- errorStatus?: number;
95
- errorMessage?: string;
96
- sessionId?: string;
97
- deviceId?: string;
98
- user_id?: string;
99
- token?: string;
100
- throwError?: Error;
101
- } = {}) {
97
+ function createMockApiClient(
98
+ options: {
99
+ success?: boolean;
100
+ versionStatus?: (typeof IdentifyVersionStatusEnum)[keyof typeof IdentifyVersionStatusEnum];
101
+ errorStatus?: number;
102
+ errorMessage?: string;
103
+ sessionId?: string;
104
+ deviceId?: string;
105
+ user_id?: string;
106
+ token?: string;
107
+ throwError?: Error;
108
+ } = {}
109
+ ) {
102
110
  const {
103
111
  success = true,
104
112
  versionStatus = IdentifyVersionStatusEnum.UP_TO_DATE,
@@ -152,18 +160,22 @@ function createMockApiClient(options: {
152
160
  },
153
161
  getCalls: () => calls,
154
162
  getLastCall: () => calls[calls.length - 1],
155
- clearCalls: () => { calls.length = 0; },
163
+ clearCalls: () => {
164
+ calls.length = 0;
165
+ },
156
166
  };
157
167
  }
158
168
 
159
169
  // Helper to create a standard client instance
160
- function createTestClient(overrides: {
161
- logging?: ReturnType<typeof createMockLoggingClient>;
162
- storage?: ReturnType<typeof createMockStorageClient>;
163
- utils?: ReturnType<typeof createMockUtilsClient>;
164
- api?: ReturnType<typeof createMockApiClient>;
165
- device?: ReturnType<typeof createMockDeviceClient>;
166
- } = {}) {
170
+ function createTestClient(
171
+ overrides: {
172
+ logging?: ReturnType<typeof createMockLoggingClient>;
173
+ storage?: ReturnType<typeof createMockStorageClient>;
174
+ utils?: ReturnType<typeof createMockUtilsClient>;
175
+ api?: ReturnType<typeof createMockApiClient>;
176
+ device?: ReturnType<typeof createMockDeviceClient>;
177
+ } = {}
178
+ ) {
167
179
  const mockLogging = overrides.logging ?? createMockLoggingClient();
168
180
  const mockStorage = overrides.storage ?? createMockStorageClient();
169
181
  const mockUtils = overrides.utils ?? createMockUtilsClient();
@@ -550,7 +562,17 @@ describe("IdentityClient", () => {
550
562
  await client.identify();
551
563
 
552
564
  const lastCall = mockApi.getLastCall();
553
- const device = (lastCall.config.body as { device?: { timestamp: string; application: { name: string; version: string; build: string; bundle_id: string }; os: { name: string; version: string }; hardware: { brand: string; model: string; device_type: string }; update: null } }).device;
565
+ const device = (
566
+ lastCall.config.body as {
567
+ device?: {
568
+ timestamp: string;
569
+ application: { name: string; version: string; build: string; bundle_id: string };
570
+ os: { name: string; version: string };
571
+ hardware: { brand: string; model: string; device_type: string };
572
+ update: null;
573
+ };
574
+ }
575
+ ).device;
554
576
 
555
577
  expect(device?.timestamp).toBe("2024-01-15T10:30:00.000Z");
556
578
  expect(device?.application.name).toBe("MyApp");
@@ -603,12 +625,12 @@ describe("IdentityClient", () => {
603
625
  // Second identify
604
626
  await client.identify();
605
627
 
606
- // Should have debug log about state already being 'identifying' (first setIdentifyState call)
607
- // Then transitions to identified
628
+ // Should have debug logs about state transitions
629
+ // When already identified, identify() will transition: identified -> identifying -> identified
608
630
  const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug");
609
- // The "identifying" to "identifying" won't happen since we start from "identified"
610
- // But we can check that state changes are logged properly
611
- expect(mockLogging.getLogs().some(l => l.level === "debug")).toBe(true);
631
+ expect(debugLogs.length).toBeGreaterThan(0);
632
+ // Check that state transitions are logged
633
+ expect(debugLogs.some((l) => l.message.includes("Identify state:"))).toBe(true);
612
634
  });
613
635
  });
614
636
 
@@ -992,7 +1014,9 @@ describe("IdentityClient", () => {
992
1014
 
993
1015
  // Make API slow so we can check intermediate state
994
1016
  let resolveApi: (value: unknown) => void;
995
- const apiPromise = new Promise((resolve) => { resolveApi = resolve; });
1017
+ const apiPromise = new Promise((resolve) => {
1018
+ resolveApi = resolve;
1019
+ });
996
1020
 
997
1021
  mockApi.client = async () => {
998
1022
  await apiPromise;
@@ -1015,7 +1039,7 @@ describe("IdentityClient", () => {
1015
1039
  const identifyPromise = client.identify();
1016
1040
 
1017
1041
  // Check storage while API call is pending
1018
- await new Promise(resolve => setTimeout(resolve, 10));
1042
+ await new Promise((resolve) => setTimeout(resolve, 10));
1019
1043
  const intermediateStored = mockStorage.getStorage().get(IDENTIFY_STORAGE_KEY);
1020
1044
  expect(JSON.parse(intermediateStored!).type).toBe("identifying");
1021
1045
 
@@ -1048,14 +1072,10 @@ describe("IdentityClient", () => {
1048
1072
  test("handles rapid successive identify calls", async () => {
1049
1073
  const { client } = createTestClient();
1050
1074
 
1051
- const results = await Promise.all([
1052
- client.identify(),
1053
- client.identify(),
1054
- client.identify(),
1055
- ]);
1075
+ const results = await Promise.all([client.identify(), client.identify(), client.identify()]);
1056
1076
 
1057
1077
  // All should succeed
1058
- results.forEach(result => {
1078
+ results.forEach((result) => {
1059
1079
  expect(result.success).toBe(true);
1060
1080
  });
1061
1081
 
@@ -1116,4 +1136,4 @@ describe("IdentityClient", () => {
1116
1136
  expect(parsed.session.session_id).toBe("session-with-émojis-🚀-and-üñíçödé");
1117
1137
  });
1118
1138
  });
1119
- });
1139
+ });
@@ -8,13 +8,13 @@ import type { Logger, LoggingClient } from "../logging";
8
8
  import type { StorageClient, SupportedStorage } from "../storage";
9
9
  import type { UtilsClient } from "../utils";
10
10
 
11
- export type Persona = {
11
+ export interface Persona {
12
12
  name?: string | undefined;
13
13
  user_id?: string | undefined;
14
14
  email?: string | undefined;
15
- };
15
+ }
16
16
 
17
- export type IdentityUser = {
17
+ export interface IdentityUser {
18
18
  session_id: string;
19
19
  device_id: string;
20
20
  user_id: string;
@@ -39,7 +39,7 @@ export const UpdateVersionStatusBodySchema = z.object({
39
39
  export const SessionSchema = z.object({
40
40
  session_id: z.string(),
41
41
  device_id: z.string(),
42
- user_id: z.string(),
42
+ user_id: z.string().or(z.object({ persona_id: z.string() }).transform((val) => val.persona_id)),
43
43
  token: z.string(),
44
44
  });
45
45
  export type Session = z.infer<typeof SessionSchema>;
@@ -69,9 +69,9 @@ export type UnidentifiedSessionState = z.infer<typeof UnidentifiedSessionStateSc
69
69
  export type IdentifyingSessionState = z.infer<typeof IdentifyingSessionStateSchema>;
70
70
  export type IdentifiedSessionState = z.infer<typeof IdentifiedSessionStateSchema>;
71
71
 
72
- export type IdentifyStateChangeEvents = {
72
+ export interface IdentifyStateChangeEvents {
73
73
  IDENTIFY_STATE_CHANGED: (state: IdentifyState) => void;
74
- };
74
+ }
75
75
 
76
76
  export const IDENTIFY_STORAGE_KEY = "IDENTIFY_STATE";
77
77
 
@@ -8,9 +8,9 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
8
8
  verbose: 4,
9
9
  };
10
10
 
11
- export type LoggingClientOptions = {
11
+ export interface LoggingClientOptions {
12
12
  logLevel?: LogLevel;
13
- };
13
+ }
14
14
 
15
15
  export class LoggingClient {
16
16
  private logLevel: LogLevel;
@@ -42,12 +42,12 @@ export class LoggingClient {
42
42
  /**
43
43
  * Configuration options for logger creation
44
44
  */
45
- export type LoggerOptions = {
45
+ export interface LoggerOptions {
46
46
  /** Logger name used in log prefixes */
47
47
  name: string;
48
48
  /** Reference to parent LoggingClient for log level checks */
49
49
  loggingClient: LoggingClient;
50
- };
50
+ }
51
51
 
52
52
  export class Logger {
53
53
  /** Bound console methods to preserve call site */
@@ -1,80 +1,72 @@
1
1
  import type { Logger, LoggingClient } from "../logging";
2
2
  import type { StorageAdapter, SupportedStorage } from "./adapters/storage.adpater-interface";
3
3
 
4
-
5
4
  export class StorageClient {
6
-
7
- private readonly logger: Logger;
8
-
9
- private readonly storage: Map<string, SupportedStorage> = new Map();
10
-
11
- private readonly preloadPromises: Promise<void>[] = [];
12
-
13
- private _isReady = false;
14
-
15
- get isReady(): boolean {
16
- return this._isReady;
17
- }
18
-
19
- constructor(
20
- logging: LoggingClient,
21
- private readonly orgId: string,
22
- private readonly projectId: string,
23
- private readonly storageAdapter: StorageAdapter) {
24
- this.logger = logging.createLogger({
25
- name: "StorageClient",
26
- });
27
- }
28
-
29
- private createStorageKey(storageKey: string): string {
30
- return `teardown:v1:${this.orgId}:${this.projectId}:${storageKey}`;
31
- }
32
-
33
- createStorage(storageKey: string): SupportedStorage {
34
- const fullStorageKey = this.createStorageKey(storageKey);
35
-
36
- this.logger.debug(`Creating storage for ${fullStorageKey}`);
37
- if (this.storage.has(fullStorageKey)) {
38
- this.logger.debug(`Storage already exists for ${fullStorageKey}`);
39
- const existingStorage = this.storage.get(fullStorageKey);
40
-
41
- if (existingStorage != null) {
42
- this.logger.debug(`Returning existing storage for ${fullStorageKey}`);
43
- return existingStorage;
44
- }
45
-
46
- this.logger.error(`Existing storage was found for ${fullStorageKey}, but it was null`);
47
- }
48
-
49
- this.logger.debug(`Creating new storage for ${fullStorageKey}`);
50
- const newStorage = this.storageAdapter.createStorage(fullStorageKey);
51
- const preloadResult = newStorage.preload();
52
- if (preloadResult instanceof Promise) {
53
- this.preloadPromises.push(preloadResult);
54
- }
55
-
56
- const remappedStorage = {
57
- ...newStorage,
58
- clear: () => {
59
- this.storage.delete(fullStorageKey);
60
- },
61
- }
62
-
63
- this.storage.set(fullStorageKey, remappedStorage);
64
- this.logger.debug(`Storage created for ${fullStorageKey}`);
65
-
66
- return remappedStorage;
67
- }
68
-
69
- async whenReady(): Promise<void> {
70
- await Promise.all(this.preloadPromises);
71
- this._isReady = true;
72
- }
73
-
74
- shutdown(): void {
75
- this.storage.forEach((storage) => {
76
- storage.clear();
77
- });
78
- this.storage.clear();
79
- }
80
- }
5
+ private readonly logger: Logger;
6
+
7
+ private readonly storage: Map<string, SupportedStorage> = new Map();
8
+
9
+ private readonly preloadPromises: Promise<void>[] = [];
10
+
11
+ private _isReady = false;
12
+
13
+ get isReady(): boolean {
14
+ return this._isReady;
15
+ }
16
+
17
+ constructor(
18
+ logging: LoggingClient,
19
+ private readonly orgId: string,
20
+ private readonly projectId: string,
21
+ private readonly storageAdapter: StorageAdapter
22
+ ) {
23
+ this.logger = logging.createLogger({
24
+ name: "StorageClient",
25
+ });
26
+ }
27
+
28
+ private createStorageKey(storageKey: string): string {
29
+ return `teardown:v1:${this.orgId}:${this.projectId}:${storageKey}`;
30
+ }
31
+
32
+ createStorage(storageKey: string): SupportedStorage {
33
+ const fullStorageKey = this.createStorageKey(storageKey);
34
+
35
+ this.logger.debug(`Creating storage for ${fullStorageKey}`);
36
+ if (this.storage.has(fullStorageKey)) {
37
+ this.logger.debug(`Storage already exists for ${fullStorageKey}`);
38
+ const existingStorage = this.storage.get(fullStorageKey);
39
+
40
+ if (existingStorage != null) {
41
+ this.logger.debug(`Returning existing storage for ${fullStorageKey}`);
42
+ return existingStorage;
43
+ }
44
+
45
+ this.logger.error(`Existing storage was found for ${fullStorageKey}, but it was null`);
46
+ }
47
+
48
+ this.logger.debug(`Creating new storage for ${fullStorageKey}`);
49
+ const newStorage = this.storageAdapter.createStorage(fullStorageKey);
50
+ const preloadResult = newStorage.preload();
51
+ if (preloadResult instanceof Promise) {
52
+ this.preloadPromises.push(preloadResult);
53
+ }
54
+
55
+ const remappedStorage = {
56
+ ...newStorage,
57
+ clear: () => {
58
+ this.storage.delete(fullStorageKey);
59
+ },
60
+ };
61
+
62
+ this.storage.set(fullStorageKey, remappedStorage);
63
+ this.logger.debug(`Storage created for ${fullStorageKey}`);
64
+
65
+ return remappedStorage;
66
+ }
67
+
68
+ async whenReady(): Promise<void> {
69
+ await Promise.all(this.preloadPromises);
70
+ this._isReady = true;
71
+ }
72
+ }
@@ -2,9 +2,9 @@ import { createContext, useContext } from "react";
2
2
 
3
3
  import type { TeardownCore } from "../teardown.core";
4
4
 
5
- export type TeardownContextType = {
5
+ export interface TeardownContextType {
6
6
  core: TeardownCore;
7
- };
7
+ }
8
8
 
9
9
  export const TeardownContext = createContext<TeardownContextType | null>(null);
10
10
 
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
2
2
  import type { VersionStatus } from "../clients/force-update";
3
3
  import { useTeardown } from "../contexts/teardown.context";
4
4
 
5
- export type UseForceUpdateResult = {
5
+ export interface UseForceUpdateResult {
6
6
  /**
7
7
  * The current version status.
8
8
  */
@@ -19,7 +19,7 @@ export type UseForceUpdateResult = {
19
19
  * Whether the the current version is out of date and is forced to be updated.
20
20
  */
21
21
  isUpdateRequired: boolean;
22
- };
22
+ }
23
23
 
24
24
  export const useForceUpdate = (): UseForceUpdateResult => {
25
25
  const { core } = useTeardown();
@@ -2,28 +2,26 @@ import { useEffect, useState } from "react";
2
2
  import type { Session } from "../clients/identity/identity.client";
3
3
  import { useTeardown } from "../contexts/teardown.context";
4
4
 
5
- export type UseSessionResult = Session | null
5
+ export type UseSessionResult = Session | null;
6
6
 
7
7
  export const useSession = (): UseSessionResult => {
8
- const { core } = useTeardown();
8
+ const { core } = useTeardown();
9
9
 
10
- const [session, setSession] = useState<Session | null>(
11
- core.identity.getSessionState()
12
- );
10
+ const [session, setSession] = useState<Session | null>(core.identity.getSessionState());
13
11
 
14
- useEffect(() => {
15
- const unsubscribe = core.identity.onIdentifyStateChange((state) => {
16
- switch (state.type) {
17
- case "identified":
18
- setSession(state.session);
19
- break;
20
- case "unidentified":
21
- setSession(null);
22
- break;
23
- }
24
- });
25
- return unsubscribe;
26
- }, [core.identity]);
12
+ useEffect(() => {
13
+ const unsubscribe = core.identity.onIdentifyStateChange((state) => {
14
+ switch (state.type) {
15
+ case "identified":
16
+ setSession(state.session);
17
+ break;
18
+ case "unidentified":
19
+ setSession(null);
20
+ break;
21
+ }
22
+ });
23
+ return unsubscribe;
24
+ }, [core.identity]);
27
25
 
28
- return session;
29
- };
26
+ return session;
27
+ };
@@ -85,6 +85,5 @@ export class TeardownCore {
85
85
 
86
86
  shutdown(): void {
87
87
  this.logger.debug("Shutting down TeardownCore");
88
- this.storage.shutdown();
89
88
  }
90
89
  }