@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.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -21
- package/README.md +446 -402
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -418
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -30
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -40
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +5 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -375
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -176
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- 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 =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
toast.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|