@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
@@ -0,0 +1,499 @@
1
+ /**
2
+ * @fileoverview Multi-Factor Authentication (MFA) hook
3
+ * Provides OTP-based two-factor authentication functionality.
4
+ * @module hooks/useMFA
5
+ */
6
+
7
+ import { useState, useCallback } from "react";
8
+ import { api } from "@/services/api";
9
+ import { storage } from "@/services/storage";
10
+ import { toast } from "@/utils/toast";
11
+
12
+ /**
13
+ * MFA methods supported by the app
14
+ */
15
+ export type MFAMethod = "totp" | "sms" | "email";
16
+
17
+ /**
18
+ * MFA setup data returned when enabling MFA
19
+ */
20
+ export interface MFASetupData {
21
+ /** Secret key for TOTP apps (e.g., Google Authenticator) */
22
+ secret: string;
23
+ /** QR code URL for easy setup */
24
+ qrCodeUrl: string;
25
+ /** Backup codes for account recovery */
26
+ backupCodes: string[];
27
+ /** Selected MFA method */
28
+ method: MFAMethod;
29
+ }
30
+
31
+ /**
32
+ * MFA state interface
33
+ */
34
+ interface MFAState {
35
+ /** Whether MFA is enabled for the user */
36
+ isEnabled: boolean;
37
+ /** Active MFA method */
38
+ method: MFAMethod | null;
39
+ /** Whether MFA is required for current session */
40
+ isRequired: boolean;
41
+ /** Whether currently in MFA verification flow */
42
+ isPendingVerification: boolean;
43
+ }
44
+
45
+ /**
46
+ * Return type for useMFA hook
47
+ */
48
+ interface UseMFAReturn {
49
+ /** Current MFA state */
50
+ state: MFAState;
51
+ /** Whether any MFA operation is in progress */
52
+ isLoading: boolean;
53
+ /** Error message if any */
54
+ error: string | null;
55
+
56
+ /**
57
+ * Begin MFA setup process
58
+ * @param method - The MFA method to set up
59
+ * @returns Setup data including secret and QR code
60
+ */
61
+ beginSetup: (method: MFAMethod) => Promise<MFASetupData | null>;
62
+
63
+ /**
64
+ * Complete MFA setup by verifying the first code
65
+ * @param code - The verification code from authenticator app
66
+ * @returns Whether setup was successful
67
+ */
68
+ completeSetup: (code: string) => Promise<boolean>;
69
+
70
+ /**
71
+ * Verify an MFA code during login
72
+ * @param code - The verification code
73
+ * @returns Whether verification was successful
74
+ */
75
+ verifyCode: (code: string) => Promise<boolean>;
76
+
77
+ /**
78
+ * Send a verification code (for SMS/email methods)
79
+ * @returns Whether code was sent successfully
80
+ */
81
+ sendCode: () => Promise<boolean>;
82
+
83
+ /**
84
+ * Disable MFA for the user
85
+ * @param code - Current verification code to confirm
86
+ * @returns Whether MFA was disabled
87
+ */
88
+ disable: (code: string) => Promise<boolean>;
89
+
90
+ /**
91
+ * Use a backup code for authentication
92
+ * @param backupCode - One of the backup codes
93
+ * @returns Whether the backup code was valid
94
+ */
95
+ useBackupCode: (backupCode: string) => Promise<boolean>;
96
+
97
+ /**
98
+ * Check if MFA is enabled for current user
99
+ */
100
+ checkStatus: () => Promise<void>;
101
+
102
+ /**
103
+ * Clear error state
104
+ */
105
+ clearError: () => void;
106
+ }
107
+
108
+ const MFA_STORAGE_KEY = "mfa_state";
109
+
110
+ /**
111
+ * Hook for managing Multi-Factor Authentication.
112
+ *
113
+ * Supports:
114
+ * - TOTP (Time-based One-Time Password) with authenticator apps
115
+ * - SMS verification codes
116
+ * - Email verification codes
117
+ * - Backup codes for account recovery
118
+ *
119
+ * @example
120
+ * ```tsx
121
+ * function MFASetupScreen() {
122
+ * const { state, beginSetup, completeSetup, isLoading } = useMFA();
123
+ * const [setupData, setSetupData] = useState<MFASetupData | null>(null);
124
+ * const [code, setCode] = useState('');
125
+ *
126
+ * const handleSetup = async () => {
127
+ * const data = await beginSetup('totp');
128
+ * if (data) {
129
+ * setSetupData(data);
130
+ * // Show QR code for user to scan
131
+ * }
132
+ * };
133
+ *
134
+ * const handleVerify = async () => {
135
+ * const success = await completeSetup(code);
136
+ * if (success) {
137
+ * // MFA is now enabled
138
+ * // Save backup codes securely
139
+ * }
140
+ * };
141
+ *
142
+ * if (state.isEnabled) {
143
+ * return <Text>MFA is enabled</Text>;
144
+ * }
145
+ *
146
+ * return (
147
+ * <View>
148
+ * {!setupData ? (
149
+ * <Button onPress={handleSetup} isLoading={isLoading}>
150
+ * Enable 2FA
151
+ * </Button>
152
+ * ) : (
153
+ * <View>
154
+ * <QRCode value={setupData.qrCodeUrl} />
155
+ * <Input
156
+ * value={code}
157
+ * onChangeText={setCode}
158
+ * placeholder="Enter verification code"
159
+ * keyboardType="number-pad"
160
+ * />
161
+ * <Button onPress={handleVerify} isLoading={isLoading}>
162
+ * Verify
163
+ * </Button>
164
+ * </View>
165
+ * )}
166
+ * </View>
167
+ * );
168
+ * }
169
+ * ```
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * // During login when MFA is required
174
+ * function MFAVerificationScreen() {
175
+ * const { verifyCode, sendCode, useBackupCode, isLoading } = useMFA();
176
+ * const [code, setCode] = useState('');
177
+ *
178
+ * const handleVerify = async () => {
179
+ * const success = await verifyCode(code);
180
+ * if (success) {
181
+ * // Proceed with login
182
+ * router.replace('/(auth)/home');
183
+ * }
184
+ * };
185
+ *
186
+ * return (
187
+ * <View>
188
+ * <Text>Enter your verification code</Text>
189
+ * <OTPInput value={code} onChange={setCode} />
190
+ * <Button onPress={handleVerify} isLoading={isLoading}>
191
+ * Verify
192
+ * </Button>
193
+ * <Button variant="ghost" onPress={() => setShowBackupInput(true)}>
194
+ * Use backup code
195
+ * </Button>
196
+ * </View>
197
+ * );
198
+ * }
199
+ * ```
200
+ */
201
+ export function useMFA(): UseMFAReturn {
202
+ const [state, setState] = useState<MFAState>({
203
+ isEnabled: false,
204
+ method: null,
205
+ isRequired: false,
206
+ isPendingVerification: false,
207
+ });
208
+ const [isLoading, setIsLoading] = useState(false);
209
+ const [error, setError] = useState<string | null>(null);
210
+ const [pendingSetupSecret, setPendingSetupSecret] = useState<string | null>(
211
+ null
212
+ );
213
+
214
+ /**
215
+ * Check MFA status for current user
216
+ */
217
+ const checkStatus = useCallback(async () => {
218
+ try {
219
+ setIsLoading(true);
220
+ setError(null);
221
+
222
+ // Try to get cached status first
223
+ const cached = await storage.get<MFAState>(MFA_STORAGE_KEY);
224
+ if (cached) {
225
+ setState(cached);
226
+ }
227
+
228
+ // TODO: Replace with actual API call
229
+ // const response = await api.get<{ mfa: MFAState }>('/auth/mfa/status');
230
+ // setState(response.mfa);
231
+ // await storage.set(MFA_STORAGE_KEY, response.mfa);
232
+
233
+ // Mock implementation
234
+ // In real implementation, this would fetch from your API
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : "Failed to check MFA status";
237
+ setError(message);
238
+ } finally {
239
+ setIsLoading(false);
240
+ }
241
+ }, []);
242
+
243
+ /**
244
+ * Begin MFA setup
245
+ */
246
+ const beginSetup = useCallback(
247
+ async (method: MFAMethod): Promise<MFASetupData | null> => {
248
+ try {
249
+ setIsLoading(true);
250
+ setError(null);
251
+
252
+ // TODO: Replace with actual API call
253
+ // const response = await api.post<MFASetupData>('/auth/mfa/setup', { method });
254
+ // setPendingSetupSecret(response.secret);
255
+ // return response;
256
+
257
+ // Mock implementation
258
+ const mockSecret = "JBSWY3DPEHPK3PXP"; // Example TOTP secret
259
+ const mockSetupData: MFASetupData = {
260
+ secret: mockSecret,
261
+ qrCodeUrl: `otpauth://totp/YourApp:user@example.com?secret=${mockSecret}&issuer=YourApp`,
262
+ backupCodes: [
263
+ "ABC123DEF",
264
+ "GHI456JKL",
265
+ "MNO789PQR",
266
+ "STU012VWX",
267
+ "YZA345BCD",
268
+ ],
269
+ method,
270
+ };
271
+
272
+ setPendingSetupSecret(mockSecret);
273
+ return mockSetupData;
274
+ } catch (err) {
275
+ const message = err instanceof Error ? err.message : "Failed to begin MFA setup";
276
+ setError(message);
277
+ toast.error("Setup failed", message);
278
+ return null;
279
+ } finally {
280
+ setIsLoading(false);
281
+ }
282
+ },
283
+ []
284
+ );
285
+
286
+ /**
287
+ * Complete MFA setup by verifying the first code
288
+ */
289
+ const completeSetup = useCallback(
290
+ async (code: string): Promise<boolean> => {
291
+ if (!pendingSetupSecret) {
292
+ setError("No pending setup. Please start setup first.");
293
+ return false;
294
+ }
295
+
296
+ try {
297
+ setIsLoading(true);
298
+ setError(null);
299
+
300
+ // TODO: Replace with actual API call
301
+ // await api.post('/auth/mfa/verify-setup', {
302
+ // secret: pendingSetupSecret,
303
+ // code,
304
+ // });
305
+
306
+ // Mock implementation - verify code format (6 digits)
307
+ if (!/^\d{6}$/.test(code)) {
308
+ throw new Error("Invalid code format. Please enter 6 digits.");
309
+ }
310
+
311
+ // Update state
312
+ const newState: MFAState = {
313
+ isEnabled: true,
314
+ method: "totp",
315
+ isRequired: false,
316
+ isPendingVerification: false,
317
+ };
318
+ setState(newState);
319
+ await storage.set(MFA_STORAGE_KEY, newState);
320
+ setPendingSetupSecret(null);
321
+
322
+ toast.success("MFA enabled", "Two-factor authentication is now active");
323
+ return true;
324
+ } catch (err) {
325
+ const message = err instanceof Error ? err.message : "Verification failed";
326
+ setError(message);
327
+ toast.error("Verification failed", message);
328
+ return false;
329
+ } finally {
330
+ setIsLoading(false);
331
+ }
332
+ },
333
+ [pendingSetupSecret]
334
+ );
335
+
336
+ /**
337
+ * Verify an MFA code during login
338
+ */
339
+ const verifyCode = useCallback(async (code: string): Promise<boolean> => {
340
+ try {
341
+ setIsLoading(true);
342
+ setError(null);
343
+
344
+ // TODO: Replace with actual API call
345
+ // await api.post('/auth/mfa/verify', { code });
346
+
347
+ // Mock implementation
348
+ if (!/^\d{6}$/.test(code)) {
349
+ throw new Error("Invalid code format");
350
+ }
351
+
352
+ setState((prev) => ({
353
+ ...prev,
354
+ isPendingVerification: false,
355
+ isRequired: false,
356
+ }));
357
+
358
+ toast.success("Verified", "Authentication successful");
359
+ return true;
360
+ } catch (err) {
361
+ const message = err instanceof Error ? err.message : "Verification failed";
362
+ setError(message);
363
+ toast.error("Invalid code", "Please try again");
364
+ return false;
365
+ } finally {
366
+ setIsLoading(false);
367
+ }
368
+ }, []);
369
+
370
+ /**
371
+ * Send a verification code (for SMS/email methods)
372
+ */
373
+ const sendCode = useCallback(async (): Promise<boolean> => {
374
+ try {
375
+ setIsLoading(true);
376
+ setError(null);
377
+
378
+ // TODO: Replace with actual API call
379
+ // await api.post('/auth/mfa/send-code');
380
+
381
+ toast.success("Code sent", "Check your phone or email");
382
+ return true;
383
+ } catch (err) {
384
+ const message = err instanceof Error ? err.message : "Failed to send code";
385
+ setError(message);
386
+ toast.error("Failed to send code", message);
387
+ return false;
388
+ } finally {
389
+ setIsLoading(false);
390
+ }
391
+ }, []);
392
+
393
+ /**
394
+ * Disable MFA
395
+ */
396
+ const disable = useCallback(async (code: string): Promise<boolean> => {
397
+ try {
398
+ setIsLoading(true);
399
+ setError(null);
400
+
401
+ // TODO: Replace with actual API call
402
+ // await api.post('/auth/mfa/disable', { code });
403
+
404
+ // Mock implementation
405
+ if (!/^\d{6}$/.test(code)) {
406
+ throw new Error("Invalid code format");
407
+ }
408
+
409
+ const newState: MFAState = {
410
+ isEnabled: false,
411
+ method: null,
412
+ isRequired: false,
413
+ isPendingVerification: false,
414
+ };
415
+ setState(newState);
416
+ await storage.set(MFA_STORAGE_KEY, newState);
417
+
418
+ toast.success("MFA disabled", "Two-factor authentication has been turned off");
419
+ return true;
420
+ } catch (err) {
421
+ const message = err instanceof Error ? err.message : "Failed to disable MFA";
422
+ setError(message);
423
+ toast.error("Failed to disable MFA", message);
424
+ return false;
425
+ } finally {
426
+ setIsLoading(false);
427
+ }
428
+ }, []);
429
+
430
+ /**
431
+ * Use a backup code
432
+ */
433
+ const useBackupCode = useCallback(
434
+ async (backupCode: string): Promise<boolean> => {
435
+ try {
436
+ setIsLoading(true);
437
+ setError(null);
438
+
439
+ // TODO: Replace with actual API call
440
+ // await api.post('/auth/mfa/backup-code', { code: backupCode });
441
+
442
+ // Mock implementation
443
+ if (!/^[A-Z0-9]{9}$/.test(backupCode.toUpperCase())) {
444
+ throw new Error("Invalid backup code format");
445
+ }
446
+
447
+ setState((prev) => ({
448
+ ...prev,
449
+ isPendingVerification: false,
450
+ isRequired: false,
451
+ }));
452
+
453
+ toast.success("Backup code accepted", "You are now logged in");
454
+ return true;
455
+ } catch (err) {
456
+ const message = err instanceof Error ? err.message : "Invalid backup code";
457
+ setError(message);
458
+ toast.error("Invalid backup code", message);
459
+ return false;
460
+ } finally {
461
+ setIsLoading(false);
462
+ }
463
+ },
464
+ []
465
+ );
466
+
467
+ /**
468
+ * Clear error state
469
+ */
470
+ const clearError = useCallback(() => {
471
+ setError(null);
472
+ }, []);
473
+
474
+ return {
475
+ state,
476
+ isLoading,
477
+ error,
478
+ beginSetup,
479
+ completeSetup,
480
+ verifyCode,
481
+ sendCode,
482
+ disable,
483
+ useBackupCode,
484
+ checkStatus,
485
+ clearError,
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Generate a TOTP code from a secret (client-side)
491
+ * Note: For production, use a proper TOTP library
492
+ */
493
+ export function generateTOTP(_secret: string): string {
494
+ // This is a placeholder - in production use a library like 'otpauth'
495
+ // import { TOTP } from 'otpauth';
496
+ // const totp = new TOTP({ secret: secret });
497
+ // return totp.generate();
498
+ return "000000";
499
+ }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @fileoverview Push notification handling with Expo Notifications
3
+ * Provides hooks for registering, receiving, and managing push notifications.
4
+ * @module hooks/useNotifications
5
+ */
6
+
1
7
  import { useEffect, useRef } from "react";
2
8
  import { Platform } from "react-native";
3
9
  import * as Notifications from "expo-notifications";
@@ -15,6 +21,39 @@ Notifications.setNotificationHandler({
15
21
  }),
16
22
  });
17
23
 
24
+ /**
25
+ * Hook for managing push notifications.
26
+ * Handles registration, receiving, and responding to notifications.
27
+ *
28
+ * @returns Object with notification management functions
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function App() {
33
+ * const {
34
+ * registerForPushNotifications,
35
+ * scheduleLocalNotification,
36
+ * setBadgeCount,
37
+ * } = useNotifications();
38
+ *
39
+ * useEffect(() => {
40
+ * // Register for push notifications on mount
41
+ * registerForPushNotifications();
42
+ * }, []);
43
+ *
44
+ * const handleReminder = () => {
45
+ * scheduleLocalNotification(
46
+ * 'Reminder',
47
+ * 'Don\'t forget to check your tasks!',
48
+ * { screen: 'tasks' },
49
+ * { seconds: 60 } // Trigger in 1 minute
50
+ * );
51
+ * };
52
+ *
53
+ * return <Button onPress={handleReminder}>Set Reminder</Button>;
54
+ * }
55
+ * ```
56
+ */
18
57
  export function useNotifications() {
19
58
  const notificationListener = useRef<Notifications.Subscription>();
20
59
  const responseListener = useRef<Notifications.Subscription>();
@@ -1,9 +1,22 @@
1
- import { useEffect, useState } from "react";
1
+ /**
2
+ * @fileoverview Offline detection and handling
3
+ * Provides hooks for tracking network connectivity with React Query integration.
4
+ * @module hooks/useOffline
5
+ */
6
+
7
+ import { useEffect, useState, useCallback } from "react";
2
8
  import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
3
9
  import { onlineManager } from "@tanstack/react-query";
4
10
  import { toast } from "@/utils/toast";
5
11
 
12
+ /**
13
+ * Options for the useOffline hook
14
+ */
6
15
  interface UseOfflineOptions {
16
+ /**
17
+ * Whether to show toast notifications when connectivity changes
18
+ * @default true
19
+ */
7
20
  showToast?: boolean;
8
21
  }
9
22
 
@@ -54,16 +67,57 @@ export function useOffline(options: UseOfflineOptions = {}) {
54
67
  }
55
68
 
56
69
  /**
57
- * Hook for pending mutations count
58
- * Useful to show sync indicator
70
+ * Hook for tracking pending mutations count.
71
+ * Integrates with the backgroundSync mutation queue.
72
+ *
73
+ * @param pollingInterval - How often to check the queue (default: 5000ms)
74
+ * @returns Object with pendingCount, hasPending flag, and refresh function
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * function SyncIndicator() {
79
+ * const { hasPending, pendingCount } = usePendingMutations();
80
+ *
81
+ * if (!hasPending) return null;
82
+ *
83
+ * return (
84
+ * <Badge>
85
+ * {pendingCount} pending
86
+ * </Badge>
87
+ * );
88
+ * }
89
+ * ```
59
90
  */
60
- export function usePendingMutations() {
91
+ export function usePendingMutations(pollingInterval = 5000) {
61
92
  const [pendingCount, setPendingCount] = useState(0);
93
+ const [isLoading, setIsLoading] = useState(true);
94
+
95
+ const refresh = useCallback(async () => {
96
+ try {
97
+ const { getMutationQueue } = await import("@/services/backgroundSync");
98
+ const queue = await getMutationQueue();
99
+ setPendingCount(queue.length);
100
+ } catch (error) {
101
+ console.error("[usePendingMutations] Failed to get queue:", error);
102
+ } finally {
103
+ setIsLoading(false);
104
+ }
105
+ }, []);
106
+
107
+ useEffect(() => {
108
+ // Initial fetch
109
+ refresh();
110
+
111
+ // Poll for updates
112
+ const interval = setInterval(refresh, pollingInterval);
113
+
114
+ return () => clearInterval(interval);
115
+ }, [refresh, pollingInterval]);
62
116
 
63
- // This would need to be integrated with mutation cache
64
- // For now, return 0 as a placeholder
65
117
  return {
66
118
  pendingCount,
67
119
  hasPending: pendingCount > 0,
120
+ isLoading,
121
+ refresh,
68
122
  };
69
123
  }