@croacroa/react-native-template 1.0.0 → 2.0.1

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 (70) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +21 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +60 -6
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +286 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/validation.ts +2 -1
  70. package/utils/withAccessibility.tsx +272 -0
@@ -1,428 +1,428 @@
1
- /**
2
- * Analytics Adapter Pattern
3
- *
4
- * This module provides an abstraction layer for analytics providers.
5
- * Supports multiple analytics backends simultaneously.
6
- */
7
-
8
- import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
9
- import { captureException as sentryCapture, addBreadcrumb } from "./sentry";
10
-
11
- // ============================================================================
12
- // Types
13
- // ============================================================================
14
-
15
- export interface AnalyticsAdapter {
16
- /**
17
- * Track a custom event
18
- */
19
- track(event: string, properties?: Record<string, unknown>): void;
20
-
21
- /**
22
- * Identify a user
23
- */
24
- identify(userId: string, traits?: Record<string, unknown>): void;
25
-
26
- /**
27
- * Track a screen view
28
- */
29
- screen(name: string, properties?: Record<string, unknown>): void;
30
-
31
- /**
32
- * Reset the analytics state (on logout)
33
- */
34
- reset(): void;
35
-
36
- /**
37
- * Set user properties that persist across events
38
- */
39
- setUserProperties?(properties: Record<string, unknown>): void;
40
-
41
- /**
42
- * Track revenue/purchase
43
- */
44
- trackRevenue?(
45
- amount: number,
46
- currency: string,
47
- productId?: string,
48
- properties?: Record<string, unknown>
49
- ): void;
50
-
51
- /**
52
- * Start a timed event
53
- */
54
- startTimer?(event: string): void;
55
-
56
- /**
57
- * End a timed event and track duration
58
- */
59
- endTimer?(event: string, properties?: Record<string, unknown>): void;
60
- }
61
-
62
- // ============================================================================
63
- // Console Adapter (Development)
64
- // ============================================================================
65
-
66
- const consoleAdapter: AnalyticsAdapter = {
67
- track(event, properties) {
68
- console.log(`[Analytics] Track: ${event}`, properties);
69
- },
70
-
71
- identify(userId, traits) {
72
- console.log(`[Analytics] Identify: ${userId}`, traits);
73
- },
74
-
75
- screen(name, properties) {
76
- console.log(`[Analytics] Screen: ${name}`, properties);
77
- },
78
-
79
- reset() {
80
- console.log("[Analytics] Reset");
81
- },
82
-
83
- setUserProperties(properties) {
84
- console.log("[Analytics] User Properties:", properties);
85
- },
86
-
87
- trackRevenue(amount, currency, productId, properties) {
88
- console.log(`[Analytics] Revenue: ${amount} ${currency}`, {
89
- productId,
90
- ...properties,
91
- });
92
- },
93
- };
94
-
95
- // ============================================================================
96
- // Sentry Adapter (Error Tracking + Basic Analytics)
97
- // ============================================================================
98
-
99
- const sentryAdapter: AnalyticsAdapter = {
100
- track(event, properties) {
101
- addBreadcrumb("analytics", event, properties);
102
- },
103
-
104
- identify(userId, traits) {
105
- // Sentry user is set via setUser in sentry.ts
106
- addBreadcrumb("user", `Identified: ${userId}`, traits);
107
- },
108
-
109
- screen(name, properties) {
110
- addBreadcrumb("navigation", `Screen: ${name}`, properties);
111
- },
112
-
113
- reset() {
114
- addBreadcrumb("user", "User reset");
115
- },
116
- };
117
-
118
- // ============================================================================
119
- // Example: Mixpanel Adapter
120
- // ============================================================================
121
-
122
- /**
123
- * Example Mixpanel implementation:
124
- *
125
- * import { Mixpanel } from 'mixpanel-react-native';
126
- *
127
- * const mixpanel = new Mixpanel('YOUR_PROJECT_TOKEN', true);
128
- * mixpanel.init();
129
- *
130
- * const mixpanelAdapter: AnalyticsAdapter = {
131
- * track(event, properties) {
132
- * mixpanel.track(event, properties);
133
- * },
134
- *
135
- * identify(userId, traits) {
136
- * mixpanel.identify(userId);
137
- * if (traits) {
138
- * mixpanel.getPeople().set(traits);
139
- * }
140
- * },
141
- *
142
- * screen(name, properties) {
143
- * mixpanel.track('Screen View', { screen_name: name, ...properties });
144
- * },
145
- *
146
- * reset() {
147
- * mixpanel.reset();
148
- * },
149
- *
150
- * setUserProperties(properties) {
151
- * mixpanel.getPeople().set(properties);
152
- * },
153
- *
154
- * trackRevenue(amount, currency, productId, properties) {
155
- * mixpanel.getPeople().trackCharge(amount, { currency, productId, ...properties });
156
- * },
157
- * };
158
- */
159
-
160
- // ============================================================================
161
- // Example: Amplitude Adapter
162
- // ============================================================================
163
-
164
- /**
165
- * Example Amplitude implementation:
166
- *
167
- * import { Amplitude } from '@amplitude/analytics-react-native';
168
- *
169
- * Amplitude.init('YOUR_API_KEY');
170
- *
171
- * const amplitudeAdapter: AnalyticsAdapter = {
172
- * track(event, properties) {
173
- * Amplitude.track(event, properties);
174
- * },
175
- *
176
- * identify(userId, traits) {
177
- * Amplitude.setUserId(userId);
178
- * if (traits) {
179
- * const identifyObj = new Amplitude.Identify();
180
- * Object.entries(traits).forEach(([key, value]) => {
181
- * identifyObj.set(key, value);
182
- * });
183
- * Amplitude.identify(identifyObj);
184
- * }
185
- * },
186
- *
187
- * screen(name, properties) {
188
- * Amplitude.track('Screen View', { screen_name: name, ...properties });
189
- * },
190
- *
191
- * reset() {
192
- * Amplitude.reset();
193
- * },
194
- *
195
- * setUserProperties(properties) {
196
- * const identifyObj = new Amplitude.Identify();
197
- * Object.entries(properties).forEach(([key, value]) => {
198
- * identifyObj.set(key, value);
199
- * });
200
- * Amplitude.identify(identifyObj);
201
- * },
202
- *
203
- * trackRevenue(amount, currency, productId, properties) {
204
- * const revenue = new Amplitude.Revenue()
205
- * .setPrice(amount)
206
- * .setProductId(productId || 'unknown')
207
- * .setRevenueType('purchase');
208
- * Amplitude.revenue(revenue);
209
- * },
210
- * };
211
- */
212
-
213
- // ============================================================================
214
- // Multi-Provider Analytics Manager
215
- // ============================================================================
216
-
217
- class Analytics implements AnalyticsAdapter {
218
- private adapters: AnalyticsAdapter[] = [];
219
- private timers: Map<string, number> = new Map();
220
- private superProperties: Record<string, unknown> = {};
221
-
222
- /**
223
- * Add an analytics adapter
224
- */
225
- addAdapter(adapter: AnalyticsAdapter): void {
226
- this.adapters.push(adapter);
227
- }
228
-
229
- /**
230
- * Remove all adapters
231
- */
232
- clearAdapters(): void {
233
- this.adapters = [];
234
- }
235
-
236
- /**
237
- * Set properties that will be sent with every event
238
- */
239
- setSuperProperties(properties: Record<string, unknown>): void {
240
- this.superProperties = { ...this.superProperties, ...properties };
241
- }
242
-
243
- /**
244
- * Clear super properties
245
- */
246
- clearSuperProperties(): void {
247
- this.superProperties = {};
248
- }
249
-
250
- track(event: string, properties?: Record<string, unknown>): void {
251
- if (!ENABLE_ANALYTICS && !IS_DEV) return;
252
-
253
- const mergedProperties = { ...this.superProperties, ...properties };
254
-
255
- this.adapters.forEach((adapter) => {
256
- try {
257
- adapter.track(event, mergedProperties);
258
- } catch (error) {
259
- console.error(`Analytics track error:`, error);
260
- sentryCapture(error as Error, { event, properties: mergedProperties });
261
- }
262
- });
263
- }
264
-
265
- identify(userId: string, traits?: Record<string, unknown>): void {
266
- if (!ENABLE_ANALYTICS && !IS_DEV) return;
267
-
268
- this.adapters.forEach((adapter) => {
269
- try {
270
- adapter.identify(userId, traits);
271
- } catch (error) {
272
- console.error(`Analytics identify error:`, error);
273
- sentryCapture(error as Error, { userId });
274
- }
275
- });
276
- }
277
-
278
- screen(name: string, properties?: Record<string, unknown>): void {
279
- if (!ENABLE_ANALYTICS && !IS_DEV) return;
280
-
281
- const mergedProperties = { ...this.superProperties, ...properties };
282
-
283
- this.adapters.forEach((adapter) => {
284
- try {
285
- adapter.screen(name, mergedProperties);
286
- } catch (error) {
287
- console.error(`Analytics screen error:`, error);
288
- }
289
- });
290
- }
291
-
292
- reset(): void {
293
- this.clearSuperProperties();
294
- this.timers.clear();
295
-
296
- this.adapters.forEach((adapter) => {
297
- try {
298
- adapter.reset();
299
- } catch (error) {
300
- console.error(`Analytics reset error:`, error);
301
- }
302
- });
303
- }
304
-
305
- setUserProperties(properties: Record<string, unknown>): void {
306
- if (!ENABLE_ANALYTICS && !IS_DEV) return;
307
-
308
- this.adapters.forEach((adapter) => {
309
- try {
310
- adapter.setUserProperties?.(properties);
311
- } catch (error) {
312
- console.error(`Analytics setUserProperties error:`, error);
313
- }
314
- });
315
- }
316
-
317
- trackRevenue(
318
- amount: number,
319
- currency: string,
320
- productId?: string,
321
- properties?: Record<string, unknown>
322
- ): void {
323
- if (!ENABLE_ANALYTICS && !IS_DEV) return;
324
-
325
- this.adapters.forEach((adapter) => {
326
- try {
327
- adapter.trackRevenue?.(amount, currency, productId, properties);
328
- } catch (error) {
329
- console.error(`Analytics trackRevenue error:`, error);
330
- }
331
- });
332
-
333
- // Also track as a regular event for providers that don't support revenue
334
- this.track("Purchase", {
335
- amount,
336
- currency,
337
- productId,
338
- ...properties,
339
- });
340
- }
341
-
342
- startTimer(event: string): void {
343
- this.timers.set(event, Date.now());
344
- }
345
-
346
- endTimer(event: string, properties?: Record<string, unknown>): void {
347
- const startTime = this.timers.get(event);
348
- if (startTime) {
349
- const duration = Date.now() - startTime;
350
- this.timers.delete(event);
351
- this.track(event, { ...properties, duration_ms: duration });
352
- }
353
- }
354
- }
355
-
356
- // ============================================================================
357
- // Singleton Instance
358
- // ============================================================================
359
-
360
- export const analytics = new Analytics();
361
-
362
- // Initialize with default adapters
363
- if (IS_DEV) {
364
- analytics.addAdapter(consoleAdapter);
365
- }
366
- analytics.addAdapter(sentryAdapter);
367
-
368
- // ============================================================================
369
- // Convenience Exports
370
- // ============================================================================
371
-
372
- export const track = analytics.track.bind(analytics);
373
- export const identify = analytics.identify.bind(analytics);
374
- export const screen = analytics.screen.bind(analytics);
375
- export const resetAnalytics = analytics.reset.bind(analytics);
376
- export const setUserProperties = analytics.setUserProperties.bind(analytics);
377
- export const trackRevenue = analytics.trackRevenue.bind(analytics);
378
- export const startTimer = analytics.startTimer.bind(analytics);
379
- export const endTimer = analytics.endTimer.bind(analytics);
380
-
381
- // ============================================================================
382
- // Pre-defined Events (Type Safety)
383
- // ============================================================================
384
-
385
- export const AnalyticsEvents = {
386
- // Auth
387
- SIGN_UP_STARTED: "Sign Up Started",
388
- SIGN_UP_COMPLETED: "Sign Up Completed",
389
- SIGN_UP_FAILED: "Sign Up Failed",
390
- SIGN_IN_STARTED: "Sign In Started",
391
- SIGN_IN_COMPLETED: "Sign In Completed",
392
- SIGN_IN_FAILED: "Sign In Failed",
393
- SIGN_OUT: "Sign Out",
394
- PASSWORD_RESET_REQUESTED: "Password Reset Requested",
395
-
396
- // Onboarding
397
- ONBOARDING_STARTED: "Onboarding Started",
398
- ONBOARDING_STEP_COMPLETED: "Onboarding Step Completed",
399
- ONBOARDING_COMPLETED: "Onboarding Completed",
400
- ONBOARDING_SKIPPED: "Onboarding Skipped",
401
-
402
- // Navigation
403
- SCREEN_VIEW: "Screen View",
404
- TAB_CHANGED: "Tab Changed",
405
- DEEP_LINK_OPENED: "Deep Link Opened",
406
-
407
- // User Actions
408
- PROFILE_UPDATED: "Profile Updated",
409
- SETTINGS_CHANGED: "Settings Changed",
410
- NOTIFICATION_ENABLED: "Notification Enabled",
411
- NOTIFICATION_DISABLED: "Notification Disabled",
412
- BIOMETRIC_ENABLED: "Biometric Enabled",
413
- BIOMETRIC_DISABLED: "Biometric Disabled",
414
-
415
- // Errors
416
- ERROR_OCCURRED: "Error Occurred",
417
- API_ERROR: "API Error",
418
- NETWORK_ERROR: "Network Error",
419
-
420
- // Engagement
421
- FEATURE_USED: "Feature Used",
422
- BUTTON_CLICKED: "Button Clicked",
423
- SEARCH_PERFORMED: "Search Performed",
424
- CONTENT_SHARED: "Content Shared",
425
- } as const;
426
-
427
- export type AnalyticsEvent =
428
- (typeof AnalyticsEvents)[keyof typeof AnalyticsEvents];
1
+ /**
2
+ * Analytics Adapter Pattern
3
+ *
4
+ * This module provides an abstraction layer for analytics providers.
5
+ * Supports multiple analytics backends simultaneously.
6
+ */
7
+
8
+ import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
9
+ import { captureException as sentryCapture, addBreadcrumb } from "./sentry";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface AnalyticsAdapter {
16
+ /**
17
+ * Track a custom event
18
+ */
19
+ track(event: string, properties?: Record<string, unknown>): void;
20
+
21
+ /**
22
+ * Identify a user
23
+ */
24
+ identify(userId: string, traits?: Record<string, unknown>): void;
25
+
26
+ /**
27
+ * Track a screen view
28
+ */
29
+ screen(name: string, properties?: Record<string, unknown>): void;
30
+
31
+ /**
32
+ * Reset the analytics state (on logout)
33
+ */
34
+ reset(): void;
35
+
36
+ /**
37
+ * Set user properties that persist across events
38
+ */
39
+ setUserProperties?(properties: Record<string, unknown>): void;
40
+
41
+ /**
42
+ * Track revenue/purchase
43
+ */
44
+ trackRevenue?(
45
+ amount: number,
46
+ currency: string,
47
+ productId?: string,
48
+ properties?: Record<string, unknown>
49
+ ): void;
50
+
51
+ /**
52
+ * Start a timed event
53
+ */
54
+ startTimer?(event: string): void;
55
+
56
+ /**
57
+ * End a timed event and track duration
58
+ */
59
+ endTimer?(event: string, properties?: Record<string, unknown>): void;
60
+ }
61
+
62
+ // ============================================================================
63
+ // Console Adapter (Development)
64
+ // ============================================================================
65
+
66
+ const consoleAdapter: AnalyticsAdapter = {
67
+ track(event, properties) {
68
+ console.log(`[Analytics] Track: ${event}`, properties);
69
+ },
70
+
71
+ identify(userId, traits) {
72
+ console.log(`[Analytics] Identify: ${userId}`, traits);
73
+ },
74
+
75
+ screen(name, properties) {
76
+ console.log(`[Analytics] Screen: ${name}`, properties);
77
+ },
78
+
79
+ reset() {
80
+ console.log("[Analytics] Reset");
81
+ },
82
+
83
+ setUserProperties(properties) {
84
+ console.log("[Analytics] User Properties:", properties);
85
+ },
86
+
87
+ trackRevenue(amount, currency, productId, properties) {
88
+ console.log(`[Analytics] Revenue: ${amount} ${currency}`, {
89
+ productId,
90
+ ...properties,
91
+ });
92
+ },
93
+ };
94
+
95
+ // ============================================================================
96
+ // Sentry Adapter (Error Tracking + Basic Analytics)
97
+ // ============================================================================
98
+
99
+ const sentryAdapter: AnalyticsAdapter = {
100
+ track(event, properties) {
101
+ addBreadcrumb("analytics", event, properties);
102
+ },
103
+
104
+ identify(userId, traits) {
105
+ // Sentry user is set via setUser in sentry.ts
106
+ addBreadcrumb("user", `Identified: ${userId}`, traits);
107
+ },
108
+
109
+ screen(name, properties) {
110
+ addBreadcrumb("navigation", `Screen: ${name}`, properties);
111
+ },
112
+
113
+ reset() {
114
+ addBreadcrumb("user", "User reset");
115
+ },
116
+ };
117
+
118
+ // ============================================================================
119
+ // Example: Mixpanel Adapter
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Example Mixpanel implementation:
124
+ *
125
+ * import { Mixpanel } from 'mixpanel-react-native';
126
+ *
127
+ * const mixpanel = new Mixpanel('YOUR_PROJECT_TOKEN', true);
128
+ * mixpanel.init();
129
+ *
130
+ * const mixpanelAdapter: AnalyticsAdapter = {
131
+ * track(event, properties) {
132
+ * mixpanel.track(event, properties);
133
+ * },
134
+ *
135
+ * identify(userId, traits) {
136
+ * mixpanel.identify(userId);
137
+ * if (traits) {
138
+ * mixpanel.getPeople().set(traits);
139
+ * }
140
+ * },
141
+ *
142
+ * screen(name, properties) {
143
+ * mixpanel.track('Screen View', { screen_name: name, ...properties });
144
+ * },
145
+ *
146
+ * reset() {
147
+ * mixpanel.reset();
148
+ * },
149
+ *
150
+ * setUserProperties(properties) {
151
+ * mixpanel.getPeople().set(properties);
152
+ * },
153
+ *
154
+ * trackRevenue(amount, currency, productId, properties) {
155
+ * mixpanel.getPeople().trackCharge(amount, { currency, productId, ...properties });
156
+ * },
157
+ * };
158
+ */
159
+
160
+ // ============================================================================
161
+ // Example: Amplitude Adapter
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Example Amplitude implementation:
166
+ *
167
+ * import { Amplitude } from '@amplitude/analytics-react-native';
168
+ *
169
+ * Amplitude.init('YOUR_API_KEY');
170
+ *
171
+ * const amplitudeAdapter: AnalyticsAdapter = {
172
+ * track(event, properties) {
173
+ * Amplitude.track(event, properties);
174
+ * },
175
+ *
176
+ * identify(userId, traits) {
177
+ * Amplitude.setUserId(userId);
178
+ * if (traits) {
179
+ * const identifyObj = new Amplitude.Identify();
180
+ * Object.entries(traits).forEach(([key, value]) => {
181
+ * identifyObj.set(key, value);
182
+ * });
183
+ * Amplitude.identify(identifyObj);
184
+ * }
185
+ * },
186
+ *
187
+ * screen(name, properties) {
188
+ * Amplitude.track('Screen View', { screen_name: name, ...properties });
189
+ * },
190
+ *
191
+ * reset() {
192
+ * Amplitude.reset();
193
+ * },
194
+ *
195
+ * setUserProperties(properties) {
196
+ * const identifyObj = new Amplitude.Identify();
197
+ * Object.entries(properties).forEach(([key, value]) => {
198
+ * identifyObj.set(key, value);
199
+ * });
200
+ * Amplitude.identify(identifyObj);
201
+ * },
202
+ *
203
+ * trackRevenue(amount, currency, productId, properties) {
204
+ * const revenue = new Amplitude.Revenue()
205
+ * .setPrice(amount)
206
+ * .setProductId(productId || 'unknown')
207
+ * .setRevenueType('purchase');
208
+ * Amplitude.revenue(revenue);
209
+ * },
210
+ * };
211
+ */
212
+
213
+ // ============================================================================
214
+ // Multi-Provider Analytics Manager
215
+ // ============================================================================
216
+
217
+ class Analytics implements AnalyticsAdapter {
218
+ private adapters: AnalyticsAdapter[] = [];
219
+ private timers: Map<string, number> = new Map();
220
+ private superProperties: Record<string, unknown> = {};
221
+
222
+ /**
223
+ * Add an analytics adapter
224
+ */
225
+ addAdapter(adapter: AnalyticsAdapter): void {
226
+ this.adapters.push(adapter);
227
+ }
228
+
229
+ /**
230
+ * Remove all adapters
231
+ */
232
+ clearAdapters(): void {
233
+ this.adapters = [];
234
+ }
235
+
236
+ /**
237
+ * Set properties that will be sent with every event
238
+ */
239
+ setSuperProperties(properties: Record<string, unknown>): void {
240
+ this.superProperties = { ...this.superProperties, ...properties };
241
+ }
242
+
243
+ /**
244
+ * Clear super properties
245
+ */
246
+ clearSuperProperties(): void {
247
+ this.superProperties = {};
248
+ }
249
+
250
+ track(event: string, properties?: Record<string, unknown>): void {
251
+ if (!ENABLE_ANALYTICS && !IS_DEV) return;
252
+
253
+ const mergedProperties = { ...this.superProperties, ...properties };
254
+
255
+ this.adapters.forEach((adapter) => {
256
+ try {
257
+ adapter.track(event, mergedProperties);
258
+ } catch (error) {
259
+ console.error(`Analytics track error:`, error);
260
+ sentryCapture(error as Error, { event, properties: mergedProperties });
261
+ }
262
+ });
263
+ }
264
+
265
+ identify(userId: string, traits?: Record<string, unknown>): void {
266
+ if (!ENABLE_ANALYTICS && !IS_DEV) return;
267
+
268
+ this.adapters.forEach((adapter) => {
269
+ try {
270
+ adapter.identify(userId, traits);
271
+ } catch (error) {
272
+ console.error(`Analytics identify error:`, error);
273
+ sentryCapture(error as Error, { userId });
274
+ }
275
+ });
276
+ }
277
+
278
+ screen(name: string, properties?: Record<string, unknown>): void {
279
+ if (!ENABLE_ANALYTICS && !IS_DEV) return;
280
+
281
+ const mergedProperties = { ...this.superProperties, ...properties };
282
+
283
+ this.adapters.forEach((adapter) => {
284
+ try {
285
+ adapter.screen(name, mergedProperties);
286
+ } catch (error) {
287
+ console.error(`Analytics screen error:`, error);
288
+ }
289
+ });
290
+ }
291
+
292
+ reset(): void {
293
+ this.clearSuperProperties();
294
+ this.timers.clear();
295
+
296
+ this.adapters.forEach((adapter) => {
297
+ try {
298
+ adapter.reset();
299
+ } catch (error) {
300
+ console.error(`Analytics reset error:`, error);
301
+ }
302
+ });
303
+ }
304
+
305
+ setUserProperties(properties: Record<string, unknown>): void {
306
+ if (!ENABLE_ANALYTICS && !IS_DEV) return;
307
+
308
+ this.adapters.forEach((adapter) => {
309
+ try {
310
+ adapter.setUserProperties?.(properties);
311
+ } catch (error) {
312
+ console.error(`Analytics setUserProperties error:`, error);
313
+ }
314
+ });
315
+ }
316
+
317
+ trackRevenue(
318
+ amount: number,
319
+ currency: string,
320
+ productId?: string,
321
+ properties?: Record<string, unknown>
322
+ ): void {
323
+ if (!ENABLE_ANALYTICS && !IS_DEV) return;
324
+
325
+ this.adapters.forEach((adapter) => {
326
+ try {
327
+ adapter.trackRevenue?.(amount, currency, productId, properties);
328
+ } catch (error) {
329
+ console.error(`Analytics trackRevenue error:`, error);
330
+ }
331
+ });
332
+
333
+ // Also track as a regular event for providers that don't support revenue
334
+ this.track("Purchase", {
335
+ amount,
336
+ currency,
337
+ productId,
338
+ ...properties,
339
+ });
340
+ }
341
+
342
+ startTimer(event: string): void {
343
+ this.timers.set(event, Date.now());
344
+ }
345
+
346
+ endTimer(event: string, properties?: Record<string, unknown>): void {
347
+ const startTime = this.timers.get(event);
348
+ if (startTime) {
349
+ const duration = Date.now() - startTime;
350
+ this.timers.delete(event);
351
+ this.track(event, { ...properties, duration_ms: duration });
352
+ }
353
+ }
354
+ }
355
+
356
+ // ============================================================================
357
+ // Singleton Instance
358
+ // ============================================================================
359
+
360
+ export const analytics = new Analytics();
361
+
362
+ // Initialize with default adapters
363
+ if (IS_DEV) {
364
+ analytics.addAdapter(consoleAdapter);
365
+ }
366
+ analytics.addAdapter(sentryAdapter);
367
+
368
+ // ============================================================================
369
+ // Convenience Exports
370
+ // ============================================================================
371
+
372
+ export const track = analytics.track.bind(analytics);
373
+ export const identify = analytics.identify.bind(analytics);
374
+ export const screen = analytics.screen.bind(analytics);
375
+ export const resetAnalytics = analytics.reset.bind(analytics);
376
+ export const setUserProperties = analytics.setUserProperties.bind(analytics);
377
+ export const trackRevenue = analytics.trackRevenue.bind(analytics);
378
+ export const startTimer = analytics.startTimer.bind(analytics);
379
+ export const endTimer = analytics.endTimer.bind(analytics);
380
+
381
+ // ============================================================================
382
+ // Pre-defined Events (Type Safety)
383
+ // ============================================================================
384
+
385
+ export const AnalyticsEvents = {
386
+ // Auth
387
+ SIGN_UP_STARTED: "Sign Up Started",
388
+ SIGN_UP_COMPLETED: "Sign Up Completed",
389
+ SIGN_UP_FAILED: "Sign Up Failed",
390
+ SIGN_IN_STARTED: "Sign In Started",
391
+ SIGN_IN_COMPLETED: "Sign In Completed",
392
+ SIGN_IN_FAILED: "Sign In Failed",
393
+ SIGN_OUT: "Sign Out",
394
+ PASSWORD_RESET_REQUESTED: "Password Reset Requested",
395
+
396
+ // Onboarding
397
+ ONBOARDING_STARTED: "Onboarding Started",
398
+ ONBOARDING_STEP_COMPLETED: "Onboarding Step Completed",
399
+ ONBOARDING_COMPLETED: "Onboarding Completed",
400
+ ONBOARDING_SKIPPED: "Onboarding Skipped",
401
+
402
+ // Navigation
403
+ SCREEN_VIEW: "Screen View",
404
+ TAB_CHANGED: "Tab Changed",
405
+ DEEP_LINK_OPENED: "Deep Link Opened",
406
+
407
+ // User Actions
408
+ PROFILE_UPDATED: "Profile Updated",
409
+ SETTINGS_CHANGED: "Settings Changed",
410
+ NOTIFICATION_ENABLED: "Notification Enabled",
411
+ NOTIFICATION_DISABLED: "Notification Disabled",
412
+ BIOMETRIC_ENABLED: "Biometric Enabled",
413
+ BIOMETRIC_DISABLED: "Biometric Disabled",
414
+
415
+ // Errors
416
+ ERROR_OCCURRED: "Error Occurred",
417
+ API_ERROR: "API Error",
418
+ NETWORK_ERROR: "Network Error",
419
+
420
+ // Engagement
421
+ FEATURE_USED: "Feature Used",
422
+ BUTTON_CLICKED: "Button Clicked",
423
+ SEARCH_PERFORMED: "Search Performed",
424
+ CONTENT_SHARED: "Content Shared",
425
+ } as const;
426
+
427
+ export type AnalyticsEvent =
428
+ (typeof AnalyticsEvents)[keyof typeof AnalyticsEvents];