@croacroa/react-native-template 1.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 (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ ReactNode,
7
+ } from "react";
8
+ import { useColorScheme } from "react-native";
9
+ import AsyncStorage from "@react-native-async-storage/async-storage";
10
+
11
+ type ThemeMode = "light" | "dark" | "system";
12
+
13
+ interface ThemeContextType {
14
+ mode: ThemeMode;
15
+ isDark: boolean;
16
+ isLoaded: boolean;
17
+ setMode: (mode: ThemeMode) => void;
18
+ toggleTheme: () => void;
19
+ }
20
+
21
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
22
+
23
+ const THEME_KEY = "theme_mode";
24
+
25
+ export function ThemeProvider({ children }: { children: ReactNode }) {
26
+ const systemColorScheme = useColorScheme();
27
+ const [mode, setModeState] = useState<ThemeMode>("system");
28
+ const [isLoaded, setIsLoaded] = useState(false);
29
+
30
+ useEffect(() => {
31
+ loadStoredTheme();
32
+ }, []);
33
+
34
+ const loadStoredTheme = async () => {
35
+ try {
36
+ const storedMode = await AsyncStorage.getItem(THEME_KEY);
37
+ if (storedMode && ["light", "dark", "system"].includes(storedMode)) {
38
+ setModeState(storedMode as ThemeMode);
39
+ }
40
+ } catch (error) {
41
+ console.error("Failed to load theme:", error);
42
+ } finally {
43
+ setIsLoaded(true);
44
+ }
45
+ };
46
+
47
+ const setMode = async (newMode: ThemeMode) => {
48
+ setModeState(newMode);
49
+ try {
50
+ await AsyncStorage.setItem(THEME_KEY, newMode);
51
+ } catch (error) {
52
+ console.error("Failed to save theme:", error);
53
+ }
54
+ };
55
+
56
+ const toggleTheme = () => {
57
+ const newMode = isDark ? "light" : "dark";
58
+ setMode(newMode);
59
+ };
60
+
61
+ const isDark =
62
+ mode === "system" ? systemColorScheme === "dark" : mode === "dark";
63
+
64
+ return (
65
+ <ThemeContext.Provider
66
+ value={{
67
+ mode,
68
+ isDark,
69
+ isLoaded,
70
+ setMode,
71
+ toggleTheme,
72
+ }}
73
+ >
74
+ {children}
75
+ </ThemeContext.Provider>
76
+ );
77
+ }
78
+
79
+ export function useTheme() {
80
+ const context = useContext(ThemeContext);
81
+ if (context === undefined) {
82
+ throw new Error("useTheme must be used within a ThemeProvider");
83
+ }
84
+ return context;
85
+ }