@croacroa/react-native-template 1.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.
Files changed (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,290 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ useCallback,
7
+ ReactNode,
8
+ } from "react";
9
+ import * as SecureStore from "expo-secure-store";
10
+ import { router } from "expo-router";
11
+ import { toast } from "@/utils/toast";
12
+
13
+ interface User {
14
+ id: string;
15
+ email: string;
16
+ name: string;
17
+ avatar?: string;
18
+ }
19
+
20
+ interface AuthTokens {
21
+ accessToken: string;
22
+ refreshToken: string;
23
+ expiresAt: number;
24
+ }
25
+
26
+ interface AuthContextType {
27
+ user: User | null;
28
+ isAuthenticated: boolean;
29
+ isLoading: boolean;
30
+ signIn: (email: string, password: string) => Promise<void>;
31
+ signUp: (email: string, password: string, name: string) => Promise<void>;
32
+ signOut: () => Promise<void>;
33
+ updateUser: (user: Partial<User>) => void;
34
+ refreshSession: () => Promise<boolean>;
35
+ }
36
+
37
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
38
+
39
+ const TOKEN_KEY = "auth_tokens";
40
+ const USER_KEY = "auth_user";
41
+ const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes before expiry
42
+
43
+ export function AuthProvider({ children }: { children: ReactNode }) {
44
+ const [user, setUser] = useState<User | null>(null);
45
+ const [tokens, setTokens] = useState<AuthTokens | null>(null);
46
+ const [isLoading, setIsLoading] = useState(true);
47
+
48
+ // Load stored auth on mount
49
+ useEffect(() => {
50
+ loadStoredAuth();
51
+ }, []);
52
+
53
+ // Setup token refresh interval
54
+ useEffect(() => {
55
+ if (!tokens) return;
56
+
57
+ const checkAndRefresh = async () => {
58
+ const timeUntilExpiry = tokens.expiresAt - Date.now();
59
+ if (timeUntilExpiry < TOKEN_REFRESH_THRESHOLD) {
60
+ await refreshSession();
61
+ }
62
+ };
63
+
64
+ // Check immediately and then every minute
65
+ checkAndRefresh();
66
+ const interval = setInterval(checkAndRefresh, 60 * 1000);
67
+
68
+ return () => clearInterval(interval);
69
+ }, [tokens?.expiresAt]);
70
+
71
+ const loadStoredAuth = async () => {
72
+ try {
73
+ const [storedTokens, storedUser] = await Promise.all([
74
+ SecureStore.getItemAsync(TOKEN_KEY),
75
+ SecureStore.getItemAsync(USER_KEY),
76
+ ]);
77
+
78
+ if (storedTokens && storedUser) {
79
+ const parsedTokens: AuthTokens = JSON.parse(storedTokens);
80
+
81
+ // Check if tokens are expired
82
+ if (parsedTokens.expiresAt < Date.now()) {
83
+ // Try to refresh
84
+ const refreshed = await tryRefreshWithToken(parsedTokens.refreshToken);
85
+ if (!refreshed) {
86
+ await clearAuth();
87
+ return;
88
+ }
89
+ } else {
90
+ setTokens(parsedTokens);
91
+ setUser(JSON.parse(storedUser));
92
+ }
93
+ }
94
+ } catch (error) {
95
+ console.error("Failed to load auth:", error);
96
+ await clearAuth();
97
+ } finally {
98
+ setIsLoading(false);
99
+ }
100
+ };
101
+
102
+ const tryRefreshWithToken = async (refreshToken: string): Promise<boolean> => {
103
+ try {
104
+ // TODO: Replace with your actual API call
105
+ // const response = await api.post('/auth/refresh', { refreshToken });
106
+ // const { accessToken, refreshToken: newRefreshToken, expiresIn, user } = response.data;
107
+
108
+ // Mock implementation
109
+ const mockTokens: AuthTokens = {
110
+ accessToken: "new_mock_access_token",
111
+ refreshToken: "new_mock_refresh_token",
112
+ expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
113
+ };
114
+ const mockUser: User = {
115
+ id: "1",
116
+ email: "user@example.com",
117
+ name: "User",
118
+ };
119
+
120
+ await saveAuth(mockTokens, mockUser);
121
+ return true;
122
+ } catch (error) {
123
+ console.error("Token refresh failed:", error);
124
+ return false;
125
+ }
126
+ };
127
+
128
+ const saveAuth = async (newTokens: AuthTokens, newUser: User) => {
129
+ await Promise.all([
130
+ SecureStore.setItemAsync(TOKEN_KEY, JSON.stringify(newTokens)),
131
+ SecureStore.setItemAsync(USER_KEY, JSON.stringify(newUser)),
132
+ ]);
133
+ setTokens(newTokens);
134
+ setUser(newUser);
135
+ };
136
+
137
+ const clearAuth = async () => {
138
+ await Promise.all([
139
+ SecureStore.deleteItemAsync(TOKEN_KEY),
140
+ SecureStore.deleteItemAsync(USER_KEY),
141
+ ]);
142
+ setTokens(null);
143
+ setUser(null);
144
+ };
145
+
146
+ const signIn = useCallback(async (email: string, password: string) => {
147
+ try {
148
+ // TODO: Replace with your actual API call
149
+ // const response = await api.post('/auth/login', { email, password });
150
+ // const { accessToken, refreshToken, expiresIn, user } = response.data;
151
+
152
+ // Mock implementation - replace with real API
153
+ await new Promise((resolve) => setTimeout(resolve, 1000));
154
+
155
+ // Simulate validation
156
+ if (email !== "test@example.com" && !email.includes("@")) {
157
+ throw new Error("Invalid credentials");
158
+ }
159
+
160
+ const mockTokens: AuthTokens = {
161
+ accessToken: "mock_access_token",
162
+ refreshToken: "mock_refresh_token",
163
+ expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
164
+ };
165
+ const mockUser: User = {
166
+ id: "1",
167
+ email,
168
+ name: email.split("@")[0],
169
+ };
170
+
171
+ await saveAuth(mockTokens, mockUser);
172
+ toast.success("Welcome back!", `Signed in as ${mockUser.name}`);
173
+ } catch (error) {
174
+ toast.error("Sign in failed", "Invalid email or password");
175
+ throw error;
176
+ }
177
+ }, []);
178
+
179
+ const signUp = useCallback(
180
+ async (email: string, password: string, name: string) => {
181
+ try {
182
+ // TODO: Replace with your actual API call
183
+ // const response = await api.post('/auth/register', { email, password, name });
184
+ // const { accessToken, refreshToken, expiresIn, user } = response.data;
185
+
186
+ // Mock implementation
187
+ await new Promise((resolve) => setTimeout(resolve, 1000));
188
+
189
+ const mockTokens: AuthTokens = {
190
+ accessToken: "mock_access_token",
191
+ refreshToken: "mock_refresh_token",
192
+ expiresAt: Date.now() + 60 * 60 * 1000,
193
+ };
194
+ const mockUser: User = {
195
+ id: "1",
196
+ email,
197
+ name,
198
+ };
199
+
200
+ await saveAuth(mockTokens, mockUser);
201
+ toast.success("Account created!", `Welcome, ${name}`);
202
+ } catch (error) {
203
+ toast.error("Sign up failed", "Could not create account");
204
+ throw error;
205
+ }
206
+ },
207
+ []
208
+ );
209
+
210
+ const signOut = useCallback(async () => {
211
+ try {
212
+ // TODO: Optionally call logout endpoint to invalidate refresh token
213
+ // await api.post('/auth/logout', { refreshToken: tokens?.refreshToken });
214
+
215
+ await clearAuth();
216
+ toast.info("Signed out");
217
+ router.replace("/(public)/login");
218
+ } catch (error) {
219
+ console.error("Sign out error:", error);
220
+ // Clear local state anyway
221
+ await clearAuth();
222
+ }
223
+ }, []);
224
+
225
+ const updateUser = useCallback(
226
+ (updates: Partial<User>) => {
227
+ if (user) {
228
+ const updatedUser = { ...user, ...updates };
229
+ setUser(updatedUser);
230
+ SecureStore.setItemAsync(USER_KEY, JSON.stringify(updatedUser));
231
+ }
232
+ },
233
+ [user]
234
+ );
235
+
236
+ const refreshSession = useCallback(async (): Promise<boolean> => {
237
+ if (!tokens?.refreshToken) return false;
238
+ return tryRefreshWithToken(tokens.refreshToken);
239
+ }, [tokens]);
240
+
241
+ return (
242
+ <AuthContext.Provider
243
+ value={{
244
+ user,
245
+ isAuthenticated: !!user && !!tokens,
246
+ isLoading,
247
+ signIn,
248
+ signUp,
249
+ signOut,
250
+ updateUser,
251
+ refreshSession,
252
+ }}
253
+ >
254
+ {children}
255
+ </AuthContext.Provider>
256
+ );
257
+ }
258
+
259
+ export function useAuth() {
260
+ const context = useContext(AuthContext);
261
+ if (context === undefined) {
262
+ throw new Error("useAuth must be used within an AuthProvider");
263
+ }
264
+ return context;
265
+ }
266
+
267
+ /**
268
+ * Get the current access token for API calls
269
+ * Automatically handles token refresh if needed
270
+ */
271
+ export async function getAuthToken(): Promise<string | null> {
272
+ try {
273
+ const stored = await SecureStore.getItemAsync(TOKEN_KEY);
274
+ if (!stored) return null;
275
+
276
+ const tokens: AuthTokens = JSON.parse(stored);
277
+
278
+ // Check if token is expired or about to expire
279
+ if (tokens.expiresAt < Date.now() + TOKEN_REFRESH_THRESHOLD) {
280
+ // Token needs refresh - this should be handled by the auth context
281
+ // For now, return the current token and let the API handle 401
282
+ console.warn("Token is expired or about to expire");
283
+ }
284
+
285
+ return tokens.accessToken;
286
+ } catch (error) {
287
+ console.error("Failed to get auth token:", error);
288
+ return null;
289
+ }
290
+ }
@@ -0,0 +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
+ }