@teardown/react-native 1.2.39 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +75 -7
  2. package/package.json +65 -47
  3. package/src/clients/api/api.client.ts +55 -0
  4. package/src/clients/api/index.ts +1 -0
  5. package/src/clients/device/device.adpater-interface.ts +57 -0
  6. package/src/clients/device/device.client.test.ts +195 -0
  7. package/src/clients/device/device.client.ts +69 -0
  8. package/src/clients/device/expo-adapter.ts +128 -0
  9. package/src/clients/device/index.ts +4 -0
  10. package/src/clients/force-update/force-update.client.test.ts +296 -0
  11. package/src/clients/force-update/force-update.client.ts +224 -0
  12. package/src/clients/force-update/index.ts +1 -0
  13. package/src/clients/identity/identity.client.test.ts +454 -0
  14. package/src/clients/identity/identity.client.ts +249 -0
  15. package/src/clients/identity/index.ts +1 -0
  16. package/src/clients/logging/index.ts +1 -0
  17. package/src/clients/logging/logging.client.ts +92 -0
  18. package/src/clients/notifications/notifications.client.ts +10 -0
  19. package/src/clients/storage/index.ts +1 -0
  20. package/src/clients/storage/mmkv-adapter.ts +23 -0
  21. package/src/clients/storage/storage.client.ts +75 -0
  22. package/src/clients/utils/index.ts +1 -0
  23. package/src/clients/utils/utils.client.ts +75 -0
  24. package/src/components/ui/button.tsx +0 -0
  25. package/src/components/ui/input.tsx +0 -0
  26. package/src/contexts/index.ts +1 -0
  27. package/src/contexts/teardown.context.ts +17 -0
  28. package/src/exports/expo.ts +1 -0
  29. package/src/exports/index.ts +16 -0
  30. package/src/exports/mmkv.ts +1 -0
  31. package/src/hooks/use-force-update.ts +38 -0
  32. package/src/hooks/use-session.ts +26 -0
  33. package/src/providers/teardown.provider.tsx +28 -0
  34. package/src/teardown.core.ts +76 -0
  35. package/dist/components/index.d.ts +0 -1
  36. package/dist/components/index.js +0 -3
  37. package/dist/components/index.js.map +0 -1
  38. package/dist/components/teardown-logo.d.ts +0 -4
  39. package/dist/components/teardown-logo.js +0 -35
  40. package/dist/components/teardown-logo.js.map +0 -1
  41. package/dist/containers/index.d.ts +0 -1
  42. package/dist/containers/index.js +0 -18
  43. package/dist/containers/index.js.map +0 -1
  44. package/dist/containers/teardown.container.d.ts +0 -8
  45. package/dist/containers/teardown.container.js +0 -26
  46. package/dist/containers/teardown.container.js.map +0 -1
  47. package/dist/index.d.ts +0 -2
  48. package/dist/index.js +0 -22
  49. package/dist/index.js.map +0 -1
  50. package/dist/plugins/http.plugin.d.ts +0 -23
  51. package/dist/plugins/http.plugin.js +0 -145
  52. package/dist/plugins/http.plugin.js.map +0 -1
  53. package/dist/plugins/index.d.ts +0 -2
  54. package/dist/plugins/index.js +0 -20
  55. package/dist/plugins/index.js.map +0 -1
  56. package/dist/plugins/logging.plugin.d.ts +0 -9
  57. package/dist/plugins/logging.plugin.js +0 -36
  58. package/dist/plugins/logging.plugin.js.map +0 -1
  59. package/dist/plugins/websocket.plugin.d.ts +0 -1
  60. package/dist/plugins/websocket.plugin.js +0 -108
  61. package/dist/plugins/websocket.plugin.js.map +0 -1
  62. package/dist/services/index.d.ts +0 -1
  63. package/dist/services/index.js +0 -18
  64. package/dist/services/index.js.map +0 -1
  65. package/dist/services/teardown.service.d.ts +0 -10
  66. package/dist/services/teardown.service.js +0 -22
  67. package/dist/services/teardown.service.js.map +0 -1
  68. package/dist/teardown.client.d.ts +0 -41
  69. package/dist/teardown.client.js +0 -60
  70. package/dist/teardown.client.js.map +0 -1
  71. package/dist/utils/log.d.ts +0 -5
  72. package/dist/utils/log.js +0 -9
  73. package/dist/utils/log.js.map +0 -1
@@ -0,0 +1,454 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // Mock react-native before any imports that use it
4
+ mock.module("react-native", () => ({
5
+ AppState: {
6
+ addEventListener: () => ({ remove: () => {} }),
7
+ },
8
+ }));
9
+
10
+ // Import after mock
11
+ const { IdentityClient } = await import("./identity.client");
12
+ const { IdentifyVersionStatusEnum } = await import("../force-update");
13
+ type IdentifyState = import("./identity.client").IdentifyState;
14
+
15
+ function createMockLoggingClient() {
16
+ return {
17
+ createLogger: () => ({
18
+ info: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ debug: () => {},
22
+ }),
23
+ };
24
+ }
25
+
26
+ function createMockStorageClient() {
27
+ const storage = new Map<string, string>();
28
+ return {
29
+ createStorage: () => ({
30
+ getItem: (key: string) => storage.get(key) ?? null,
31
+ setItem: (key: string, value: string) => storage.set(key, value),
32
+ removeItem: (key: string) => storage.delete(key),
33
+ }),
34
+ getStorage: () => storage,
35
+ };
36
+ }
37
+
38
+ function createMockUtilsClient() {
39
+ return {
40
+ generateRandomUUID: async () => "mock-uuid",
41
+ };
42
+ }
43
+
44
+ function createMockDeviceClient() {
45
+ return {
46
+ getDeviceId: async () => "mock-device-id",
47
+ getDeviceInfo: async () => ({
48
+ application: { name: "TestApp", version: "1.0.0", build: "100", bundle_id: "com.test" },
49
+ hardware: { brand: "Apple", model: "iPhone", device_type: "PHONE" },
50
+ os: { name: "iOS", version: "17.0" },
51
+ notifications: { push_token: null, platform: null },
52
+ update: null,
53
+ }),
54
+ };
55
+ }
56
+
57
+ function createMockApiClient(options: {
58
+ success?: boolean;
59
+ versionStatus?: IdentifyVersionStatusEnum;
60
+ errorStatus?: number;
61
+ errorMessage?: string;
62
+ } = {}) {
63
+ const { success = true, versionStatus = IdentifyVersionStatusEnum.UP_TO_DATE, errorStatus, errorMessage } = options;
64
+
65
+ return {
66
+ apiKey: "test-api-key",
67
+ orgId: "test-org-id",
68
+ projectId: "test-project-id",
69
+ client: async () => {
70
+ if (!success) {
71
+ return {
72
+ error: {
73
+ status: errorStatus ?? 500,
74
+ value: { message: errorMessage ?? "API Error", error: { message: errorMessage ?? "API Error" } },
75
+ },
76
+ data: null,
77
+ };
78
+ }
79
+ return {
80
+ error: null,
81
+ data: {
82
+ data: {
83
+ session_id: "session-123",
84
+ device_id: "device-123",
85
+ persona_id: "persona-123",
86
+ token: "token-123",
87
+ version_info: { status: versionStatus },
88
+ },
89
+ },
90
+ };
91
+ },
92
+ };
93
+ }
94
+
95
+ describe("IdentityClient", () => {
96
+ describe("initial state", () => {
97
+ test("starts with unidentified state when no stored state", () => {
98
+ const mockLogging = createMockLoggingClient();
99
+ const mockStorage = createMockStorageClient();
100
+ const mockUtils = createMockUtilsClient();
101
+ const mockApi = createMockApiClient();
102
+ const mockDevice = createMockDeviceClient();
103
+
104
+ const client = new IdentityClient(
105
+ mockLogging as never,
106
+ mockUtils as never,
107
+ mockStorage as never,
108
+ mockApi as never,
109
+ mockDevice as never
110
+ );
111
+
112
+ expect(client.getIdentifyState().type).toBe("unidentified");
113
+ });
114
+
115
+ test("restores state from storage", () => {
116
+ const mockLogging = createMockLoggingClient();
117
+ const mockStorage = createMockStorageClient();
118
+ const mockUtils = createMockUtilsClient();
119
+ const mockApi = createMockApiClient();
120
+ const mockDevice = createMockDeviceClient();
121
+
122
+ // Pre-populate storage with identified state
123
+ const storedState = {
124
+ type: "identified",
125
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
126
+ version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
127
+ };
128
+ mockStorage.getStorage().set("IDENTIFY_STATE", JSON.stringify(storedState));
129
+
130
+ const client = new IdentityClient(
131
+ mockLogging as never,
132
+ mockUtils as never,
133
+ mockStorage as never,
134
+ mockApi as never,
135
+ mockDevice as never
136
+ );
137
+
138
+ const state = client.getIdentifyState();
139
+ expect(state.type).toBe("identified");
140
+ if (state.type === "identified") {
141
+ expect(state.session.session_id).toBe("s1");
142
+ }
143
+ });
144
+ });
145
+
146
+ describe("identify", () => {
147
+ test("transitions to identifying then identified on success", async () => {
148
+ const mockLogging = createMockLoggingClient();
149
+ const mockStorage = createMockStorageClient();
150
+ const mockUtils = createMockUtilsClient();
151
+ const mockApi = createMockApiClient({ success: true });
152
+ const mockDevice = createMockDeviceClient();
153
+
154
+ const client = new IdentityClient(
155
+ mockLogging as never,
156
+ mockUtils as never,
157
+ mockStorage as never,
158
+ mockApi as never,
159
+ mockDevice as never
160
+ );
161
+
162
+ const stateChanges: IdentifyState[] = [];
163
+ client.onIdentifyStateChange((state) => stateChanges.push(state));
164
+
165
+ const result = await client.identify();
166
+
167
+ expect(result.success).toBe(true);
168
+ expect(stateChanges).toHaveLength(2);
169
+ expect(stateChanges[0].type).toBe("identifying");
170
+ expect(stateChanges[1].type).toBe("identified");
171
+ });
172
+
173
+ test("returns user data on successful identify", async () => {
174
+ const mockLogging = createMockLoggingClient();
175
+ const mockStorage = createMockStorageClient();
176
+ const mockUtils = createMockUtilsClient();
177
+ const mockApi = createMockApiClient({ versionStatus: IdentifyVersionStatusEnum.UPDATE_AVAILABLE });
178
+ const mockDevice = createMockDeviceClient();
179
+
180
+ const client = new IdentityClient(
181
+ mockLogging as never,
182
+ mockUtils as never,
183
+ mockStorage as never,
184
+ mockApi as never,
185
+ mockDevice as never
186
+ );
187
+
188
+ const result = await client.identify();
189
+
190
+ expect(result.success).toBe(true);
191
+ if (result.success) {
192
+ expect(result.data.session_id).toBe("session-123");
193
+ expect(result.data.device_id).toBe("device-123");
194
+ expect(result.data.version_info.status).toBe(IdentifyVersionStatusEnum.UPDATE_AVAILABLE);
195
+ }
196
+ });
197
+
198
+ test("reverts to previous state on API error", async () => {
199
+ const mockLogging = createMockLoggingClient();
200
+ const mockStorage = createMockStorageClient();
201
+ const mockUtils = createMockUtilsClient();
202
+ const mockApi = createMockApiClient({ success: false, errorStatus: 500, errorMessage: "Server error" });
203
+ const mockDevice = createMockDeviceClient();
204
+
205
+ const client = new IdentityClient(
206
+ mockLogging as never,
207
+ mockUtils as never,
208
+ mockStorage as never,
209
+ mockApi as never,
210
+ mockDevice as never
211
+ );
212
+
213
+ const result = await client.identify();
214
+
215
+ expect(result.success).toBe(false);
216
+ // Should revert to unidentified (the previous state)
217
+ expect(client.getIdentifyState().type).toBe("unidentified");
218
+ });
219
+
220
+ test("handles 422 validation error", async () => {
221
+ const mockLogging = createMockLoggingClient();
222
+ const mockStorage = createMockStorageClient();
223
+ const mockUtils = createMockUtilsClient();
224
+ const mockApi = createMockApiClient({ success: false, errorStatus: 422, errorMessage: "Validation failed" });
225
+ const mockDevice = createMockDeviceClient();
226
+
227
+ const client = new IdentityClient(
228
+ mockLogging as never,
229
+ mockUtils as never,
230
+ mockStorage as never,
231
+ mockApi as never,
232
+ mockDevice as never
233
+ );
234
+
235
+ const result = await client.identify();
236
+
237
+ expect(result.success).toBe(false);
238
+ if (!result.success) {
239
+ expect(result.error).toBe("Validation failed");
240
+ }
241
+ });
242
+
243
+ test("persists identified state to storage", async () => {
244
+ const mockLogging = createMockLoggingClient();
245
+ const mockStorage = createMockStorageClient();
246
+ const mockUtils = createMockUtilsClient();
247
+ const mockApi = createMockApiClient({ success: true });
248
+ const mockDevice = createMockDeviceClient();
249
+
250
+ const client = new IdentityClient(
251
+ mockLogging as never,
252
+ mockUtils as never,
253
+ mockStorage as never,
254
+ mockApi as never,
255
+ mockDevice as never
256
+ );
257
+
258
+ await client.identify();
259
+
260
+ const stored = mockStorage.getStorage().get("IDENTIFY_STATE");
261
+ expect(stored).toBeDefined();
262
+ const parsed = JSON.parse(stored!);
263
+ expect(parsed.type).toBe("identified");
264
+ expect(parsed.session.session_id).toBe("session-123");
265
+ });
266
+ });
267
+
268
+ describe("onIdentifyStateChange", () => {
269
+ test("emits state changes to listeners", async () => {
270
+ const mockLogging = createMockLoggingClient();
271
+ const mockStorage = createMockStorageClient();
272
+ const mockUtils = createMockUtilsClient();
273
+ const mockApi = createMockApiClient();
274
+ const mockDevice = createMockDeviceClient();
275
+
276
+ const client = new IdentityClient(
277
+ mockLogging as never,
278
+ mockUtils as never,
279
+ mockStorage as never,
280
+ mockApi as never,
281
+ mockDevice as never
282
+ );
283
+
284
+ const states: IdentifyState[] = [];
285
+ client.onIdentifyStateChange((state) => states.push(state));
286
+
287
+ await client.identify();
288
+
289
+ expect(states.length).toBeGreaterThan(0);
290
+ });
291
+
292
+ test("returns unsubscribe function", async () => {
293
+ const mockLogging = createMockLoggingClient();
294
+ const mockStorage = createMockStorageClient();
295
+ const mockUtils = createMockUtilsClient();
296
+ const mockApi = createMockApiClient();
297
+ const mockDevice = createMockDeviceClient();
298
+
299
+ const client = new IdentityClient(
300
+ mockLogging as never,
301
+ mockUtils as never,
302
+ mockStorage as never,
303
+ mockApi as never,
304
+ mockDevice as never
305
+ );
306
+
307
+ const states: IdentifyState[] = [];
308
+ const unsubscribe = client.onIdentifyStateChange((state) => states.push(state));
309
+
310
+ unsubscribe();
311
+
312
+ await client.identify();
313
+
314
+ // Should not receive any state changes after unsubscribing
315
+ expect(states).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ describe("refresh", () => {
320
+ test("returns error when not identified", async () => {
321
+ const mockLogging = createMockLoggingClient();
322
+ const mockStorage = createMockStorageClient();
323
+ const mockUtils = createMockUtilsClient();
324
+ const mockApi = createMockApiClient();
325
+ const mockDevice = createMockDeviceClient();
326
+
327
+ const client = new IdentityClient(
328
+ mockLogging as never,
329
+ mockUtils as never,
330
+ mockStorage as never,
331
+ mockApi as never,
332
+ mockDevice as never
333
+ );
334
+
335
+ const result = await client.refresh();
336
+
337
+ expect(result.success).toBe(false);
338
+ if (!result.success) {
339
+ expect(result.error).toBe("Not identified");
340
+ }
341
+ });
342
+
343
+ test("re-identifies when already identified", async () => {
344
+ const mockLogging = createMockLoggingClient();
345
+ const mockStorage = createMockStorageClient();
346
+ const mockUtils = createMockUtilsClient();
347
+ const mockApi = createMockApiClient();
348
+ const mockDevice = createMockDeviceClient();
349
+
350
+ const client = new IdentityClient(
351
+ mockLogging as never,
352
+ mockUtils as never,
353
+ mockStorage as never,
354
+ mockApi as never,
355
+ mockDevice as never
356
+ );
357
+
358
+ // First identify
359
+ await client.identify();
360
+ expect(client.getIdentifyState().type).toBe("identified");
361
+
362
+ // Then refresh
363
+ const result = await client.refresh();
364
+ expect(result.success).toBe(true);
365
+ });
366
+ });
367
+
368
+ describe("reset", () => {
369
+ test("resets state to unidentified", async () => {
370
+ const mockLogging = createMockLoggingClient();
371
+ const mockStorage = createMockStorageClient();
372
+ const mockUtils = createMockUtilsClient();
373
+ const mockApi = createMockApiClient();
374
+ const mockDevice = createMockDeviceClient();
375
+
376
+ const client = new IdentityClient(
377
+ mockLogging as never,
378
+ mockUtils as never,
379
+ mockStorage as never,
380
+ mockApi as never,
381
+ mockDevice as never
382
+ );
383
+
384
+ // First identify
385
+ await client.identify();
386
+ expect(client.getIdentifyState().type).toBe("identified");
387
+
388
+ // Then reset
389
+ client.reset();
390
+
391
+ expect(client.getIdentifyState().type).toBe("unidentified");
392
+ // Storage contains unidentified state after reset (setIdentifyState saves it)
393
+ const stored = mockStorage.getStorage().get("IDENTIFY_STATE");
394
+ expect(stored).toBeDefined();
395
+ if (stored) {
396
+ expect(JSON.parse(stored).type).toBe("unidentified");
397
+ }
398
+ });
399
+
400
+ test("emits unidentified state on reset", async () => {
401
+ const mockLogging = createMockLoggingClient();
402
+ const mockStorage = createMockStorageClient();
403
+ const mockUtils = createMockUtilsClient();
404
+ const mockApi = createMockApiClient();
405
+ const mockDevice = createMockDeviceClient();
406
+
407
+ const client = new IdentityClient(
408
+ mockLogging as never,
409
+ mockUtils as never,
410
+ mockStorage as never,
411
+ mockApi as never,
412
+ mockDevice as never
413
+ );
414
+
415
+ await client.identify();
416
+
417
+ const states: IdentifyState[] = [];
418
+ client.onIdentifyStateChange((state) => states.push(state));
419
+
420
+ client.reset();
421
+
422
+ expect(states).toHaveLength(1);
423
+ expect(states[0].type).toBe("unidentified");
424
+ });
425
+ });
426
+
427
+ describe("shutdown", () => {
428
+ test("removes all listeners", async () => {
429
+ const mockLogging = createMockLoggingClient();
430
+ const mockStorage = createMockStorageClient();
431
+ const mockUtils = createMockUtilsClient();
432
+ const mockApi = createMockApiClient();
433
+ const mockDevice = createMockDeviceClient();
434
+
435
+ const client = new IdentityClient(
436
+ mockLogging as never,
437
+ mockUtils as never,
438
+ mockStorage as never,
439
+ mockApi as never,
440
+ mockDevice as never
441
+ );
442
+
443
+ const states: IdentifyState[] = [];
444
+ client.onIdentifyStateChange((state) => states.push(state));
445
+
446
+ client.shutdown();
447
+
448
+ await client.identify();
449
+
450
+ // Should not receive any state changes after shutdown
451
+ expect(states).toHaveLength(0);
452
+ });
453
+ });
454
+ });
@@ -0,0 +1,249 @@
1
+ import type { AsyncResult } from "@teardown/types";
2
+ import { EventEmitter } from "eventemitter3";
3
+ import { z } from "zod";
4
+ import type { ApiClient } from "../api";
5
+ import type { DeviceClient } from "../device/device.client";
6
+ import { IdentifyVersionStatusEnum } from "../force-update";
7
+ import type { Logger, LoggingClient } from "../logging";
8
+ import type { StorageClient, SupportedStorage } from "../storage";
9
+ import type { UtilsClient } from "../utils";
10
+
11
+ export type Persona = {
12
+ name?: string | undefined;
13
+ user_id?: string | undefined;
14
+ email?: string | undefined;
15
+ };
16
+
17
+ export type IdentityUser = {
18
+ session_id: string;
19
+ device_id: string;
20
+ persona_id: string;
21
+ token: string;
22
+ version_info: {
23
+ status: IdentifyVersionStatusEnum;
24
+ update: null
25
+ }
26
+ }
27
+
28
+ export const UnidentifiedSessionStateSchema = z.object({
29
+ type: z.literal("unidentified"),
30
+ });
31
+ export const IdentifyingSessionStateSchema = z.object({
32
+ type: z.literal("identifying"),
33
+ });
34
+
35
+
36
+ export const UpdateVersionStatusBodySchema = z.object({
37
+ version: z.string(),
38
+ });
39
+
40
+ export const SessionSchema = z.object({
41
+ session_id: z.string(),
42
+ device_id: z.string(),
43
+ persona_id: z.string(),
44
+ token: z.string(),
45
+ });
46
+ export type Session = z.infer<typeof SessionSchema>;
47
+
48
+ export const VersionStatusResponseSchema = z.object({
49
+ status: z.enum(["UPDATE_AVAILABLE", "UPDATE_REQUIRED", "UP_TO_DATE"]),
50
+ latest_version: z.string().optional(),
51
+ });
52
+
53
+ export const IdentifiedSessionStateSchema = z.object({
54
+ type: z.literal("identified"),
55
+ session: SessionSchema,
56
+ version_info: z.object({
57
+ status: z.enum(IdentifyVersionStatusEnum),
58
+ update: z.null(),
59
+ }),
60
+ });
61
+
62
+ export const IdentifyStateSchema = z.discriminatedUnion("type", [UnidentifiedSessionStateSchema, IdentifyingSessionStateSchema, IdentifiedSessionStateSchema]);
63
+ export type IdentifyState = z.infer<typeof IdentifyStateSchema>;
64
+
65
+ export type UnidentifiedSessionState = z.infer<typeof UnidentifiedSessionStateSchema>;
66
+ export type IdentifyingSessionState = z.infer<typeof IdentifyingSessionStateSchema>;
67
+ export type IdentifiedSessionState = z.infer<typeof IdentifiedSessionStateSchema>;
68
+
69
+ export type IdentifyStateChangeEvents = {
70
+ IDENTIFY_STATE_CHANGED: (state: IdentifyState) => void;
71
+ };
72
+
73
+
74
+ export const IDENTIFY_STORAGE_KEY = "IDENTIFY_STATE";
75
+
76
+ export class IdentityClient {
77
+ private emitter = new EventEmitter<IdentifyStateChangeEvents>();
78
+ private identifyState: IdentifyState;
79
+
80
+ public readonly logger: Logger;
81
+ public readonly utils: UtilsClient;
82
+ public readonly storage: SupportedStorage;
83
+
84
+ constructor(
85
+ logging: LoggingClient,
86
+ utils: UtilsClient,
87
+ storage: StorageClient,
88
+ private readonly api: ApiClient,
89
+ private readonly device: DeviceClient
90
+ ) {
91
+ this.logger = logging.createLogger({
92
+ name: "IdentityClient",
93
+ });
94
+ this.storage = storage.createStorage("identity");
95
+ this.utils = utils;
96
+ this.identifyState = this.getIdentifyStateFromStorage();
97
+ }
98
+
99
+ async initialize(): Promise<void> {
100
+ await this.identify();
101
+ }
102
+
103
+ private getIdentifyStateFromStorage(): IdentifyState {
104
+ const stored = this.storage.getItem(IDENTIFY_STORAGE_KEY);
105
+ if (stored == null) {
106
+ // console.log("no stored session state");
107
+ return UnidentifiedSessionStateSchema.parse({ type: "unidentified" });
108
+ }
109
+
110
+ // console.log("stored session state", stored);
111
+ return IdentifyStateSchema.parse(JSON.parse(stored));
112
+ }
113
+
114
+ private saveIdentifyStateToStorage(identifyState: IdentifyState): void {
115
+ this.storage.setItem(IDENTIFY_STORAGE_KEY, JSON.stringify(identifyState));
116
+ }
117
+
118
+ private setIdentifyState(newState: IdentifyState): void {
119
+ this.logger.info(`Identify state: ${this.identifyState.type} -> ${newState.type}`);
120
+ this.identifyState = newState;
121
+ this.saveIdentifyStateToStorage(newState);
122
+ this.emitter.emit("IDENTIFY_STATE_CHANGED", newState);
123
+ }
124
+
125
+ public onIdentifyStateChange(listener: (state: IdentifyState) => void) {
126
+ this.emitter.addListener("IDENTIFY_STATE_CHANGED", listener);
127
+ return () => {
128
+ this.emitter.removeListener("IDENTIFY_STATE_CHANGED", listener);
129
+ };
130
+ }
131
+
132
+ public getIdentifyState(): IdentifyState {
133
+ return this.identifyState;
134
+ }
135
+
136
+ public getSessionState(): Session | null {
137
+ if (this.identifyState.type !== "identified") {
138
+ return null;
139
+ }
140
+ return this.identifyState.session;
141
+ }
142
+
143
+ public shutdown() {
144
+ this.emitter.removeAllListeners("IDENTIFY_STATE_CHANGED");
145
+ }
146
+
147
+ public reset() {
148
+ this.storage.removeItem(IDENTIFY_STORAGE_KEY);
149
+ this.setIdentifyState({ type: "unidentified" });
150
+ }
151
+
152
+ /**
153
+ * Re-identify the current persona to refresh session data.
154
+ * Only works if already identified.
155
+ */
156
+ async refresh(): AsyncResult<IdentityUser> {
157
+ if (this.identifyState.type !== "identified") {
158
+ return { success: false, error: "Not identified" };
159
+ }
160
+ return this.identify();
161
+ }
162
+
163
+ /**
164
+ * Catches all errors and returns an AsyncResult
165
+ * @param fn - The function to try
166
+ * @returns An {@link AsyncResult}
167
+ */
168
+ private async tryCatch<T>(fn: () => AsyncResult<T>): AsyncResult<T> {
169
+ try {
170
+ const result = await fn();
171
+ return result;
172
+ } catch (error) {
173
+ return {
174
+ success: false,
175
+ error: error instanceof Error ? error.message : "Unknown error",
176
+ };
177
+ }
178
+ }
179
+
180
+ async identify(persona?: Persona): AsyncResult<IdentityUser> {
181
+ const previousState = this.identifyState;
182
+ this.setIdentifyState({ type: "identifying" });
183
+
184
+ return this.tryCatch(async () => {
185
+ const deviceId = await this.device.getDeviceId();
186
+ const deviceInfo = await this.device.getDeviceInfo();
187
+ const response = await this.api.client("/v1/identify", {
188
+ method: "POST",
189
+ headers: {
190
+ "td-api-key": this.api.apiKey,
191
+ "td-org-id": this.api.orgId,
192
+ "td-project-id": this.api.projectId,
193
+ "td-environment-slug": "production",
194
+ "td-device-id": deviceId,
195
+ },
196
+ body: {
197
+ persona,
198
+ device: {
199
+ ...deviceInfo,
200
+ update: deviceInfo.update
201
+ ? {
202
+ ...deviceInfo.update,
203
+ created_at: deviceInfo.update.created_at,
204
+ }
205
+ : null,
206
+ },
207
+ },
208
+ });
209
+
210
+ if (response.error != null) {
211
+ this.setIdentifyState(previousState);
212
+
213
+ if (response.error.status === 422) {
214
+ this.logger.warn("422 Error identifying user", response.error.value);
215
+ return {
216
+ success: false,
217
+ error: response.error.value.message ?? "Unknown error",
218
+ };
219
+ }
220
+
221
+ const value = response.error.value;
222
+ return {
223
+ success: false,
224
+ error: value?.error?.message ?? "Unknown error",
225
+ };
226
+ }
227
+
228
+ this.setIdentifyState({
229
+ type: "identified",
230
+ session: response.data.data,
231
+ version_info: {
232
+ status: response.data.data.version_info.status,
233
+ update: null,
234
+ },
235
+ });
236
+
237
+ return {
238
+ success: true,
239
+ data: {
240
+ ...response.data.data,
241
+ version_info: {
242
+ status: response.data.data.version_info.status,
243
+ update: null,
244
+ },
245
+ },
246
+ };
247
+ });
248
+ }
249
+ }
@@ -0,0 +1 @@
1
+ export * from "./identity.client";
@@ -0,0 +1 @@
1
+ export * from "./logging.client";