@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 +1 -1
- package/src/.DS_Store +0 -0
- 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 +84 -100
- package/src/clients/force-update/force-update.client.ts +27 -20
- package/src/clients/identity/identity.client.test.ts +66 -46
- package/src/clients/identity/identity.client.ts +6 -6
- package/src/clients/logging/logging.client.ts +4 -4
- package/src/clients/storage/storage.client.ts +68 -76
- 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/src/teardown.core.ts +0 -1
package/package.json
CHANGED
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
|
|
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);
|
|
@@ -289,12 +267,10 @@ describe("ForceUpdateClient", () => {
|
|
|
289
267
|
|
|
290
268
|
mockIdentity.setNextIdentifyResult(null);
|
|
291
269
|
|
|
292
|
-
const client = new ForceUpdateClient(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
//
|
|
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("
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
246
|
-
|
|
247
|
-
|
|
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 =
|
|
252
|
-
|
|
253
|
-
|
|
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: () => {
|
|
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: () => {
|
|
53
|
+
resetCounter: () => {
|
|
54
|
+
uuidCounter = 0;
|
|
55
|
+
},
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function createMockDeviceClient(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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: () => {
|
|
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(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 = (
|
|
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
|
|
607
|
-
//
|
|
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
|
-
|
|
610
|
-
//
|
|
611
|
-
expect(
|
|
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) => {
|
|
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
|
|
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;
|
|
@@ -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
|
|
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 */
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
+
};
|