@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.
- package/.github/workflows/ci.yml +187 -184
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/CHANGELOG.md +106 -106
- package/CONTRIBUTING.md +377 -377
- package/README.md +399 -399
- package/__tests__/components/snapshots.test.tsx +131 -0
- package/__tests__/integration/auth-api.test.tsx +227 -0
- package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
- package/app/(public)/onboarding.tsx +5 -5
- package/app.config.ts +45 -2
- package/assets/images/.gitkeep +7 -7
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/SuspenseBoundary.tsx +357 -0
- package/components/providers/index.ts +21 -0
- package/components/ui/Avatar.tsx +316 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Select.tsx +240 -240
- package/components/ui/VirtualizedList.tsx +285 -0
- package/components/ui/index.ts +23 -18
- package/constants/config.ts +97 -54
- 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/hooks/index.ts +27 -25
- package/hooks/useApi.ts +102 -5
- package/hooks/useAuth.tsx +82 -0
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useMFA.ts +499 -0
- package/hooks/useNotifications.ts +39 -0
- package/hooks/useOffline.ts +60 -6
- package/hooks/usePerformance.ts +434 -434
- package/hooks/useTheme.tsx +76 -0
- package/hooks/useUpdates.ts +358 -358
- package/i18n/index.ts +194 -77
- package/i18n/locales/ar.json +101 -0
- package/i18n/locales/de.json +101 -0
- package/i18n/locales/en.json +101 -101
- package/i18n/locales/es.json +101 -0
- package/i18n/locales/fr.json +101 -101
- package/jest.config.js +4 -4
- 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 -0
- package/maestro/flows/mfa-setup.yaml +86 -0
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -0
- package/maestro/flows/offline-sync.yaml +128 -0
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +175 -170
- package/services/analytics.ts +428 -428
- package/services/api.ts +340 -340
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +626 -0
- package/services/index.ts +54 -22
- package/services/security.ts +286 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- package/utils/validation.ts +2 -1
- package/utils/withAccessibility.tsx +272 -0
package/hooks/useMFA.ts
ADDED
|
@@ -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>();
|
package/hooks/useOffline.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
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
|
}
|