@croacroa/react-native-template 2.0.1 → 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 -0
  10. package/README.md +446 -399
  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 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  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 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -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 -0
  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 -175
  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
@@ -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
+ }