@croacroa/react-native-template 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +13 -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 +32 -2
- 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 +229 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- package/utils/withAccessibility.tsx +272 -0
package/hooks/useBiometrics.ts
CHANGED
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import * as LocalAuthentication from "expo-local-authentication";
|
|
3
|
-
import { storage } from "@/services/storage";
|
|
4
|
-
import { ENABLE_BIOMETRIC_AUTH } from "@/constants/config";
|
|
5
|
-
|
|
6
|
-
// Storage key for biometric preference
|
|
7
|
-
const BIOMETRIC_ENABLED_KEY = "biometric_auth_enabled";
|
|
8
|
-
|
|
9
|
-
export type BiometricType = "fingerprint" | "face" | "iris" | "none";
|
|
10
|
-
|
|
11
|
-
interface BiometricCapabilities {
|
|
12
|
-
/**
|
|
13
|
-
* Whether biometric authentication is available on this device
|
|
14
|
-
*/
|
|
15
|
-
isAvailable: boolean;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Whether biometrics are enrolled on the device
|
|
19
|
-
*/
|
|
20
|
-
isEnrolled: boolean;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The type of biometric authentication available
|
|
24
|
-
*/
|
|
25
|
-
biometricType: BiometricType;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Security level of the biometric hardware
|
|
29
|
-
*/
|
|
30
|
-
securityLevel: LocalAuthentication.SecurityLevel;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface UseBiometricsReturn {
|
|
34
|
-
/**
|
|
35
|
-
* Device biometric capabilities
|
|
36
|
-
*/
|
|
37
|
-
capabilities: BiometricCapabilities;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Whether biometric auth is enabled by the user
|
|
41
|
-
*/
|
|
42
|
-
isEnabled: boolean;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Whether the hook is still loading
|
|
46
|
-
*/
|
|
47
|
-
isLoading: boolean;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Authenticate using biometrics
|
|
51
|
-
* @returns true if authentication succeeded
|
|
52
|
-
*/
|
|
53
|
-
authenticate: (options?: AuthenticateOptions) => Promise<boolean>;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Enable biometric authentication for the user
|
|
57
|
-
*/
|
|
58
|
-
enable: () => Promise<boolean>;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Disable biometric authentication for the user
|
|
62
|
-
*/
|
|
63
|
-
disable: () => Promise<void>;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Toggle biometric authentication
|
|
67
|
-
*/
|
|
68
|
-
toggle: () => Promise<boolean>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface AuthenticateOptions {
|
|
72
|
-
/**
|
|
73
|
-
* Message to display in the authentication prompt
|
|
74
|
-
*/
|
|
75
|
-
promptMessage?: string;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Message for the fallback button (e.g., "Use passcode")
|
|
79
|
-
*/
|
|
80
|
-
fallbackLabel?: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Whether to allow device passcode as fallback
|
|
84
|
-
* @default true
|
|
85
|
-
*/
|
|
86
|
-
allowDeviceCredentials?: boolean;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Cancel button label
|
|
90
|
-
*/
|
|
91
|
-
cancelLabel?: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the biometric type from the available types
|
|
96
|
-
*/
|
|
97
|
-
function getBiometricType(
|
|
98
|
-
types: LocalAuthentication.AuthenticationType[]
|
|
99
|
-
): BiometricType {
|
|
100
|
-
if (
|
|
101
|
-
types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)
|
|
102
|
-
) {
|
|
103
|
-
return "face";
|
|
104
|
-
}
|
|
105
|
-
if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
|
|
106
|
-
return "fingerprint";
|
|
107
|
-
}
|
|
108
|
-
if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) {
|
|
109
|
-
return "iris";
|
|
110
|
-
}
|
|
111
|
-
return "none";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Hook for handling biometric authentication
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```tsx
|
|
119
|
-
* function LoginScreen() {
|
|
120
|
-
* const {
|
|
121
|
-
* capabilities,
|
|
122
|
-
* isEnabled,
|
|
123
|
-
* authenticate,
|
|
124
|
-
* enable,
|
|
125
|
-
* disable
|
|
126
|
-
* } = useBiometrics();
|
|
127
|
-
*
|
|
128
|
-
* const handleBiometricLogin = async () => {
|
|
129
|
-
* const success = await authenticate({
|
|
130
|
-
* promptMessage: 'Authenticate to sign in',
|
|
131
|
-
* });
|
|
132
|
-
*
|
|
133
|
-
* if (success) {
|
|
134
|
-
* // Proceed with login
|
|
135
|
-
* }
|
|
136
|
-
* };
|
|
137
|
-
*
|
|
138
|
-
* if (!capabilities.isAvailable) {
|
|
139
|
-
* return null;
|
|
140
|
-
* }
|
|
141
|
-
*
|
|
142
|
-
* return (
|
|
143
|
-
* <Button onPress={handleBiometricLogin}>
|
|
144
|
-
* Sign in with {capabilities.biometricType === 'face' ? 'Face ID' : 'Touch ID'}
|
|
145
|
-
* </Button>
|
|
146
|
-
* );
|
|
147
|
-
* }
|
|
148
|
-
* ```
|
|
149
|
-
*/
|
|
150
|
-
export function useBiometrics(): UseBiometricsReturn {
|
|
151
|
-
const [capabilities, setCapabilities] = useState<BiometricCapabilities>({
|
|
152
|
-
isAvailable: false,
|
|
153
|
-
isEnrolled: false,
|
|
154
|
-
biometricType: "none",
|
|
155
|
-
securityLevel: LocalAuthentication.SecurityLevel.NONE,
|
|
156
|
-
});
|
|
157
|
-
const [isEnabled, setIsEnabled] = useState(false);
|
|
158
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
159
|
-
|
|
160
|
-
// Check biometric capabilities on mount
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
async function checkCapabilities() {
|
|
163
|
-
try {
|
|
164
|
-
// Check if feature flag is enabled
|
|
165
|
-
if (!ENABLE_BIOMETRIC_AUTH) {
|
|
166
|
-
setIsLoading(false);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const [hasHardware, isEnrolled, supportedTypes, securityLevel] =
|
|
171
|
-
await Promise.all([
|
|
172
|
-
LocalAuthentication.hasHardwareAsync(),
|
|
173
|
-
LocalAuthentication.isEnrolledAsync(),
|
|
174
|
-
LocalAuthentication.supportedAuthenticationTypesAsync(),
|
|
175
|
-
LocalAuthentication.getEnrolledLevelAsync(),
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
setCapabilities({
|
|
179
|
-
isAvailable: hasHardware,
|
|
180
|
-
isEnrolled,
|
|
181
|
-
biometricType: getBiometricType(supportedTypes),
|
|
182
|
-
securityLevel,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Load user preference
|
|
186
|
-
if (hasHardware && isEnrolled) {
|
|
187
|
-
const enabled = await storage.get<boolean>(BIOMETRIC_ENABLED_KEY);
|
|
188
|
-
setIsEnabled(enabled ?? false);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.error("Failed to check biometric capabilities:", error);
|
|
192
|
-
} finally {
|
|
193
|
-
setIsLoading(false);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
checkCapabilities();
|
|
198
|
-
}, []);
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Authenticate using biometrics
|
|
202
|
-
*/
|
|
203
|
-
const authenticate = useCallback(
|
|
204
|
-
async (options: AuthenticateOptions = {}): Promise<boolean> => {
|
|
205
|
-
if (!capabilities.isAvailable || !capabilities.isEnrolled) {
|
|
206
|
-
console.warn("Biometric authentication not available");
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const result = await LocalAuthentication.authenticateAsync({
|
|
212
|
-
promptMessage: options.promptMessage || "Authenticate to continue",
|
|
213
|
-
fallbackLabel: options.fallbackLabel || "Use passcode",
|
|
214
|
-
cancelLabel: options.cancelLabel || "Cancel",
|
|
215
|
-
disableDeviceFallback: options.allowDeviceCredentials === false,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
return result.success;
|
|
219
|
-
} catch (error) {
|
|
220
|
-
console.error("Biometric authentication error:", error);
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
[capabilities]
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Enable biometric authentication
|
|
229
|
-
* Requires successful authentication first
|
|
230
|
-
*/
|
|
231
|
-
const enable = useCallback(async (): Promise<boolean> => {
|
|
232
|
-
if (!capabilities.isAvailable || !capabilities.isEnrolled) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Require authentication before enabling
|
|
237
|
-
const authenticated = await authenticate({
|
|
238
|
-
promptMessage: "Authenticate to enable biometric login",
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
if (authenticated) {
|
|
242
|
-
await storage.set(BIOMETRIC_ENABLED_KEY, true);
|
|
243
|
-
setIsEnabled(true);
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return false;
|
|
248
|
-
}, [capabilities, authenticate]);
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Disable biometric authentication
|
|
252
|
-
*/
|
|
253
|
-
const disable = useCallback(async (): Promise<void> => {
|
|
254
|
-
await storage.set(BIOMETRIC_ENABLED_KEY, false);
|
|
255
|
-
setIsEnabled(false);
|
|
256
|
-
}, []);
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Toggle biometric authentication
|
|
260
|
-
*/
|
|
261
|
-
const toggle = useCallback(async (): Promise<boolean> => {
|
|
262
|
-
if (isEnabled) {
|
|
263
|
-
await disable();
|
|
264
|
-
return false;
|
|
265
|
-
} else {
|
|
266
|
-
return enable();
|
|
267
|
-
}
|
|
268
|
-
}, [isEnabled, enable, disable]);
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
capabilities,
|
|
272
|
-
isEnabled,
|
|
273
|
-
isLoading,
|
|
274
|
-
authenticate,
|
|
275
|
-
enable,
|
|
276
|
-
disable,
|
|
277
|
-
toggle,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Get a human-readable name for the biometric type
|
|
283
|
-
*/
|
|
284
|
-
export function getBiometricName(type: BiometricType): string {
|
|
285
|
-
switch (type) {
|
|
286
|
-
case "face":
|
|
287
|
-
return "Face ID";
|
|
288
|
-
case "fingerprint":
|
|
289
|
-
return "Touch ID";
|
|
290
|
-
case "iris":
|
|
291
|
-
return "Iris Scan";
|
|
292
|
-
default:
|
|
293
|
-
return "Biometrics";
|
|
294
|
-
}
|
|
295
|
-
}
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import * as LocalAuthentication from "expo-local-authentication";
|
|
3
|
+
import { storage } from "@/services/storage";
|
|
4
|
+
import { ENABLE_BIOMETRIC_AUTH } from "@/constants/config";
|
|
5
|
+
|
|
6
|
+
// Storage key for biometric preference
|
|
7
|
+
const BIOMETRIC_ENABLED_KEY = "biometric_auth_enabled";
|
|
8
|
+
|
|
9
|
+
export type BiometricType = "fingerprint" | "face" | "iris" | "none";
|
|
10
|
+
|
|
11
|
+
interface BiometricCapabilities {
|
|
12
|
+
/**
|
|
13
|
+
* Whether biometric authentication is available on this device
|
|
14
|
+
*/
|
|
15
|
+
isAvailable: boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether biometrics are enrolled on the device
|
|
19
|
+
*/
|
|
20
|
+
isEnrolled: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The type of biometric authentication available
|
|
24
|
+
*/
|
|
25
|
+
biometricType: BiometricType;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Security level of the biometric hardware
|
|
29
|
+
*/
|
|
30
|
+
securityLevel: LocalAuthentication.SecurityLevel;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface UseBiometricsReturn {
|
|
34
|
+
/**
|
|
35
|
+
* Device biometric capabilities
|
|
36
|
+
*/
|
|
37
|
+
capabilities: BiometricCapabilities;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether biometric auth is enabled by the user
|
|
41
|
+
*/
|
|
42
|
+
isEnabled: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the hook is still loading
|
|
46
|
+
*/
|
|
47
|
+
isLoading: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Authenticate using biometrics
|
|
51
|
+
* @returns true if authentication succeeded
|
|
52
|
+
*/
|
|
53
|
+
authenticate: (options?: AuthenticateOptions) => Promise<boolean>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Enable biometric authentication for the user
|
|
57
|
+
*/
|
|
58
|
+
enable: () => Promise<boolean>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Disable biometric authentication for the user
|
|
62
|
+
*/
|
|
63
|
+
disable: () => Promise<void>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Toggle biometric authentication
|
|
67
|
+
*/
|
|
68
|
+
toggle: () => Promise<boolean>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AuthenticateOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Message to display in the authentication prompt
|
|
74
|
+
*/
|
|
75
|
+
promptMessage?: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Message for the fallback button (e.g., "Use passcode")
|
|
79
|
+
*/
|
|
80
|
+
fallbackLabel?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether to allow device passcode as fallback
|
|
84
|
+
* @default true
|
|
85
|
+
*/
|
|
86
|
+
allowDeviceCredentials?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Cancel button label
|
|
90
|
+
*/
|
|
91
|
+
cancelLabel?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the biometric type from the available types
|
|
96
|
+
*/
|
|
97
|
+
function getBiometricType(
|
|
98
|
+
types: LocalAuthentication.AuthenticationType[]
|
|
99
|
+
): BiometricType {
|
|
100
|
+
if (
|
|
101
|
+
types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)
|
|
102
|
+
) {
|
|
103
|
+
return "face";
|
|
104
|
+
}
|
|
105
|
+
if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
|
|
106
|
+
return "fingerprint";
|
|
107
|
+
}
|
|
108
|
+
if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) {
|
|
109
|
+
return "iris";
|
|
110
|
+
}
|
|
111
|
+
return "none";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Hook for handling biometric authentication
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* function LoginScreen() {
|
|
120
|
+
* const {
|
|
121
|
+
* capabilities,
|
|
122
|
+
* isEnabled,
|
|
123
|
+
* authenticate,
|
|
124
|
+
* enable,
|
|
125
|
+
* disable
|
|
126
|
+
* } = useBiometrics();
|
|
127
|
+
*
|
|
128
|
+
* const handleBiometricLogin = async () => {
|
|
129
|
+
* const success = await authenticate({
|
|
130
|
+
* promptMessage: 'Authenticate to sign in',
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* if (success) {
|
|
134
|
+
* // Proceed with login
|
|
135
|
+
* }
|
|
136
|
+
* };
|
|
137
|
+
*
|
|
138
|
+
* if (!capabilities.isAvailable) {
|
|
139
|
+
* return null;
|
|
140
|
+
* }
|
|
141
|
+
*
|
|
142
|
+
* return (
|
|
143
|
+
* <Button onPress={handleBiometricLogin}>
|
|
144
|
+
* Sign in with {capabilities.biometricType === 'face' ? 'Face ID' : 'Touch ID'}
|
|
145
|
+
* </Button>
|
|
146
|
+
* );
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function useBiometrics(): UseBiometricsReturn {
|
|
151
|
+
const [capabilities, setCapabilities] = useState<BiometricCapabilities>({
|
|
152
|
+
isAvailable: false,
|
|
153
|
+
isEnrolled: false,
|
|
154
|
+
biometricType: "none",
|
|
155
|
+
securityLevel: LocalAuthentication.SecurityLevel.NONE,
|
|
156
|
+
});
|
|
157
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
158
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
159
|
+
|
|
160
|
+
// Check biometric capabilities on mount
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
async function checkCapabilities() {
|
|
163
|
+
try {
|
|
164
|
+
// Check if feature flag is enabled
|
|
165
|
+
if (!ENABLE_BIOMETRIC_AUTH) {
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [hasHardware, isEnrolled, supportedTypes, securityLevel] =
|
|
171
|
+
await Promise.all([
|
|
172
|
+
LocalAuthentication.hasHardwareAsync(),
|
|
173
|
+
LocalAuthentication.isEnrolledAsync(),
|
|
174
|
+
LocalAuthentication.supportedAuthenticationTypesAsync(),
|
|
175
|
+
LocalAuthentication.getEnrolledLevelAsync(),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
setCapabilities({
|
|
179
|
+
isAvailable: hasHardware,
|
|
180
|
+
isEnrolled,
|
|
181
|
+
biometricType: getBiometricType(supportedTypes),
|
|
182
|
+
securityLevel,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Load user preference
|
|
186
|
+
if (hasHardware && isEnrolled) {
|
|
187
|
+
const enabled = await storage.get<boolean>(BIOMETRIC_ENABLED_KEY);
|
|
188
|
+
setIsEnabled(enabled ?? false);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error("Failed to check biometric capabilities:", error);
|
|
192
|
+
} finally {
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
checkCapabilities();
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Authenticate using biometrics
|
|
202
|
+
*/
|
|
203
|
+
const authenticate = useCallback(
|
|
204
|
+
async (options: AuthenticateOptions = {}): Promise<boolean> => {
|
|
205
|
+
if (!capabilities.isAvailable || !capabilities.isEnrolled) {
|
|
206
|
+
console.warn("Biometric authentication not available");
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
212
|
+
promptMessage: options.promptMessage || "Authenticate to continue",
|
|
213
|
+
fallbackLabel: options.fallbackLabel || "Use passcode",
|
|
214
|
+
cancelLabel: options.cancelLabel || "Cancel",
|
|
215
|
+
disableDeviceFallback: options.allowDeviceCredentials === false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return result.success;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error("Biometric authentication error:", error);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
[capabilities]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Enable biometric authentication
|
|
229
|
+
* Requires successful authentication first
|
|
230
|
+
*/
|
|
231
|
+
const enable = useCallback(async (): Promise<boolean> => {
|
|
232
|
+
if (!capabilities.isAvailable || !capabilities.isEnrolled) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Require authentication before enabling
|
|
237
|
+
const authenticated = await authenticate({
|
|
238
|
+
promptMessage: "Authenticate to enable biometric login",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (authenticated) {
|
|
242
|
+
await storage.set(BIOMETRIC_ENABLED_KEY, true);
|
|
243
|
+
setIsEnabled(true);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return false;
|
|
248
|
+
}, [capabilities, authenticate]);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Disable biometric authentication
|
|
252
|
+
*/
|
|
253
|
+
const disable = useCallback(async (): Promise<void> => {
|
|
254
|
+
await storage.set(BIOMETRIC_ENABLED_KEY, false);
|
|
255
|
+
setIsEnabled(false);
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Toggle biometric authentication
|
|
260
|
+
*/
|
|
261
|
+
const toggle = useCallback(async (): Promise<boolean> => {
|
|
262
|
+
if (isEnabled) {
|
|
263
|
+
await disable();
|
|
264
|
+
return false;
|
|
265
|
+
} else {
|
|
266
|
+
return enable();
|
|
267
|
+
}
|
|
268
|
+
}, [isEnabled, enable, disable]);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
capabilities,
|
|
272
|
+
isEnabled,
|
|
273
|
+
isLoading,
|
|
274
|
+
authenticate,
|
|
275
|
+
enable,
|
|
276
|
+
disable,
|
|
277
|
+
toggle,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get a human-readable name for the biometric type
|
|
283
|
+
*/
|
|
284
|
+
export function getBiometricName(type: BiometricType): string {
|
|
285
|
+
switch (type) {
|
|
286
|
+
case "face":
|
|
287
|
+
return "Face ID";
|
|
288
|
+
case "fingerprint":
|
|
289
|
+
return "Touch ID";
|
|
290
|
+
case "iris":
|
|
291
|
+
return "Iris Scan";
|
|
292
|
+
default:
|
|
293
|
+
return "Biometrics";
|
|
294
|
+
}
|
|
295
|
+
}
|