@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
package/hooks/useMFA.ts CHANGED
@@ -1,499 +1,509 @@
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
+ /**
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 as _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 =
237
+ err instanceof Error ? err.message : "Failed to check MFA status";
238
+ setError(message);
239
+ } finally {
240
+ setIsLoading(false);
241
+ }
242
+ }, []);
243
+
244
+ /**
245
+ * Begin MFA setup
246
+ */
247
+ const beginSetup = useCallback(
248
+ async (method: MFAMethod): Promise<MFASetupData | null> => {
249
+ try {
250
+ setIsLoading(true);
251
+ setError(null);
252
+
253
+ // TODO: Replace with actual API call
254
+ // const response = await api.post<MFASetupData>('/auth/mfa/setup', { method });
255
+ // setPendingSetupSecret(response.secret);
256
+ // return response;
257
+
258
+ // Mock implementation
259
+ const mockSecret = "JBSWY3DPEHPK3PXP"; // Example TOTP secret
260
+ const mockSetupData: MFASetupData = {
261
+ secret: mockSecret,
262
+ qrCodeUrl: `otpauth://totp/YourApp:user@example.com?secret=${mockSecret}&issuer=YourApp`,
263
+ backupCodes: [
264
+ "ABC123DEF",
265
+ "GHI456JKL",
266
+ "MNO789PQR",
267
+ "STU012VWX",
268
+ "YZA345BCD",
269
+ ],
270
+ method,
271
+ };
272
+
273
+ setPendingSetupSecret(mockSecret);
274
+ return mockSetupData;
275
+ } catch (err) {
276
+ const message =
277
+ err instanceof Error ? err.message : "Failed to begin MFA setup";
278
+ setError(message);
279
+ toast.error("Setup failed", message);
280
+ return null;
281
+ } finally {
282
+ setIsLoading(false);
283
+ }
284
+ },
285
+ []
286
+ );
287
+
288
+ /**
289
+ * Complete MFA setup by verifying the first code
290
+ */
291
+ const completeSetup = useCallback(
292
+ async (code: string): Promise<boolean> => {
293
+ if (!pendingSetupSecret) {
294
+ setError("No pending setup. Please start setup first.");
295
+ return false;
296
+ }
297
+
298
+ try {
299
+ setIsLoading(true);
300
+ setError(null);
301
+
302
+ // TODO: Replace with actual API call
303
+ // await api.post('/auth/mfa/verify-setup', {
304
+ // secret: pendingSetupSecret,
305
+ // code,
306
+ // });
307
+
308
+ // Mock implementation - verify code format (6 digits)
309
+ if (!/^\d{6}$/.test(code)) {
310
+ throw new Error("Invalid code format. Please enter 6 digits.");
311
+ }
312
+
313
+ // Update state
314
+ const newState: MFAState = {
315
+ isEnabled: true,
316
+ method: "totp",
317
+ isRequired: false,
318
+ isPendingVerification: false,
319
+ };
320
+ setState(newState);
321
+ await storage.set(MFA_STORAGE_KEY, newState);
322
+ setPendingSetupSecret(null);
323
+
324
+ toast.success("MFA enabled", "Two-factor authentication is now active");
325
+ return true;
326
+ } catch (err) {
327
+ const message =
328
+ err instanceof Error ? err.message : "Verification failed";
329
+ setError(message);
330
+ toast.error("Verification failed", message);
331
+ return false;
332
+ } finally {
333
+ setIsLoading(false);
334
+ }
335
+ },
336
+ [pendingSetupSecret]
337
+ );
338
+
339
+ /**
340
+ * Verify an MFA code during login
341
+ */
342
+ const verifyCode = useCallback(async (code: string): Promise<boolean> => {
343
+ try {
344
+ setIsLoading(true);
345
+ setError(null);
346
+
347
+ // TODO: Replace with actual API call
348
+ // await api.post('/auth/mfa/verify', { code });
349
+
350
+ // Mock implementation
351
+ if (!/^\d{6}$/.test(code)) {
352
+ throw new Error("Invalid code format");
353
+ }
354
+
355
+ setState((prev) => ({
356
+ ...prev,
357
+ isPendingVerification: false,
358
+ isRequired: false,
359
+ }));
360
+
361
+ toast.success("Verified", "Authentication successful");
362
+ return true;
363
+ } catch (err) {
364
+ const message =
365
+ err instanceof Error ? err.message : "Verification failed";
366
+ setError(message);
367
+ toast.error("Invalid code", "Please try again");
368
+ return false;
369
+ } finally {
370
+ setIsLoading(false);
371
+ }
372
+ }, []);
373
+
374
+ /**
375
+ * Send a verification code (for SMS/email methods)
376
+ */
377
+ const sendCode = useCallback(async (): Promise<boolean> => {
378
+ try {
379
+ setIsLoading(true);
380
+ setError(null);
381
+
382
+ // TODO: Replace with actual API call
383
+ // await api.post('/auth/mfa/send-code');
384
+
385
+ toast.success("Code sent", "Check your phone or email");
386
+ return true;
387
+ } catch (err) {
388
+ const message =
389
+ err instanceof Error ? err.message : "Failed to send code";
390
+ setError(message);
391
+ toast.error("Failed to send code", message);
392
+ return false;
393
+ } finally {
394
+ setIsLoading(false);
395
+ }
396
+ }, []);
397
+
398
+ /**
399
+ * Disable MFA
400
+ */
401
+ const disable = useCallback(async (code: string): Promise<boolean> => {
402
+ try {
403
+ setIsLoading(true);
404
+ setError(null);
405
+
406
+ // TODO: Replace with actual API call
407
+ // await api.post('/auth/mfa/disable', { code });
408
+
409
+ // Mock implementation
410
+ if (!/^\d{6}$/.test(code)) {
411
+ throw new Error("Invalid code format");
412
+ }
413
+
414
+ const newState: MFAState = {
415
+ isEnabled: false,
416
+ method: null,
417
+ isRequired: false,
418
+ isPendingVerification: false,
419
+ };
420
+ setState(newState);
421
+ await storage.set(MFA_STORAGE_KEY, newState);
422
+
423
+ toast.success(
424
+ "MFA disabled",
425
+ "Two-factor authentication has been turned off"
426
+ );
427
+ return true;
428
+ } catch (err) {
429
+ const message =
430
+ err instanceof Error ? err.message : "Failed to disable MFA";
431
+ setError(message);
432
+ toast.error("Failed to disable MFA", message);
433
+ return false;
434
+ } finally {
435
+ setIsLoading(false);
436
+ }
437
+ }, []);
438
+
439
+ /**
440
+ * Use a backup code
441
+ */
442
+ const useBackupCode = useCallback(
443
+ async (backupCode: string): Promise<boolean> => {
444
+ try {
445
+ setIsLoading(true);
446
+ setError(null);
447
+
448
+ // TODO: Replace with actual API call
449
+ // await api.post('/auth/mfa/backup-code', { code: backupCode });
450
+
451
+ // Mock implementation
452
+ if (!/^[A-Z0-9]{9}$/.test(backupCode.toUpperCase())) {
453
+ throw new Error("Invalid backup code format");
454
+ }
455
+
456
+ setState((prev) => ({
457
+ ...prev,
458
+ isPendingVerification: false,
459
+ isRequired: false,
460
+ }));
461
+
462
+ toast.success("Backup code accepted", "You are now logged in");
463
+ return true;
464
+ } catch (err) {
465
+ const message =
466
+ err instanceof Error ? err.message : "Invalid backup code";
467
+ setError(message);
468
+ toast.error("Invalid backup code", message);
469
+ return false;
470
+ } finally {
471
+ setIsLoading(false);
472
+ }
473
+ },
474
+ []
475
+ );
476
+
477
+ /**
478
+ * Clear error state
479
+ */
480
+ const clearError = useCallback(() => {
481
+ setError(null);
482
+ }, []);
483
+
484
+ return {
485
+ state,
486
+ isLoading,
487
+ error,
488
+ beginSetup,
489
+ completeSetup,
490
+ verifyCode,
491
+ sendCode,
492
+ disable,
493
+ useBackupCode,
494
+ checkStatus,
495
+ clearError,
496
+ };
497
+ }
498
+
499
+ /**
500
+ * Generate a TOTP code from a secret (client-side)
501
+ * Note: For production, use a proper TOTP library
502
+ */
503
+ export function generateTOTP(_secret: string): string {
504
+ // This is a placeholder - in production use a library like 'otpauth'
505
+ // import { TOTP } from 'otpauth';
506
+ // const totp = new TOTP({ secret: secret });
507
+ // return totp.generate();
508
+ return "000000";
509
+ }