@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.
Files changed (69) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +13 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +32 -2
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +229 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/withAccessibility.tsx +272 -0
@@ -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
+ }