@croacroa/react-native-template 2.1.0 → 3.2.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 (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -21
  10. package/README.md +446 -402
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -418
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -30
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -40
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +5 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -375
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -176
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,434 +1,434 @@
1
- import { useEffect, useRef, useCallback, useState } from "react";
2
- import { InteractionManager } from "react-native";
3
- import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
4
- import { analytics, AnalyticsEvents } from "@/services/analytics";
5
-
6
- // ============================================================================
7
- // Types
8
- // ============================================================================
9
-
10
- interface PerformanceMetrics {
11
- /**
12
- * Time since component mounted (ms)
13
- */
14
- mountTime: number;
15
-
16
- /**
17
- * Time for initial render (ms)
18
- */
19
- renderTime: number;
20
-
21
- /**
22
- * Number of re-renders
23
- */
24
- renderCount: number;
25
-
26
- /**
27
- * Last render duration (ms)
28
- */
29
- lastRenderDuration: number;
30
-
31
- /**
32
- * Average FPS (if tracking enabled)
33
- */
34
- fps: number;
35
-
36
- /**
37
- * Memory usage (if available)
38
- */
39
- memoryUsage: number | null;
40
- }
41
-
42
- interface UsePerformanceOptions {
43
- /**
44
- * Name for identifying this component in logs
45
- */
46
- name: string;
47
-
48
- /**
49
- * Enable FPS tracking
50
- * @default false
51
- */
52
- trackFps?: boolean;
53
-
54
- /**
55
- * Log metrics to console in development
56
- * @default true
57
- */
58
- logInDev?: boolean;
59
-
60
- /**
61
- * Report metrics to analytics
62
- * @default false
63
- */
64
- reportToAnalytics?: boolean;
65
-
66
- /**
67
- * Threshold for slow render warning (ms)
68
- * @default 16
69
- */
70
- slowRenderThreshold?: number;
71
- }
72
-
73
- interface UsePerformanceReturn {
74
- /**
75
- * Current performance metrics
76
- */
77
- metrics: PerformanceMetrics;
78
-
79
- /**
80
- * Mark the start of an operation
81
- */
82
- markStart: (name: string) => void;
83
-
84
- /**
85
- * Mark the end of an operation and get duration
86
- */
87
- markEnd: (name: string) => number;
88
-
89
- /**
90
- * Track a custom metric
91
- */
92
- trackMetric: (name: string, value: number) => void;
93
-
94
- /**
95
- * Reset all metrics
96
- */
97
- reset: () => void;
98
- }
99
-
100
- // ============================================================================
101
- // Hook Implementation
102
- // ============================================================================
103
-
104
- /**
105
- * Hook for tracking component performance
106
- *
107
- * @example
108
- * ```tsx
109
- * function MyScreen() {
110
- * const { metrics, markStart, markEnd } = usePerformance({
111
- * name: 'MyScreen',
112
- * trackFps: true,
113
- * });
114
- *
115
- * const fetchData = async () => {
116
- * markStart('fetchData');
117
- * const data = await api.get('/data');
118
- * const duration = markEnd('fetchData');
119
- * console.log(`Fetch took ${duration}ms`);
120
- * };
121
- *
122
- * return <View />;
123
- * }
124
- * ```
125
- */
126
- export function usePerformance(
127
- options: UsePerformanceOptions
128
- ): UsePerformanceReturn {
129
- const {
130
- name,
131
- trackFps = false,
132
- logInDev = true,
133
- reportToAnalytics = false,
134
- slowRenderThreshold = 16, // 60fps = 16.67ms per frame
135
- } = options;
136
-
137
- const mountTimeRef = useRef(Date.now());
138
- const renderCountRef = useRef(0);
139
- const lastRenderStartRef = useRef(Date.now());
140
- const marksRef = useRef<Map<string, number>>(new Map());
141
- const fpsRef = useRef(60);
142
- const frameCountRef = useRef(0);
143
- const lastFpsUpdateRef = useRef(Date.now());
144
-
145
- const [metrics, setMetrics] = useState<PerformanceMetrics>({
146
- mountTime: 0,
147
- renderTime: 0,
148
- renderCount: 0,
149
- lastRenderDuration: 0,
150
- fps: 60,
151
- memoryUsage: null,
152
- });
153
-
154
- // Track render count and duration
155
- useEffect(() => {
156
- const renderEnd = Date.now();
157
- const renderDuration = renderEnd - lastRenderStartRef.current;
158
- renderCountRef.current += 1;
159
-
160
- // Update metrics
161
- setMetrics((prev) => ({
162
- ...prev,
163
- renderCount: renderCountRef.current,
164
- lastRenderDuration: renderDuration,
165
- renderTime:
166
- renderCountRef.current === 1 ? renderDuration : prev.renderTime,
167
- }));
168
-
169
- // Warn on slow render
170
- if (renderDuration > slowRenderThreshold && IS_DEV && logInDev) {
171
- console.warn(
172
- `[Performance] ${name}: Slow render detected (${renderDuration.toFixed(1)}ms)`
173
- );
174
- }
175
-
176
- // Prepare for next render
177
- lastRenderStartRef.current = Date.now();
178
- });
179
-
180
- // Track mount time
181
- useEffect(() => {
182
- const mountDuration = Date.now() - mountTimeRef.current;
183
-
184
- setMetrics((prev) => ({
185
- ...prev,
186
- mountTime: mountDuration,
187
- }));
188
-
189
- if (IS_DEV && logInDev) {
190
- console.log(`[Performance] ${name}: Mounted in ${mountDuration}ms`);
191
- }
192
-
193
- // Report to analytics
194
- if (reportToAnalytics && ENABLE_ANALYTICS) {
195
- analytics.track(AnalyticsEvents.SCREEN_VIEW, {
196
- screen: name,
197
- mountTime: mountDuration,
198
- });
199
- }
200
-
201
- return () => {
202
- if (IS_DEV && logInDev) {
203
- console.log(
204
- `[Performance] ${name}: Unmounted after ${renderCountRef.current} renders`
205
- );
206
- }
207
- };
208
- }, [name, logInDev, reportToAnalytics]);
209
-
210
- // FPS tracking
211
- useEffect(() => {
212
- if (!trackFps) return;
213
-
214
- let animationFrameId: number;
215
- let isRunning = true;
216
-
217
- const trackFrame = () => {
218
- if (!isRunning) return;
219
-
220
- frameCountRef.current += 1;
221
- const now = Date.now();
222
- const elapsed = now - lastFpsUpdateRef.current;
223
-
224
- // Update FPS every second
225
- if (elapsed >= 1000) {
226
- fpsRef.current = Math.round((frameCountRef.current * 1000) / elapsed);
227
- frameCountRef.current = 0;
228
- lastFpsUpdateRef.current = now;
229
-
230
- setMetrics((prev) => ({
231
- ...prev,
232
- fps: fpsRef.current,
233
- }));
234
-
235
- // Warn on low FPS
236
- if (fpsRef.current < 30 && IS_DEV && logInDev) {
237
- console.warn(
238
- `[Performance] ${name}: Low FPS detected (${fpsRef.current})`
239
- );
240
- }
241
- }
242
-
243
- animationFrameId = requestAnimationFrame(trackFrame);
244
- };
245
-
246
- animationFrameId = requestAnimationFrame(trackFrame);
247
-
248
- return () => {
249
- isRunning = false;
250
- cancelAnimationFrame(animationFrameId);
251
- };
252
- }, [trackFps, name, logInDev]);
253
-
254
- /**
255
- * Mark the start of an operation
256
- */
257
- const markStart = useCallback((markName: string) => {
258
- marksRef.current.set(markName, Date.now());
259
- }, []);
260
-
261
- /**
262
- * Mark the end of an operation and get duration
263
- */
264
- const markEnd = useCallback(
265
- (markName: string): number => {
266
- const start = marksRef.current.get(markName);
267
- if (!start) {
268
- console.warn(`[Performance] No start mark found for "${markName}"`);
269
- return 0;
270
- }
271
-
272
- const duration = Date.now() - start;
273
- marksRef.current.delete(markName);
274
-
275
- if (IS_DEV && logInDev) {
276
- console.log(`[Performance] ${name}.${markName}: ${duration}ms`);
277
- }
278
-
279
- if (reportToAnalytics && ENABLE_ANALYTICS) {
280
- analytics.track("Performance Metric", {
281
- component: name,
282
- operation: markName,
283
- duration,
284
- });
285
- }
286
-
287
- return duration;
288
- },
289
- [name, logInDev, reportToAnalytics]
290
- );
291
-
292
- /**
293
- * Track a custom metric
294
- */
295
- const trackMetric = useCallback(
296
- (metricName: string, value: number) => {
297
- if (IS_DEV && logInDev) {
298
- console.log(`[Performance] ${name}.${metricName}: ${value}`);
299
- }
300
-
301
- if (reportToAnalytics && ENABLE_ANALYTICS) {
302
- analytics.track("Performance Metric", {
303
- component: name,
304
- metric: metricName,
305
- value,
306
- });
307
- }
308
- },
309
- [name, logInDev, reportToAnalytics]
310
- );
311
-
312
- /**
313
- * Reset all metrics
314
- */
315
- const reset = useCallback(() => {
316
- mountTimeRef.current = Date.now();
317
- renderCountRef.current = 0;
318
- lastRenderStartRef.current = Date.now();
319
- marksRef.current.clear();
320
-
321
- setMetrics({
322
- mountTime: 0,
323
- renderTime: 0,
324
- renderCount: 0,
325
- lastRenderDuration: 0,
326
- fps: 60,
327
- memoryUsage: null,
328
- });
329
- }, []);
330
-
331
- return {
332
- metrics,
333
- markStart,
334
- markEnd,
335
- trackMetric,
336
- reset,
337
- };
338
- }
339
-
340
- // ============================================================================
341
- // Utility Functions
342
- // ============================================================================
343
-
344
- /**
345
- * Measure the execution time of an async function
346
- */
347
- export async function measureAsync<T>(
348
- name: string,
349
- fn: () => Promise<T>,
350
- options?: { log?: boolean; reportToAnalytics?: boolean }
351
- ): Promise<{ result: T; duration: number }> {
352
- const { log = IS_DEV, reportToAnalytics = false } = options || {};
353
-
354
- const start = Date.now();
355
- const result = await fn();
356
- const duration = Date.now() - start;
357
-
358
- if (log) {
359
- console.log(`[Performance] ${name}: ${duration}ms`);
360
- }
361
-
362
- if (reportToAnalytics && ENABLE_ANALYTICS) {
363
- analytics.track("Performance Metric", {
364
- operation: name,
365
- duration,
366
- });
367
- }
368
-
369
- return { result, duration };
370
- }
371
-
372
- /**
373
- * Measure the execution time of a sync function
374
- */
375
- export function measureSync<T>(
376
- name: string,
377
- fn: () => T,
378
- options?: { log?: boolean; reportToAnalytics?: boolean }
379
- ): { result: T; duration: number } {
380
- const { log = IS_DEV, reportToAnalytics = false } = options || {};
381
-
382
- const start = Date.now();
383
- const result = fn();
384
- const duration = Date.now() - start;
385
-
386
- if (log) {
387
- console.log(`[Performance] ${name}: ${duration}ms`);
388
- }
389
-
390
- if (reportToAnalytics && ENABLE_ANALYTICS) {
391
- analytics.track("Performance Metric", {
392
- operation: name,
393
- duration,
394
- });
395
- }
396
-
397
- return { result, duration };
398
- }
399
-
400
- /**
401
- * Wait for interactions to complete before executing
402
- * Useful for deferring heavy operations
403
- */
404
- export function runAfterInteractions<T>(fn: () => T | Promise<T>): Promise<T> {
405
- return new Promise((resolve) => {
406
- InteractionManager.runAfterInteractions(async () => {
407
- const result = await fn();
408
- resolve(result);
409
- });
410
- });
411
- }
412
-
413
- /**
414
- * Create a debounced function with performance tracking
415
- */
416
- export function createTrackedDebounce<
417
- T extends (...args: unknown[]) => unknown,
418
- >(fn: T, delay: number, name: string): T {
419
- let timeoutId: NodeJS.Timeout;
420
- let callCount = 0;
421
-
422
- return ((...args: Parameters<T>) => {
423
- callCount++;
424
- clearTimeout(timeoutId);
425
-
426
- timeoutId = setTimeout(() => {
427
- if (IS_DEV) {
428
- console.log(`[Performance] ${name}: Debounced ${callCount} calls to 1`);
429
- }
430
- callCount = 0;
431
- fn(...args);
432
- }, delay);
433
- }) as T;
434
- }
1
+ import { useEffect, useRef, useCallback, useState } from "react";
2
+ import { InteractionManager } from "react-native";
3
+ import { IS_DEV, ENABLE_ANALYTICS } from "@/constants/config";
4
+ import { analytics, AnalyticsEvents } from "@/services/analytics";
5
+
6
+ // ============================================================================
7
+ // Types
8
+ // ============================================================================
9
+
10
+ interface PerformanceMetrics {
11
+ /**
12
+ * Time since component mounted (ms)
13
+ */
14
+ mountTime: number;
15
+
16
+ /**
17
+ * Time for initial render (ms)
18
+ */
19
+ renderTime: number;
20
+
21
+ /**
22
+ * Number of re-renders
23
+ */
24
+ renderCount: number;
25
+
26
+ /**
27
+ * Last render duration (ms)
28
+ */
29
+ lastRenderDuration: number;
30
+
31
+ /**
32
+ * Average FPS (if tracking enabled)
33
+ */
34
+ fps: number;
35
+
36
+ /**
37
+ * Memory usage (if available)
38
+ */
39
+ memoryUsage: number | null;
40
+ }
41
+
42
+ interface UsePerformanceOptions {
43
+ /**
44
+ * Name for identifying this component in logs
45
+ */
46
+ name: string;
47
+
48
+ /**
49
+ * Enable FPS tracking
50
+ * @default false
51
+ */
52
+ trackFps?: boolean;
53
+
54
+ /**
55
+ * Log metrics to console in development
56
+ * @default true
57
+ */
58
+ logInDev?: boolean;
59
+
60
+ /**
61
+ * Report metrics to analytics
62
+ * @default false
63
+ */
64
+ reportToAnalytics?: boolean;
65
+
66
+ /**
67
+ * Threshold for slow render warning (ms)
68
+ * @default 16
69
+ */
70
+ slowRenderThreshold?: number;
71
+ }
72
+
73
+ interface UsePerformanceReturn {
74
+ /**
75
+ * Current performance metrics
76
+ */
77
+ metrics: PerformanceMetrics;
78
+
79
+ /**
80
+ * Mark the start of an operation
81
+ */
82
+ markStart: (name: string) => void;
83
+
84
+ /**
85
+ * Mark the end of an operation and get duration
86
+ */
87
+ markEnd: (name: string) => number;
88
+
89
+ /**
90
+ * Track a custom metric
91
+ */
92
+ trackMetric: (name: string, value: number) => void;
93
+
94
+ /**
95
+ * Reset all metrics
96
+ */
97
+ reset: () => void;
98
+ }
99
+
100
+ // ============================================================================
101
+ // Hook Implementation
102
+ // ============================================================================
103
+
104
+ /**
105
+ * Hook for tracking component performance
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * function MyScreen() {
110
+ * const { metrics, markStart, markEnd } = usePerformance({
111
+ * name: 'MyScreen',
112
+ * trackFps: true,
113
+ * });
114
+ *
115
+ * const fetchData = async () => {
116
+ * markStart('fetchData');
117
+ * const data = await api.get('/data');
118
+ * const duration = markEnd('fetchData');
119
+ * console.log(`Fetch took ${duration}ms`);
120
+ * };
121
+ *
122
+ * return <View />;
123
+ * }
124
+ * ```
125
+ */
126
+ export function usePerformance(
127
+ options: UsePerformanceOptions
128
+ ): UsePerformanceReturn {
129
+ const {
130
+ name,
131
+ trackFps = false,
132
+ logInDev = true,
133
+ reportToAnalytics = false,
134
+ slowRenderThreshold = 16, // 60fps = 16.67ms per frame
135
+ } = options;
136
+
137
+ const mountTimeRef = useRef(Date.now());
138
+ const renderCountRef = useRef(0);
139
+ const lastRenderStartRef = useRef(Date.now());
140
+ const marksRef = useRef<Map<string, number>>(new Map());
141
+ const fpsRef = useRef(60);
142
+ const frameCountRef = useRef(0);
143
+ const lastFpsUpdateRef = useRef(Date.now());
144
+
145
+ const [metrics, setMetrics] = useState<PerformanceMetrics>({
146
+ mountTime: 0,
147
+ renderTime: 0,
148
+ renderCount: 0,
149
+ lastRenderDuration: 0,
150
+ fps: 60,
151
+ memoryUsage: null,
152
+ });
153
+
154
+ // Track render count and duration
155
+ useEffect(() => {
156
+ const renderEnd = Date.now();
157
+ const renderDuration = renderEnd - lastRenderStartRef.current;
158
+ renderCountRef.current += 1;
159
+
160
+ // Update metrics
161
+ setMetrics((prev) => ({
162
+ ...prev,
163
+ renderCount: renderCountRef.current,
164
+ lastRenderDuration: renderDuration,
165
+ renderTime:
166
+ renderCountRef.current === 1 ? renderDuration : prev.renderTime,
167
+ }));
168
+
169
+ // Warn on slow render
170
+ if (renderDuration > slowRenderThreshold && IS_DEV && logInDev) {
171
+ console.warn(
172
+ `[Performance] ${name}: Slow render detected (${renderDuration.toFixed(1)}ms)`
173
+ );
174
+ }
175
+
176
+ // Prepare for next render
177
+ lastRenderStartRef.current = Date.now();
178
+ });
179
+
180
+ // Track mount time
181
+ useEffect(() => {
182
+ const mountDuration = Date.now() - mountTimeRef.current;
183
+
184
+ setMetrics((prev) => ({
185
+ ...prev,
186
+ mountTime: mountDuration,
187
+ }));
188
+
189
+ if (IS_DEV && logInDev) {
190
+ console.log(`[Performance] ${name}: Mounted in ${mountDuration}ms`);
191
+ }
192
+
193
+ // Report to analytics
194
+ if (reportToAnalytics && ENABLE_ANALYTICS) {
195
+ analytics.track(AnalyticsEvents.SCREEN_VIEW, {
196
+ screen: name,
197
+ mountTime: mountDuration,
198
+ });
199
+ }
200
+
201
+ return () => {
202
+ if (IS_DEV && logInDev) {
203
+ console.log(
204
+ `[Performance] ${name}: Unmounted after ${renderCountRef.current} renders`
205
+ );
206
+ }
207
+ };
208
+ }, [name, logInDev, reportToAnalytics]);
209
+
210
+ // FPS tracking
211
+ useEffect(() => {
212
+ if (!trackFps) return;
213
+
214
+ let animationFrameId: number;
215
+ let isRunning = true;
216
+
217
+ const trackFrame = () => {
218
+ if (!isRunning) return;
219
+
220
+ frameCountRef.current += 1;
221
+ const now = Date.now();
222
+ const elapsed = now - lastFpsUpdateRef.current;
223
+
224
+ // Update FPS every second
225
+ if (elapsed >= 1000) {
226
+ fpsRef.current = Math.round((frameCountRef.current * 1000) / elapsed);
227
+ frameCountRef.current = 0;
228
+ lastFpsUpdateRef.current = now;
229
+
230
+ setMetrics((prev) => ({
231
+ ...prev,
232
+ fps: fpsRef.current,
233
+ }));
234
+
235
+ // Warn on low FPS
236
+ if (fpsRef.current < 30 && IS_DEV && logInDev) {
237
+ console.warn(
238
+ `[Performance] ${name}: Low FPS detected (${fpsRef.current})`
239
+ );
240
+ }
241
+ }
242
+
243
+ animationFrameId = requestAnimationFrame(trackFrame);
244
+ };
245
+
246
+ animationFrameId = requestAnimationFrame(trackFrame);
247
+
248
+ return () => {
249
+ isRunning = false;
250
+ cancelAnimationFrame(animationFrameId);
251
+ };
252
+ }, [trackFps, name, logInDev]);
253
+
254
+ /**
255
+ * Mark the start of an operation
256
+ */
257
+ const markStart = useCallback((markName: string) => {
258
+ marksRef.current.set(markName, Date.now());
259
+ }, []);
260
+
261
+ /**
262
+ * Mark the end of an operation and get duration
263
+ */
264
+ const markEnd = useCallback(
265
+ (markName: string): number => {
266
+ const start = marksRef.current.get(markName);
267
+ if (!start) {
268
+ console.warn(`[Performance] No start mark found for "${markName}"`);
269
+ return 0;
270
+ }
271
+
272
+ const duration = Date.now() - start;
273
+ marksRef.current.delete(markName);
274
+
275
+ if (IS_DEV && logInDev) {
276
+ console.log(`[Performance] ${name}.${markName}: ${duration}ms`);
277
+ }
278
+
279
+ if (reportToAnalytics && ENABLE_ANALYTICS) {
280
+ analytics.track("Performance Metric", {
281
+ component: name,
282
+ operation: markName,
283
+ duration,
284
+ });
285
+ }
286
+
287
+ return duration;
288
+ },
289
+ [name, logInDev, reportToAnalytics]
290
+ );
291
+
292
+ /**
293
+ * Track a custom metric
294
+ */
295
+ const trackMetric = useCallback(
296
+ (metricName: string, value: number) => {
297
+ if (IS_DEV && logInDev) {
298
+ console.log(`[Performance] ${name}.${metricName}: ${value}`);
299
+ }
300
+
301
+ if (reportToAnalytics && ENABLE_ANALYTICS) {
302
+ analytics.track("Performance Metric", {
303
+ component: name,
304
+ metric: metricName,
305
+ value,
306
+ });
307
+ }
308
+ },
309
+ [name, logInDev, reportToAnalytics]
310
+ );
311
+
312
+ /**
313
+ * Reset all metrics
314
+ */
315
+ const reset = useCallback(() => {
316
+ mountTimeRef.current = Date.now();
317
+ renderCountRef.current = 0;
318
+ lastRenderStartRef.current = Date.now();
319
+ marksRef.current.clear();
320
+
321
+ setMetrics({
322
+ mountTime: 0,
323
+ renderTime: 0,
324
+ renderCount: 0,
325
+ lastRenderDuration: 0,
326
+ fps: 60,
327
+ memoryUsage: null,
328
+ });
329
+ }, []);
330
+
331
+ return {
332
+ metrics,
333
+ markStart,
334
+ markEnd,
335
+ trackMetric,
336
+ reset,
337
+ };
338
+ }
339
+
340
+ // ============================================================================
341
+ // Utility Functions
342
+ // ============================================================================
343
+
344
+ /**
345
+ * Measure the execution time of an async function
346
+ */
347
+ export async function measureAsync<T>(
348
+ name: string,
349
+ fn: () => Promise<T>,
350
+ options?: { log?: boolean; reportToAnalytics?: boolean }
351
+ ): Promise<{ result: T; duration: number }> {
352
+ const { log = IS_DEV, reportToAnalytics = false } = options || {};
353
+
354
+ const start = Date.now();
355
+ const result = await fn();
356
+ const duration = Date.now() - start;
357
+
358
+ if (log) {
359
+ console.log(`[Performance] ${name}: ${duration}ms`);
360
+ }
361
+
362
+ if (reportToAnalytics && ENABLE_ANALYTICS) {
363
+ analytics.track("Performance Metric", {
364
+ operation: name,
365
+ duration,
366
+ });
367
+ }
368
+
369
+ return { result, duration };
370
+ }
371
+
372
+ /**
373
+ * Measure the execution time of a sync function
374
+ */
375
+ export function measureSync<T>(
376
+ name: string,
377
+ fn: () => T,
378
+ options?: { log?: boolean; reportToAnalytics?: boolean }
379
+ ): { result: T; duration: number } {
380
+ const { log = IS_DEV, reportToAnalytics = false } = options || {};
381
+
382
+ const start = Date.now();
383
+ const result = fn();
384
+ const duration = Date.now() - start;
385
+
386
+ if (log) {
387
+ console.log(`[Performance] ${name}: ${duration}ms`);
388
+ }
389
+
390
+ if (reportToAnalytics && ENABLE_ANALYTICS) {
391
+ analytics.track("Performance Metric", {
392
+ operation: name,
393
+ duration,
394
+ });
395
+ }
396
+
397
+ return { result, duration };
398
+ }
399
+
400
+ /**
401
+ * Wait for interactions to complete before executing
402
+ * Useful for deferring heavy operations
403
+ */
404
+ export function runAfterInteractions<T>(fn: () => T | Promise<T>): Promise<T> {
405
+ return new Promise((resolve) => {
406
+ InteractionManager.runAfterInteractions(async () => {
407
+ const result = await fn();
408
+ resolve(result);
409
+ });
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Create a debounced function with performance tracking
415
+ */
416
+ export function createTrackedDebounce<
417
+ T extends (...args: unknown[]) => unknown,
418
+ >(fn: T, delay: number, name: string): T {
419
+ let timeoutId: NodeJS.Timeout;
420
+ let callCount = 0;
421
+
422
+ return ((...args: Parameters<T>) => {
423
+ callCount++;
424
+ clearTimeout(timeoutId);
425
+
426
+ timeoutId = setTimeout(() => {
427
+ if (IS_DEV) {
428
+ console.log(`[Performance] ${name}: Debounced ${callCount} calls to 1`);
429
+ }
430
+ callCount = 0;
431
+ fn(...args);
432
+ }, delay);
433
+ }) as T;
434
+ }