@teardown/react-native 2.0.4 → 2.0.10

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.
@@ -27,7 +27,7 @@ function createMockLoggingClient() {
27
27
  debug: (message: string, ...args: unknown[]) => logs.push({ level: "debug", message, args }),
28
28
  }),
29
29
  getLogs: () => logs,
30
- clearLogs: () => logs.length = 0,
30
+ clearLogs: () => { logs.length = 0; },
31
31
  };
32
32
  }
33
33
 
@@ -48,7 +48,7 @@ function createMockUtilsClient() {
48
48
  let uuidCounter = 0;
49
49
  return {
50
50
  generateRandomUUID: async () => `mock-uuid-${++uuidCounter}`,
51
- resetCounter: () => uuidCounter = 0,
51
+ resetCounter: () => { uuidCounter = 0; },
52
52
  };
53
53
  }
54
54
 
@@ -90,12 +90,12 @@ type ApiCallRecord = {
90
90
 
91
91
  function createMockApiClient(options: {
92
92
  success?: boolean;
93
- versionStatus?: IdentifyVersionStatusEnum;
93
+ versionStatus?: (typeof IdentifyVersionStatusEnum)[keyof typeof IdentifyVersionStatusEnum];
94
94
  errorStatus?: number;
95
95
  errorMessage?: string;
96
96
  sessionId?: string;
97
97
  deviceId?: string;
98
- personaId?: string;
98
+ user_id?: string;
99
99
  token?: string;
100
100
  throwError?: Error;
101
101
  } = {}) {
@@ -106,7 +106,7 @@ function createMockApiClient(options: {
106
106
  errorMessage,
107
107
  sessionId = "session-123",
108
108
  deviceId = "device-123",
109
- personaId = "persona-123",
109
+ user_id = "user-123",
110
110
  token = "token-123",
111
111
  throwError,
112
112
  } = options;
@@ -143,7 +143,7 @@ function createMockApiClient(options: {
143
143
  data: {
144
144
  session_id: sessionId,
145
145
  device_id: deviceId,
146
- persona_id: personaId,
146
+ user_id: user_id,
147
147
  token: token,
148
148
  version_info: { status: versionStatus },
149
149
  },
@@ -152,7 +152,7 @@ function createMockApiClient(options: {
152
152
  },
153
153
  getCalls: () => calls,
154
154
  getLastCall: () => calls[calls.length - 1],
155
- clearCalls: () => calls.length = 0,
155
+ clearCalls: () => { calls.length = 0; },
156
156
  };
157
157
  }
158
158
 
@@ -214,7 +214,7 @@ describe("IdentityClient", () => {
214
214
  const mockStorage = createMockStorageClient();
215
215
  const storedState = {
216
216
  type: "identified",
217
- session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
217
+ session: { session_id: "s1", device_id: "d1", user_id: "p1", token: "t1" },
218
218
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
219
219
  };
220
220
  mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, JSON.stringify(storedState));
@@ -227,7 +227,7 @@ describe("IdentityClient", () => {
227
227
  if (state.type === "identified") {
228
228
  expect(state.session.session_id).toBe("s1");
229
229
  expect(state.session.device_id).toBe("d1");
230
- expect(state.session.persona_id).toBe("p1");
230
+ expect(state.session.user_id).toBe("p1");
231
231
  expect(state.session.token).toBe("t1");
232
232
  }
233
233
  });
@@ -296,7 +296,7 @@ describe("IdentityClient", () => {
296
296
  versionStatus: IdentifyVersionStatusEnum.UPDATE_AVAILABLE,
297
297
  sessionId: "custom-session",
298
298
  deviceId: "custom-device",
299
- personaId: "custom-persona",
299
+ user_id: "custom-user_id",
300
300
  token: "custom-token",
301
301
  });
302
302
  const { client } = createTestClient({ api: mockApi });
@@ -307,7 +307,7 @@ describe("IdentityClient", () => {
307
307
  if (result.success) {
308
308
  expect(result.data.session_id).toBe("custom-session");
309
309
  expect(result.data.device_id).toBe("custom-device");
310
- expect(result.data.persona_id).toBe("custom-persona");
310
+ expect(result.data.user_id).toBe("custom-user_id");
311
311
  expect(result.data.token).toBe("custom-token");
312
312
  expect(result.data.version_info.status).toBe(IdentifyVersionStatusEnum.UPDATE_AVAILABLE);
313
313
  expect(result.data.version_info.update).toBeNull();
@@ -360,7 +360,7 @@ describe("IdentityClient", () => {
360
360
  const mockStorage = createMockStorageClient();
361
361
  const storedState = {
362
362
  type: "identified",
363
- session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
363
+ session: { session_id: "s1", device_id: "d1", user_id: "p1", token: "t1" },
364
364
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
365
365
  };
366
366
  mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, JSON.stringify(storedState));
@@ -484,7 +484,7 @@ describe("IdentityClient", () => {
484
484
 
485
485
  const persona: Persona = {
486
486
  name: "John Doe",
487
- user_id: "user-456",
487
+ user_id: "user-123",
488
488
  email: "john@example.com",
489
489
  };
490
490
 
@@ -566,7 +566,7 @@ describe("IdentityClient", () => {
566
566
  const mockStorage = createMockStorageClient();
567
567
  const storedState = {
568
568
  type: "identified",
569
- session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
569
+ session: { session_id: "s1", device_id: "d1", user_id: "p1", token: "t1" },
570
570
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
571
571
  };
572
572
  mockStorage.getStorage().set(IDENTIFY_STORAGE_KEY, JSON.stringify(storedState));
@@ -605,10 +605,10 @@ describe("IdentityClient", () => {
605
605
 
606
606
  // Should have debug log about state already being 'identifying' (first setIdentifyState call)
607
607
  // Then transitions to identified
608
- const debugLogs = mockLogging.getLogs().filter(l => l.level === "debug");
608
+ const debugLogs = mockLogging.getLogs().filter((l) => l.level === "debug");
609
609
  // The "identifying" to "identifying" won't happen since we start from "identified"
610
610
  // But we can check that state changes are logged properly
611
- expect(mockLogging.getLogs().some(l => l.level === "info")).toBe(true);
611
+ expect(mockLogging.getLogs().some(l => l.level === "debug")).toBe(true);
612
612
  });
613
613
  });
614
614
 
@@ -731,7 +731,7 @@ describe("IdentityClient", () => {
731
731
  data: {
732
732
  session_id: "refreshed-session",
733
733
  device_id: "device-123",
734
- persona_id: "persona-123",
734
+ user_id: "user-123",
735
735
  token: "token-123",
736
736
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE },
737
737
  },
@@ -873,7 +873,7 @@ describe("IdentityClient", () => {
873
873
  expect(session).not.toBeNull();
874
874
  expect(session?.session_id).toBe("session-123");
875
875
  expect(session?.device_id).toBe("device-123");
876
- expect(session?.persona_id).toBe("persona-123");
876
+ expect(session?.user_id).toBe("user-123");
877
877
  expect(session?.token).toBe("token-123");
878
878
  });
879
879
 
@@ -891,7 +891,7 @@ describe("IdentityClient", () => {
891
891
  data: {
892
892
  session_id: "second-session",
893
893
  device_id: "device-123",
894
- persona_id: "persona-123",
894
+ user_id: "user-123",
895
895
  token: "token-123",
896
896
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE },
897
897
  },
@@ -1002,7 +1002,7 @@ describe("IdentityClient", () => {
1002
1002
  data: {
1003
1003
  session_id: "session-123",
1004
1004
  device_id: "device-123",
1005
- persona_id: "persona-123",
1005
+ user_id: "user-123",
1006
1006
  token: "token-123",
1007
1007
  version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE },
1008
1008
  },
@@ -17,7 +17,7 @@ export type Persona = {
17
17
  export type IdentityUser = {
18
18
  session_id: string;
19
19
  device_id: string;
20
- persona_id: string;
20
+ user_id: string;
21
21
  token: string;
22
22
  version_info: {
23
23
  status: IdentifyVersionStatusEnum;
@@ -40,7 +40,7 @@ export const UpdateVersionStatusBodySchema = z.object({
40
40
  export const SessionSchema = z.object({
41
41
  session_id: z.string(),
42
42
  device_id: z.string(),
43
- persona_id: z.string(),
43
+ user_id: z.string(),
44
44
  token: z.string(),
45
45
  });
46
46
  export type Session = z.infer<typeof SessionSchema>;
@@ -211,8 +211,8 @@ export class IdentityClient {
211
211
  }
212
212
  }
213
213
 
214
- async identify(persona?: Persona): AsyncResult<IdentityUser> {
215
- this.logger.debug(`Identifying user with persona: ${persona?.name ?? "none"}`);
214
+ async identify(user?: Persona): AsyncResult<IdentityUser> {
215
+ this.logger.debug(`Identifying user with persona: ${user?.name ?? "none"}`);
216
216
  const previousState = this.identifyState;
217
217
  this.setIdentifyState({ type: "identifying" });
218
218
 
@@ -231,8 +231,7 @@ export class IdentityClient {
231
231
  "td-device-id": deviceId,
232
232
  },
233
233
  body: {
234
- persona,
235
- // @ts-expect-error - notifications is not yet implemented
234
+ user,
236
235
  device: {
237
236
  timestamp: deviceInfo.timestamp,
238
237
  os: deviceInfo.os,
@@ -21,29 +21,27 @@ export class AsyncStorageAdapter extends StorageAdapter {
21
21
  const prefixedKey = (key: string): string => `${storageKey}:${key}`;
22
22
 
23
23
  return {
24
- preload: () => {
24
+ preload: async (): Promise<void> => {
25
25
  if (hydrated) return;
26
26
 
27
- // Fire async hydration - cache will be populated when complete
28
- AsyncStorage.getAllKeys()
29
- .then((allKeys) => {
30
- const relevantKeys = allKeys.filter((k) =>
31
- k.startsWith(`${storageKey}:`)
32
- );
33
- return AsyncStorage.multiGet(relevantKeys);
34
- })
35
- .then((pairs) => {
36
- for (const [fullKey, value] of pairs) {
37
- if (value != null) {
38
- const key = fullKey.replace(`${storageKey}:`, "");
39
- cache[key] = value;
40
- }
27
+ try {
28
+ const allKeys = await AsyncStorage.getAllKeys();
29
+ const relevantKeys = allKeys.filter((k) =>
30
+ k.startsWith(`${storageKey}:`)
31
+ );
32
+ const pairs = await AsyncStorage.multiGet(relevantKeys);
33
+
34
+ for (const [fullKey, value] of pairs) {
35
+ if (value != null) {
36
+ const key = fullKey.replace(`${storageKey}:`, "");
37
+ cache[key] = value;
41
38
  }
42
- hydrated = true;
43
- })
44
- .catch(() => {
45
- // Silently fail - cache remains empty
46
- });
39
+ }
40
+ } catch {
41
+ // Silently fail - cache remains empty
42
+ } finally {
43
+ hydrated = true;
44
+ }
47
45
  },
48
46
 
49
47
  getItem: (key: string): string | null => {
@@ -3,7 +3,7 @@
3
3
  * A storage interface that is used to store data.
4
4
  */
5
5
  export type SupportedStorage = {
6
- preload: () => void;
6
+ preload: () => void | Promise<void>;
7
7
  getItem: (key: string) => string | null;
8
8
  setItem: (key: string, value: string) => void;
9
9
  removeItem: (key: string) => void;
@@ -8,6 +8,14 @@ export class StorageClient {
8
8
 
9
9
  private readonly storage: Map<string, SupportedStorage> = new Map();
10
10
 
11
+ private readonly preloadPromises: Promise<void>[] = [];
12
+
13
+ private _isReady = false;
14
+
15
+ get isReady(): boolean {
16
+ return this._isReady;
17
+ }
18
+
11
19
  constructor(
12
20
  logging: LoggingClient,
13
21
  private readonly orgId: string,
@@ -40,7 +48,10 @@ export class StorageClient {
40
48
 
41
49
  this.logger.debug(`Creating new storage for ${fullStorageKey}`);
42
50
  const newStorage = this.storageAdapter.createStorage(fullStorageKey);
43
- newStorage.preload();
51
+ const preloadResult = newStorage.preload();
52
+ if (preloadResult instanceof Promise) {
53
+ this.preloadPromises.push(preloadResult);
54
+ }
44
55
 
45
56
  const remappedStorage = {
46
57
  ...newStorage,
@@ -55,6 +66,11 @@ export class StorageClient {
55
66
  return remappedStorage;
56
67
  }
57
68
 
69
+ async whenReady(): Promise<void> {
70
+ await Promise.all(this.preloadPromises);
71
+ this._isReady = true;
72
+ }
73
+
58
74
  shutdown(): void {
59
75
  this.storage.forEach((storage) => {
60
76
  storage.clear();
@@ -0,0 +1 @@
1
+ export * from "../../clients/device/adapters/device-info.adapter";
@@ -69,7 +69,9 @@ export class TeardownCore {
69
69
  }
70
70
 
71
71
  async initialize(): Promise<void> {
72
- // Initialize identity first (loads from storage, then identifies if needed)
72
+ // Wait for all storage hydration to complete
73
+ await this.storage.whenReady();
74
+ // Initialize identity (loads from storage, then identifies if needed)
73
75
  await this.identity.initialize();
74
76
  // Then initialize force update (subscribes to identity events)
75
77
  this.forceUpdate.initialize();
@@ -1,147 +0,0 @@
1
- # Getting Started
2
-
3
- This guide will walk you through installing and setting up the Teardown SDK in your React Native or Expo application.
4
-
5
- ## Installation
6
-
7
- Install the core package:
8
-
9
- ```bash
10
- bun add @teardown/react-native
11
- ```
12
-
13
- ### Peer Dependencies
14
-
15
- Install required peer dependencies:
16
-
17
- ```bash
18
- bun add react react-native zod
19
- ```
20
-
21
- ### Platform Adapters
22
-
23
- #### For Expo Projects
24
-
25
- ```bash
26
- bun add expo-application expo-device expo-updates
27
- ```
28
-
29
- #### For React Native CLI Projects
30
-
31
- ```bash
32
- bun add react-native-device-info
33
- ```
34
-
35
- ### Storage Adapter
36
-
37
- Install MMKV for fast, encrypted storage:
38
-
39
- ```bash
40
- bun add react-native-mmkv
41
- ```
42
-
43
- ## Initial Setup
44
-
45
- ### 1. Create SDK Configuration
46
-
47
- Create a file to initialize the Teardown SDK (e.g., `lib/teardown.ts`):
48
-
49
- ```typescript
50
- import { TeardownCore } from '@teardown/react-native';
51
- import { ExpoDeviceAdapter } from '@teardown/react-native/expo';
52
- import { createMMKVStorageFactory } from '@teardown/react-native/mmkv';
53
-
54
- export const teardown = new TeardownCore({
55
- org_id: 'your-org-id',
56
- project_id: 'your-project-id',
57
- api_key: 'your-api-key',
58
- storageFactory: createMMKVStorageFactory(),
59
- deviceAdapter: new ExpoDeviceAdapter(),
60
- forceUpdate: {
61
- throttleMs: 30_000, // Check every 30 seconds when app returns to foreground
62
- checkCooldownMs: 300_000, // Wait 5 minutes between checks
63
- },
64
- });
65
- ```
66
-
67
- ### 2. Wrap Your App with TeardownProvider
68
-
69
- In your root layout file (e.g., `app/_layout.tsx` for Expo Router):
70
-
71
- ```typescript
72
- import { TeardownProvider } from '@teardown/react-native';
73
- import { teardown } from '../lib/teardown';
74
-
75
- export default function RootLayout() {
76
- return (
77
- <TeardownProvider core={teardown}>
78
- <YourApp />
79
- </TeardownProvider>
80
- );
81
- }
82
- ```
83
-
84
- For traditional React Native:
85
-
86
- ```typescript
87
- import { TeardownProvider } from '@teardown/react-native';
88
- import { teardown } from './lib/teardown';
89
-
90
- export default function App() {
91
- return (
92
- <TeardownProvider core={teardown}>
93
- <YourNavigationStack />
94
- </TeardownProvider>
95
- );
96
- }
97
- ```
98
-
99
- ### 3. Use the SDK in Your Components
100
-
101
- ```typescript
102
- import { useTeardown } from '@teardown/react-native';
103
-
104
- function MyComponent() {
105
- const { core } = useTeardown();
106
-
107
- const handleLogin = async () => {
108
- const result = await core.identity.identify({
109
- user_id: 'user-123',
110
- email: 'user@example.com',
111
- name: 'John Doe',
112
- });
113
-
114
- if (result.success) {
115
- console.log('User identified:', result.data);
116
- }
117
- };
118
-
119
- return <Button onPress={handleLogin} title="Login" />;
120
- }
121
- ```
122
-
123
- ## Configuration Options
124
-
125
- ### TeardownCore Options
126
-
127
- | Option | Type | Required | Description |
128
- |--------|------|----------|-------------|
129
- | `org_id` | string | Yes | Your organization ID from Teardown dashboard |
130
- | `project_id` | string | Yes | Your project ID from Teardown dashboard |
131
- | `api_key` | string | Yes | Your API key from Teardown dashboard |
132
- | `storageFactory` | function | Yes | Storage adapter factory (e.g., MMKV) |
133
- | `deviceAdapter` | object | Yes | Device info adapter (Expo or RN) |
134
- | `forceUpdate` | object | No | Force update configuration |
135
-
136
- ### Force Update Options
137
-
138
- | Option | Type | Default | Description |
139
- |--------|------|---------|-------------|
140
- | `throttleMs` | number | 30000 | Minimum time between foreground checks (ms) |
141
- | `checkCooldownMs` | number | 300000 | Minimum time since last successful check (ms) |
142
-
143
- ## Next Steps
144
-
145
- - [Core Concepts](./02-core-concepts.mdx) - Understand the architecture
146
- - [Identity & Authentication](./03-identity.mdx) - Manage user sessions
147
- - [Force Updates](./04-force-updates.mdx) - Handle version management
@@ -1,188 +0,0 @@
1
- # Core Concepts
2
-
3
- Understanding the architecture and design principles of the Teardown SDK.
4
-
5
- ## Architecture Overview
6
-
7
- The Teardown SDK is built around a central `TeardownCore` instance that manages several specialized clients:
8
-
9
- ```
10
- TeardownCore
11
- ├── IdentityClient - User and device identity
12
- ├── ForceUpdateClient - Version management
13
- ├── DeviceClient - Device information
14
- ├── StorageClient - Persistent storage
15
- ├── LoggingClient - Structured logging
16
- └── ApiClient - API communication
17
- ```
18
-
19
- ## Core Principles
20
-
21
- ### 1. Single Source of Truth
22
-
23
- The `TeardownCore` instance is your single entry point to all SDK functionality. It's initialized once and passed through your app via the `TeardownProvider`.
24
-
25
- ```typescript
26
- const teardown = new TeardownCore({...});
27
-
28
- // Use throughout your app
29
- <TeardownProvider core={teardown}>
30
- <App />
31
- </TeardownProvider>
32
- ```
33
-
34
- ### 2. Automatic Initialization
35
-
36
- The SDK automatically initializes when the `TeardownProvider` mounts:
37
-
38
- - Loads persisted session data
39
- - Identifies the device
40
- - Checks version status
41
- - Subscribes to app lifecycle events
42
-
43
- ### 3. Reactive State Management
44
-
45
- All clients use event emitters to notify of state changes. React hooks automatically subscribe to these events:
46
-
47
- ```typescript
48
- // Hooks automatically subscribe and cleanup
49
- const session = useSession();
50
- const { versionStatus } = useForceUpdate();
51
- ```
52
-
53
- ### 4. Namespaced Storage
54
-
55
- Each client gets its own namespaced storage to prevent key collisions:
56
-
57
- ```typescript
58
- // Storage is automatically namespaced
59
- teardown:v1:identity:IDENTIFY_STATE
60
- teardown:v1:device:deviceId
61
- teardown:v1:version:VERSION_STATUS
62
- ```
63
-
64
- ## Client Responsibilities
65
-
66
- ### IdentityClient
67
-
68
- Manages device and user identity:
69
-
70
- - Generates unique device IDs
71
- - Tracks user sessions
72
- - Persists authentication state
73
- - Provides persona management (anonymous or identified users)
74
-
75
- ### ForceUpdateClient
76
-
77
- Handles version management:
78
-
79
- - Checks app version against backend
80
- - Monitors app state changes
81
- - Throttles version checks
82
- - Emits update status changes
83
-
84
- ### DeviceClient
85
-
86
- Collects device information:
87
-
88
- - OS version and platform
89
- - Device model and manufacturer
90
- - App version and build number
91
- - Uses platform adapters for consistency
92
-
93
- ### StorageClient
94
-
95
- Provides persistent storage:
96
-
97
- - Namespaced key-value storage
98
- - Platform-agnostic interface
99
- - Support for multiple adapters (MMKV, AsyncStorage, etc.)
100
- - Automatic cleanup on shutdown
101
-
102
- ### LoggingClient
103
-
104
- Structured logging system:
105
-
106
- - Configurable log levels
107
- - Named loggers for each client
108
- - Console binding preservation
109
- - Debug mode support
110
-
111
- ### ApiClient
112
-
113
- Handles API communication:
114
-
115
- - Type-safe API client
116
- - Automatic header injection
117
- - Request/response logging
118
- - Error handling
119
-
120
- ## Lifecycle Management
121
-
122
- ### Initialization
123
-
124
- ```typescript
125
- const teardown = new TeardownCore({...});
126
- // Automatically initializes identity client
127
- ```
128
-
129
- ### Runtime
130
-
131
- The SDK operates automatically:
132
- - Listens for app state changes
133
- - Checks version on foreground
134
- - Persists state changes
135
-
136
- ### Cleanup
137
-
138
- ```typescript
139
- // Automatic cleanup when provider unmounts
140
- useEffect(() => {
141
- return () => {
142
- core.shutdown();
143
- };
144
- }, [core]);
145
- ```
146
-
147
- ## State Persistence
148
-
149
- All critical state is automatically persisted:
150
-
151
- ```typescript
152
- // Identity state
153
- { type: "identified", session: {...}, version_info: {...} }
154
-
155
- // Version status
156
- { type: "up_to_date" | "update_available" | "update_required" | ... }
157
- ```
158
-
159
- State is restored on app restart for seamless user experience.
160
-
161
- ## Error Handling
162
-
163
- The SDK uses `AsyncResult` pattern for predictable error handling:
164
-
165
- ```typescript
166
- const result = await core.identity.identify({...});
167
-
168
- if (result.success) {
169
- // Handle success
170
- console.log(result.data);
171
- } else {
172
- // Handle error
173
- console.error(result.error);
174
- }
175
- ```
176
-
177
- ## Type Safety
178
-
179
- Full TypeScript support with:
180
- - Runtime validation using Zod schemas
181
- - Discriminated unions for state types
182
- - Exported types for all public APIs
183
-
184
- ## Next Steps
185
-
186
- - [Identity & Authentication](./03-identity.mdx)
187
- - [Force Updates](./04-force-updates.mdx)
188
- - [API Reference](./07-api-reference.mdx)