@teardown/react-native 2.0.22 → 2.0.24
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 +1 -1
- package/src/clients/api/api.client.ts +0 -1
- package/src/clients/device/device.client.ts +2 -2
- package/src/clients/force-update/force-update.client.test.ts +135 -105
- package/src/clients/force-update/force-update.client.ts +53 -63
- package/src/clients/identity/identity.client.test.ts +79 -51
- package/src/clients/identity/identity.client.ts +5 -5
- package/src/clients/logging/logging.client.ts +4 -4
- package/src/contexts/teardown.context.ts +2 -2
- package/src/hooks/use-force-update.ts +2 -2
- package/src/hooks/use-session.ts +18 -20
package/package.json
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
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: {
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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);
|
|
@@ -281,7 +259,7 @@ describe("ForceUpdateClient", () => {
|
|
|
281
259
|
});
|
|
282
260
|
});
|
|
283
261
|
|
|
284
|
-
describe("
|
|
262
|
+
describe("checkIntervalMs", () => {
|
|
285
263
|
test("skips version check when identify returns null", async () => {
|
|
286
264
|
const mockIdentity = createMockIdentityClient();
|
|
287
265
|
const mockLogging = createMockLoggingClient();
|
|
@@ -289,12 +267,9 @@ describe("ForceUpdateClient", () => {
|
|
|
289
267
|
|
|
290
268
|
mockIdentity.setNextIdentifyResult(null);
|
|
291
269
|
|
|
292
|
-
const client = new ForceUpdateClient(
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
checkIntervalMs: 0,
|
|
272
|
+
});
|
|
298
273
|
client.initialize();
|
|
299
274
|
|
|
300
275
|
const statusChanges: VersionStatus[] = [];
|
|
@@ -311,17 +286,14 @@ describe("ForceUpdateClient", () => {
|
|
|
311
286
|
client.shutdown();
|
|
312
287
|
});
|
|
313
288
|
|
|
314
|
-
test("
|
|
289
|
+
test("checkIntervalMs -1 disables version checking entirely", async () => {
|
|
315
290
|
const mockIdentity = createMockIdentityClient();
|
|
316
291
|
const mockLogging = createMockLoggingClient();
|
|
317
292
|
const mockStorage = createMockStorageClient();
|
|
318
293
|
|
|
319
|
-
const client = new ForceUpdateClient(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mockIdentity as never,
|
|
323
|
-
{ throttleMs: 0, checkCooldownMs: -1 }
|
|
324
|
-
);
|
|
294
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
295
|
+
checkIntervalMs: -1,
|
|
296
|
+
});
|
|
325
297
|
client.initialize();
|
|
326
298
|
|
|
327
299
|
const foregroundHandler = mockAppStateListeners[0];
|
|
@@ -336,17 +308,47 @@ describe("ForceUpdateClient", () => {
|
|
|
336
308
|
client.shutdown();
|
|
337
309
|
});
|
|
338
310
|
|
|
339
|
-
test("
|
|
311
|
+
test("interval prevents checks too soon after successful check", async () => {
|
|
340
312
|
const mockIdentity = createMockIdentityClient();
|
|
341
313
|
const mockLogging = createMockLoggingClient();
|
|
342
314
|
const mockStorage = createMockStorageClient();
|
|
343
315
|
|
|
344
|
-
const client = new ForceUpdateClient(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
316
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
317
|
+
checkOnForeground: true,
|
|
318
|
+
checkIntervalMs: 100_000,
|
|
319
|
+
});
|
|
320
|
+
client.initialize();
|
|
321
|
+
|
|
322
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
323
|
+
|
|
324
|
+
// First foreground - should trigger check
|
|
325
|
+
await foregroundHandler("active");
|
|
326
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
327
|
+
|
|
328
|
+
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
329
|
+
|
|
330
|
+
// Second foreground immediately (within interval window)
|
|
331
|
+
await foregroundHandler("active");
|
|
332
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
333
|
+
|
|
334
|
+
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
335
|
+
|
|
336
|
+
// Only first call should have triggered identify (interval blocks second)
|
|
337
|
+
expect(callsAfterFirst).toBe(1);
|
|
338
|
+
expect(callsAfterSecond).toBe(1);
|
|
339
|
+
|
|
340
|
+
client.shutdown();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("checkOnForeground: true respects interval", async () => {
|
|
344
|
+
const mockIdentity = createMockIdentityClient();
|
|
345
|
+
const mockLogging = createMockLoggingClient();
|
|
346
|
+
const mockStorage = createMockStorageClient();
|
|
347
|
+
|
|
348
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
349
|
+
checkOnForeground: true,
|
|
350
|
+
checkIntervalMs: 100_000,
|
|
351
|
+
});
|
|
350
352
|
client.initialize();
|
|
351
353
|
|
|
352
354
|
const foregroundHandler = mockAppStateListeners[0];
|
|
@@ -357,30 +359,78 @@ describe("ForceUpdateClient", () => {
|
|
|
357
359
|
|
|
358
360
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
359
361
|
|
|
360
|
-
// Second foreground immediately (within
|
|
362
|
+
// Second foreground immediately (within interval window)
|
|
361
363
|
await foregroundHandler("active");
|
|
362
364
|
await new Promise((r) => setTimeout(r, 10));
|
|
363
365
|
|
|
364
366
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
365
367
|
|
|
366
|
-
//
|
|
368
|
+
// Only first should trigger - interval blocks second even with checkOnForeground: true
|
|
367
369
|
expect(callsAfterFirst).toBe(1);
|
|
368
370
|
expect(callsAfterSecond).toBe(1);
|
|
369
371
|
|
|
370
372
|
client.shutdown();
|
|
371
373
|
});
|
|
372
374
|
|
|
373
|
-
test("
|
|
375
|
+
test("checkOnForeground: false disables foreground checking entirely", async () => {
|
|
376
|
+
const mockIdentity = createMockIdentityClient();
|
|
377
|
+
const mockLogging = createMockLoggingClient();
|
|
378
|
+
const mockStorage = createMockStorageClient();
|
|
379
|
+
|
|
380
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
381
|
+
checkOnForeground: false,
|
|
382
|
+
checkIntervalMs: 0, // Would normally allow every check
|
|
383
|
+
});
|
|
384
|
+
client.initialize();
|
|
385
|
+
|
|
386
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
387
|
+
|
|
388
|
+
// Foreground should not trigger check when checkOnForeground is false
|
|
389
|
+
await foregroundHandler("active");
|
|
390
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
391
|
+
|
|
392
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(0);
|
|
393
|
+
|
|
394
|
+
client.shutdown();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("checkIntervalMs 0 checks on every foreground", async () => {
|
|
374
398
|
const mockIdentity = createMockIdentityClient();
|
|
375
399
|
const mockLogging = createMockLoggingClient();
|
|
376
400
|
const mockStorage = createMockStorageClient();
|
|
377
401
|
|
|
378
|
-
const client = new ForceUpdateClient(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
402
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
403
|
+
checkOnForeground: true,
|
|
404
|
+
checkIntervalMs: 0,
|
|
405
|
+
});
|
|
406
|
+
client.initialize();
|
|
407
|
+
|
|
408
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
409
|
+
|
|
410
|
+
// First foreground
|
|
411
|
+
await foregroundHandler("active");
|
|
412
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
413
|
+
|
|
414
|
+
// Second foreground immediately
|
|
415
|
+
await foregroundHandler("active");
|
|
416
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
417
|
+
|
|
418
|
+
// Both should trigger since interval is 0
|
|
419
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(2);
|
|
420
|
+
|
|
421
|
+
client.shutdown();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("values below 30s are clamped to 30s minimum", async () => {
|
|
425
|
+
const mockIdentity = createMockIdentityClient();
|
|
426
|
+
const mockLogging = createMockLoggingClient();
|
|
427
|
+
const mockStorage = createMockStorageClient();
|
|
428
|
+
|
|
429
|
+
// Set interval to 10ms (below 30s minimum)
|
|
430
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
431
|
+
checkOnForeground: true,
|
|
432
|
+
checkIntervalMs: 10,
|
|
433
|
+
});
|
|
384
434
|
client.initialize();
|
|
385
435
|
|
|
386
436
|
const foregroundHandler = mockAppStateListeners[0];
|
|
@@ -391,13 +441,13 @@ describe("ForceUpdateClient", () => {
|
|
|
391
441
|
|
|
392
442
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
393
443
|
|
|
394
|
-
// Second foreground
|
|
444
|
+
// Second foreground immediately - should be blocked by 30s minimum
|
|
395
445
|
await foregroundHandler("active");
|
|
396
446
|
await new Promise((r) => setTimeout(r, 10));
|
|
397
447
|
|
|
398
448
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
399
449
|
|
|
400
|
-
// Only first call should have triggered
|
|
450
|
+
// Only first call should have triggered (30s minimum enforced)
|
|
401
451
|
expect(callsAfterFirst).toBe(1);
|
|
402
452
|
expect(callsAfterSecond).toBe(1);
|
|
403
453
|
|
|
@@ -414,11 +464,7 @@ describe("ForceUpdateClient", () => {
|
|
|
414
464
|
// Pre-populate storage with stale "checking" state
|
|
415
465
|
mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "checking" }));
|
|
416
466
|
|
|
417
|
-
const client = new ForceUpdateClient(
|
|
418
|
-
mockLogging as never,
|
|
419
|
-
mockStorage as never,
|
|
420
|
-
mockIdentity as never
|
|
421
|
-
);
|
|
467
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
|
|
422
468
|
client.initialize();
|
|
423
469
|
|
|
424
470
|
// Should reset to initializing, not stay in checking
|
|
@@ -435,11 +481,7 @@ describe("ForceUpdateClient", () => {
|
|
|
435
481
|
// Pre-populate storage with stale "initializing" state
|
|
436
482
|
mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "initializing" }));
|
|
437
483
|
|
|
438
|
-
const client = new ForceUpdateClient(
|
|
439
|
-
mockLogging as never,
|
|
440
|
-
mockStorage as never,
|
|
441
|
-
mockIdentity as never
|
|
442
|
-
);
|
|
484
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
|
|
443
485
|
client.initialize();
|
|
444
486
|
|
|
445
487
|
// Should reset to initializing (which it already is, but storage should be cleared)
|
|
@@ -456,11 +498,7 @@ describe("ForceUpdateClient", () => {
|
|
|
456
498
|
// Pre-populate storage with valid state
|
|
457
499
|
mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "up_to_date" }));
|
|
458
500
|
|
|
459
|
-
const client = new ForceUpdateClient(
|
|
460
|
-
mockLogging as never,
|
|
461
|
-
mockStorage as never,
|
|
462
|
-
mockIdentity as never
|
|
463
|
-
);
|
|
501
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
|
|
464
502
|
client.initialize();
|
|
465
503
|
|
|
466
504
|
// Should keep up_to_date from storage
|
|
@@ -477,11 +515,7 @@ describe("ForceUpdateClient", () => {
|
|
|
477
515
|
// Pre-populate storage with valid state
|
|
478
516
|
mockStorage.getStorage().set(VERSION_STATUS_STORAGE_KEY, JSON.stringify({ type: "update_required" }));
|
|
479
517
|
|
|
480
|
-
const client = new ForceUpdateClient(
|
|
481
|
-
mockLogging as never,
|
|
482
|
-
mockStorage as never,
|
|
483
|
-
mockIdentity as never
|
|
484
|
-
);
|
|
518
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
|
|
485
519
|
client.initialize();
|
|
486
520
|
|
|
487
521
|
// Should keep update_required from storage
|
|
@@ -503,11 +537,7 @@ describe("ForceUpdateClient", () => {
|
|
|
503
537
|
const mockLogging = createMockLoggingClient();
|
|
504
538
|
const mockStorage = createMockStorageClient();
|
|
505
539
|
|
|
506
|
-
const client = new ForceUpdateClient(
|
|
507
|
-
mockLogging as never,
|
|
508
|
-
mockStorage as never,
|
|
509
|
-
mockIdentity as never
|
|
510
|
-
);
|
|
540
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never);
|
|
511
541
|
client.initialize();
|
|
512
542
|
|
|
513
543
|
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,38 @@ 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
|
|
66
|
+
export interface VersionStatusChangeEvents {
|
|
68
67
|
VERSION_STATUS_CHANGED: (status: VersionStatus) => void;
|
|
69
|
-
}
|
|
68
|
+
}
|
|
70
69
|
|
|
71
70
|
export type ForceUpdateClientOptions = {
|
|
72
|
-
/**
|
|
73
|
-
* Minimum time (ms) between
|
|
74
|
-
* Measured from the last time the app came to foreground.
|
|
75
|
-
* Prevents checking when user quickly switches apps back and forth.
|
|
76
|
-
* Default: 30000 (30 seconds)
|
|
77
|
-
*
|
|
78
|
-
* Special values:
|
|
79
|
-
* - -1: Disable throttling, check on every foreground (respects checkCooldownMs)
|
|
80
|
-
*
|
|
81
|
-
* Example: If throttleMs is 30s and user backgrounds then foregrounds the app
|
|
82
|
-
* twice within 20s, only the first transition triggers a check.
|
|
83
|
-
*/
|
|
84
|
-
throttleMs?: number;
|
|
85
|
-
/**
|
|
86
|
-
* Minimum time (ms) since the last successful version check before checking again.
|
|
87
|
-
* Measured from when the last check completed successfully (not when it started).
|
|
88
|
-
* Prevents unnecessary API calls after we already have fresh version data.
|
|
71
|
+
/**
|
|
72
|
+
* Minimum time (ms) between version checks.
|
|
89
73
|
* Default: 300000 (5 minutes)
|
|
90
|
-
*
|
|
74
|
+
*
|
|
75
|
+
* Values below 30 seconds are clamped to 30 seconds to prevent excessive API calls.
|
|
76
|
+
*
|
|
91
77
|
* Special values:
|
|
92
|
-
* - 0:
|
|
93
|
-
* - -1: Disable
|
|
94
|
-
*
|
|
95
|
-
* Example: If
|
|
78
|
+
* - 0: Check on every foreground (no interval)
|
|
79
|
+
* - -1: Disable automatic version checking entirely
|
|
80
|
+
*
|
|
81
|
+
* Example: If checkIntervalMs is 5min and a check completes at 12:00pm,
|
|
96
82
|
* no new checks occur until 12:05pm, even if user foregrounds the app multiple times.
|
|
97
83
|
*/
|
|
98
|
-
|
|
84
|
+
checkIntervalMs?: number;
|
|
85
|
+
/** Check version when app comes to foreground, respecting checkIntervalMs (default: true) */
|
|
86
|
+
checkOnForeground?: boolean;
|
|
87
|
+
/** If true, check version even when not identified by using anonymous device identification (default: false) */
|
|
88
|
+
identifyAnonymousDevice?: boolean;
|
|
99
89
|
};
|
|
100
90
|
|
|
91
|
+
/** Hard minimum interval between checks to prevent excessive API calls */
|
|
92
|
+
const MIN_CHECK_INTERVAL_MS = 30_000; // 30 seconds
|
|
93
|
+
|
|
101
94
|
const DEFAULT_OPTIONS: Required<ForceUpdateClientOptions> = {
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
checkIntervalMs: 300_000, // 5 minutes
|
|
96
|
+
checkOnForeground: true,
|
|
97
|
+
identifyAnonymousDevice: false,
|
|
104
98
|
};
|
|
105
99
|
|
|
106
100
|
export const VERSION_STATUS_STORAGE_KEY = "version_status";
|
|
@@ -111,7 +105,6 @@ export class ForceUpdateClient {
|
|
|
111
105
|
private unsubscribe: (() => void) | null = null;
|
|
112
106
|
private appStateSubscription: NativeEventSubscription | null = null;
|
|
113
107
|
private lastCheckTime: number | null = null;
|
|
114
|
-
private lastForegroundTime: number | null = null;
|
|
115
108
|
private initialized = false;
|
|
116
109
|
|
|
117
110
|
private readonly logger: Logger;
|
|
@@ -127,7 +120,6 @@ export class ForceUpdateClient {
|
|
|
127
120
|
this.logger = logging.createLogger({ name: "ForceUpdateClient" });
|
|
128
121
|
this.storage = storage.createStorage("version");
|
|
129
122
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
130
|
-
// Don't initialize here - defer to initialize()
|
|
131
123
|
}
|
|
132
124
|
|
|
133
125
|
initialize(): void {
|
|
@@ -137,7 +129,6 @@ export class ForceUpdateClient {
|
|
|
137
129
|
}
|
|
138
130
|
this.initialized = true;
|
|
139
131
|
|
|
140
|
-
// Load from storage, subscribe to events, and sync with current identity state
|
|
141
132
|
this.versionStatus = this.getVersionStatusFromStorage();
|
|
142
133
|
this.logger.debug(`Initialized with version status: ${this.versionStatus.type}`);
|
|
143
134
|
this.subscribeToIdentity();
|
|
@@ -149,7 +140,9 @@ export class ForceUpdateClient {
|
|
|
149
140
|
const currentState = this.identity.getIdentifyState();
|
|
150
141
|
this.logger.debug(`Current identity state during init: ${currentState.type}`);
|
|
151
142
|
if (currentState.type === "identified") {
|
|
152
|
-
this.logger.debug(
|
|
143
|
+
this.logger.debug(
|
|
144
|
+
`Identity already identified, syncing version status from: ${currentState.version_info.status}`
|
|
145
|
+
);
|
|
153
146
|
this.updateFromVersionStatus(currentState.version_info.status);
|
|
154
147
|
} else {
|
|
155
148
|
this.logger.debug(`Identity not yet identified (${currentState.type}), waiting for identify event`);
|
|
@@ -157,25 +150,30 @@ export class ForceUpdateClient {
|
|
|
157
150
|
}
|
|
158
151
|
|
|
159
152
|
private getVersionStatusFromStorage(): VersionStatus {
|
|
160
|
-
|
|
153
|
+
try {
|
|
154
|
+
const stored = this.storage.getItem(VERSION_STATUS_STORAGE_KEY);
|
|
161
155
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
if (stored == null) {
|
|
157
|
+
this.logger.debug("No stored version status, returning initializing");
|
|
158
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
159
|
+
}
|
|
166
160
|
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
const parsed = VersionStatusSchema.parse(JSON.parse(stored));
|
|
162
|
+
this.logger.debug(`Parsed version status from storage: ${parsed.type}`);
|
|
163
|
+
|
|
164
|
+
// "checking" and "initializing" are transient states - if we restore them, reset to initializing
|
|
165
|
+
// This can happen if the app was killed during a version check
|
|
166
|
+
if (parsed.type === "checking" || parsed.type === "initializing") {
|
|
167
|
+
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
168
|
+
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
169
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
170
|
+
}
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
174
|
-
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
172
|
+
return parsed;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.logger.debugError("Error getting version status from storage", { error });
|
|
175
175
|
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
176
176
|
}
|
|
177
|
-
|
|
178
|
-
return parsed;
|
|
179
177
|
}
|
|
180
178
|
|
|
181
179
|
private saveVersionStatusToStorage(status: VersionStatus): void {
|
|
@@ -232,29 +230,22 @@ export class ForceUpdateClient {
|
|
|
232
230
|
if (nextState === "active") {
|
|
233
231
|
this.logger.debug("App state changed to active");
|
|
234
232
|
|
|
235
|
-
// If
|
|
236
|
-
if (this.options.
|
|
237
|
-
this.logger.debug("Version checking disabled (
|
|
233
|
+
// If checkIntervalMs is -1, disable checking entirely
|
|
234
|
+
if (this.options.checkIntervalMs === -1) {
|
|
235
|
+
this.logger.debug("Version checking disabled (checkIntervalMs = -1)");
|
|
238
236
|
return;
|
|
239
237
|
}
|
|
240
238
|
|
|
241
239
|
const now = Date.now();
|
|
242
240
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|| !this.lastForegroundTime
|
|
247
|
-
|| now - this.lastForegroundTime >= this.options.throttleMs;
|
|
248
|
-
|
|
249
|
-
// If checkCooldownMs is 0, always allow check (no cooldown)
|
|
250
|
-
// 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;
|
|
241
|
+
// Calculate effective interval (clamp to minimum unless 0 for "always check")
|
|
242
|
+
const effectiveInterval =
|
|
243
|
+
this.options.checkIntervalMs === 0 ? 0 : Math.max(this.options.checkIntervalMs, MIN_CHECK_INTERVAL_MS);
|
|
254
244
|
|
|
255
|
-
|
|
245
|
+
// Check if enough time has passed since last successful check
|
|
246
|
+
const canCheck = effectiveInterval === 0 || !this.lastCheckTime || now - this.lastCheckTime >= effectiveInterval;
|
|
256
247
|
|
|
257
|
-
if (
|
|
248
|
+
if (this.options.checkOnForeground && canCheck) {
|
|
258
249
|
this.checkVersionOnForeground();
|
|
259
250
|
}
|
|
260
251
|
}
|
|
@@ -304,5 +295,4 @@ export class ForceUpdateClient {
|
|
|
304
295
|
}
|
|
305
296
|
this.emitter.removeAllListeners("VERSION_STATUS_CHANGED");
|
|
306
297
|
}
|
|
307
|
-
|
|
308
298
|
}
|
|
@@ -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
|
|
|
@@ -25,9 +25,13 @@ function createMockLoggingClient() {
|
|
|
25
25
|
warn: (message: string, ...args: unknown[]) => logs.push({ level: "warn", message, args }),
|
|
26
26
|
error: (message: string, ...args: unknown[]) => logs.push({ level: "error", message, args }),
|
|
27
27
|
debug: (message: string, ...args: unknown[]) => logs.push({ level: "debug", message, args }),
|
|
28
|
+
debugInfo: (message: string, ...args: unknown[]) => logs.push({ level: "debugInfo", message, args }),
|
|
29
|
+
debugError: (message: string, ...args: unknown[]) => logs.push({ level: "debugError", message, args }),
|
|
28
30
|
}),
|
|
29
31
|
getLogs: () => logs,
|
|
30
|
-
clearLogs: () => {
|
|
32
|
+
clearLogs: () => {
|
|
33
|
+
logs.length = 0;
|
|
34
|
+
},
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -48,19 +52,23 @@ function createMockUtilsClient() {
|
|
|
48
52
|
let uuidCounter = 0;
|
|
49
53
|
return {
|
|
50
54
|
generateRandomUUID: async () => `mock-uuid-${++uuidCounter}`,
|
|
51
|
-
resetCounter: () => {
|
|
55
|
+
resetCounter: () => {
|
|
56
|
+
uuidCounter = 0;
|
|
57
|
+
},
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
function createMockDeviceClient(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
function createMockDeviceClient(
|
|
62
|
+
overrides: Partial<{
|
|
63
|
+
deviceId: string;
|
|
64
|
+
timestamp: string;
|
|
65
|
+
application: { name: string; version: string; build: string; bundle_id: string };
|
|
66
|
+
hardware: { brand: string; model: string; device_type: string };
|
|
67
|
+
os: { name: string; version: string };
|
|
68
|
+
notifications: { push_token: string | null; platform: string | null };
|
|
69
|
+
update: null;
|
|
70
|
+
}> = {}
|
|
71
|
+
) {
|
|
64
72
|
const defaultDeviceInfo = {
|
|
65
73
|
timestamp: new Date().toISOString(),
|
|
66
74
|
application: { name: "TestApp", version: "1.0.0", build: "100", bundle_id: "com.test.app" },
|
|
@@ -88,17 +96,19 @@ type ApiCallRecord = {
|
|
|
88
96
|
};
|
|
89
97
|
};
|
|
90
98
|
|
|
91
|
-
function createMockApiClient(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
function createMockApiClient(
|
|
100
|
+
options: {
|
|
101
|
+
success?: boolean;
|
|
102
|
+
versionStatus?: (typeof IdentifyVersionStatusEnum)[keyof typeof IdentifyVersionStatusEnum];
|
|
103
|
+
errorStatus?: number;
|
|
104
|
+
errorMessage?: string;
|
|
105
|
+
sessionId?: string;
|
|
106
|
+
deviceId?: string;
|
|
107
|
+
user_id?: string;
|
|
108
|
+
token?: string;
|
|
109
|
+
throwError?: Error;
|
|
110
|
+
} = {}
|
|
111
|
+
) {
|
|
102
112
|
const {
|
|
103
113
|
success = true,
|
|
104
114
|
versionStatus = IdentifyVersionStatusEnum.UP_TO_DATE,
|
|
@@ -152,18 +162,22 @@ function createMockApiClient(options: {
|
|
|
152
162
|
},
|
|
153
163
|
getCalls: () => calls,
|
|
154
164
|
getLastCall: () => calls[calls.length - 1],
|
|
155
|
-
clearCalls: () => {
|
|
165
|
+
clearCalls: () => {
|
|
166
|
+
calls.length = 0;
|
|
167
|
+
},
|
|
156
168
|
};
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
// Helper to create a standard client instance
|
|
160
|
-
function createTestClient(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
function createTestClient(
|
|
173
|
+
overrides: {
|
|
174
|
+
logging?: ReturnType<typeof createMockLoggingClient>;
|
|
175
|
+
storage?: ReturnType<typeof createMockStorageClient>;
|
|
176
|
+
utils?: ReturnType<typeof createMockUtilsClient>;
|
|
177
|
+
api?: ReturnType<typeof createMockApiClient>;
|
|
178
|
+
device?: ReturnType<typeof createMockDeviceClient>;
|
|
179
|
+
} = {}
|
|
180
|
+
) {
|
|
167
181
|
const mockLogging = overrides.logging ?? createMockLoggingClient();
|
|
168
182
|
const mockStorage = overrides.storage ?? createMockStorageClient();
|
|
169
183
|
const mockUtils = overrides.utils ?? createMockUtilsClient();
|
|
@@ -244,20 +258,26 @@ describe("IdentityClient", () => {
|
|
|
244
258
|
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
245
259
|
});
|
|
246
260
|
|
|
247
|
-
test("
|
|
261
|
+
test("gracefully handles invalid stored state (corrupt JSON)", () => {
|
|
248
262
|
const mockStorage = createMockStorageClient();
|
|
249
263
|
mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, "not-valid-json{{{");
|
|
250
264
|
|
|
251
265
|
const { client } = createTestClient({ storage: mockStorage });
|
|
252
|
-
|
|
266
|
+
loadStateFromStorage(client);
|
|
267
|
+
|
|
268
|
+
// Should gracefully fall back to unidentified state
|
|
269
|
+
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
253
270
|
});
|
|
254
271
|
|
|
255
|
-
test("
|
|
272
|
+
test("gracefully handles invalid stored state (schema mismatch)", () => {
|
|
256
273
|
const mockStorage = createMockStorageClient();
|
|
257
274
|
mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, JSON.stringify({ type: "invalid_type" }));
|
|
258
275
|
|
|
259
276
|
const { client } = createTestClient({ storage: mockStorage });
|
|
260
|
-
|
|
277
|
+
loadStateFromStorage(client);
|
|
278
|
+
|
|
279
|
+
// Should gracefully fall back to unidentified state
|
|
280
|
+
expect(client.getIdentifyState().type).toBe("unidentified");
|
|
261
281
|
});
|
|
262
282
|
|
|
263
283
|
test("creates logger with correct name", () => {
|
|
@@ -550,7 +570,17 @@ describe("IdentityClient", () => {
|
|
|
550
570
|
await client.identify();
|
|
551
571
|
|
|
552
572
|
const lastCall = mockApi.getLastCall();
|
|
553
|
-
const device = (
|
|
573
|
+
const device = (
|
|
574
|
+
lastCall.config.body as {
|
|
575
|
+
device?: {
|
|
576
|
+
timestamp: string;
|
|
577
|
+
application: { name: string; version: string; build: string; bundle_id: string };
|
|
578
|
+
os: { name: string; version: string };
|
|
579
|
+
hardware: { brand: string; model: string; device_type: string };
|
|
580
|
+
update: null;
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
).device;
|
|
554
584
|
|
|
555
585
|
expect(device?.timestamp).toBe("2024-01-15T10:30:00.000Z");
|
|
556
586
|
expect(device?.application.name).toBe("MyApp");
|
|
@@ -603,12 +633,12 @@ describe("IdentityClient", () => {
|
|
|
603
633
|
// Second identify
|
|
604
634
|
await client.identify();
|
|
605
635
|
|
|
606
|
-
// Should have debug
|
|
607
|
-
//
|
|
608
|
-
const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug");
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
expect(
|
|
636
|
+
// Should have debug logs about state transitions
|
|
637
|
+
// When already identified, identify() will transition: identified -> identifying -> identified
|
|
638
|
+
const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug" || l.level === "debugInfo");
|
|
639
|
+
expect(debugLogs.length).toBeGreaterThan(0);
|
|
640
|
+
// Check that state transitions are logged
|
|
641
|
+
expect(debugLogs.some((l) => l.message.includes("Identify state"))).toBe(true);
|
|
612
642
|
});
|
|
613
643
|
});
|
|
614
644
|
|
|
@@ -992,7 +1022,9 @@ describe("IdentityClient", () => {
|
|
|
992
1022
|
|
|
993
1023
|
// Make API slow so we can check intermediate state
|
|
994
1024
|
let resolveApi: (value: unknown) => void;
|
|
995
|
-
const apiPromise = new Promise((resolve) => {
|
|
1025
|
+
const apiPromise = new Promise((resolve) => {
|
|
1026
|
+
resolveApi = resolve;
|
|
1027
|
+
});
|
|
996
1028
|
|
|
997
1029
|
mockApi.client = async () => {
|
|
998
1030
|
await apiPromise;
|
|
@@ -1015,7 +1047,7 @@ describe("IdentityClient", () => {
|
|
|
1015
1047
|
const identifyPromise = client.identify();
|
|
1016
1048
|
|
|
1017
1049
|
// Check storage while API call is pending
|
|
1018
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1050
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1019
1051
|
const intermediateStored = mockStorage.getStorage().get(IDENTIFY_STORAGE_KEY);
|
|
1020
1052
|
expect(JSON.parse(intermediateStored!).type).toBe("identifying");
|
|
1021
1053
|
|
|
@@ -1048,14 +1080,10 @@ describe("IdentityClient", () => {
|
|
|
1048
1080
|
test("handles rapid successive identify calls", async () => {
|
|
1049
1081
|
const { client } = createTestClient();
|
|
1050
1082
|
|
|
1051
|
-
const results = await Promise.all([
|
|
1052
|
-
client.identify(),
|
|
1053
|
-
client.identify(),
|
|
1054
|
-
client.identify(),
|
|
1055
|
-
]);
|
|
1083
|
+
const results = await Promise.all([client.identify(), client.identify(), client.identify()]);
|
|
1056
1084
|
|
|
1057
1085
|
// All should succeed
|
|
1058
|
-
results.forEach(result => {
|
|
1086
|
+
results.forEach((result) => {
|
|
1059
1087
|
expect(result.success).toBe(true);
|
|
1060
1088
|
});
|
|
1061
1089
|
|
|
@@ -1116,4 +1144,4 @@ describe("IdentityClient", () => {
|
|
|
1116
1144
|
expect(parsed.session.session_id).toBe("session-with-émojis-🚀-and-üñíçödé");
|
|
1117
1145
|
});
|
|
1118
1146
|
});
|
|
1119
|
-
});
|
|
1147
|
+
});
|
|
@@ -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
|
|
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
|
|
17
|
+
export interface IdentityUser {
|
|
18
18
|
session_id: string;
|
|
19
19
|
device_id: string;
|
|
20
20
|
user_id: string;
|
|
@@ -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
|
|
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
|
|
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
|
|
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 */
|
|
@@ -2,9 +2,9 @@ import { createContext, useContext } from "react";
|
|
|
2
2
|
|
|
3
3
|
import type { TeardownCore } from "../teardown.core";
|
|
4
4
|
|
|
5
|
-
export
|
|
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
|
|
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();
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
+
const { core } = useTeardown();
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
core.identity.getSessionState()
|
|
12
|
-
);
|
|
10
|
+
const [session, setSession] = useState<Session | null>(core.identity.getSessionState());
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
};
|
|
26
|
+
return session;
|
|
27
|
+
};
|