@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,358 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import * as Updates from "expo-updates";
3
+ import { Alert, AppState, AppStateStatus } from "react-native";
4
+ import { IS_DEV } from "@/constants/config";
5
+
6
+ export type UpdateStatus =
7
+ | "idle"
8
+ | "checking"
9
+ | "available"
10
+ | "downloading"
11
+ | "ready"
12
+ | "error"
13
+ | "no-update";
14
+
15
+ interface UpdateInfo {
16
+ /**
17
+ * Whether an update is available
18
+ */
19
+ isAvailable: boolean;
20
+
21
+ /**
22
+ * Current update status
23
+ */
24
+ status: UpdateStatus;
25
+
26
+ /**
27
+ * Download progress (0-100)
28
+ */
29
+ progress: number;
30
+
31
+ /**
32
+ * Error message if status is 'error'
33
+ */
34
+ error: string | null;
35
+
36
+ /**
37
+ * Update manifest info
38
+ */
39
+ manifest: Updates.Manifest | null;
40
+ }
41
+
42
+ interface UseUpdatesOptions {
43
+ /**
44
+ * Check for updates on mount
45
+ * @default true
46
+ */
47
+ checkOnMount?: boolean;
48
+
49
+ /**
50
+ * Check for updates when app returns to foreground
51
+ * @default true
52
+ */
53
+ checkOnForeground?: boolean;
54
+
55
+ /**
56
+ * Show alert when update is available
57
+ * @default true
58
+ */
59
+ showAlert?: boolean;
60
+
61
+ /**
62
+ * Auto download and apply updates
63
+ * @default false
64
+ */
65
+ autoUpdate?: boolean;
66
+
67
+ /**
68
+ * Callback when update is available
69
+ */
70
+ onUpdateAvailable?: (manifest: Updates.Manifest) => void;
71
+
72
+ /**
73
+ * Callback when update is downloaded and ready
74
+ */
75
+ onUpdateReady?: () => void;
76
+
77
+ /**
78
+ * Callback on error
79
+ */
80
+ onError?: (error: Error) => void;
81
+ }
82
+
83
+ interface UseUpdatesReturn extends UpdateInfo {
84
+ /**
85
+ * Manually check for updates
86
+ */
87
+ checkForUpdate: () => Promise<boolean>;
88
+
89
+ /**
90
+ * Download the available update
91
+ */
92
+ downloadUpdate: () => Promise<void>;
93
+
94
+ /**
95
+ * Apply the downloaded update (restarts the app)
96
+ */
97
+ applyUpdate: () => Promise<void>;
98
+
99
+ /**
100
+ * Reset update state
101
+ */
102
+ reset: () => void;
103
+ }
104
+
105
+ /**
106
+ * Hook for managing OTA updates with expo-updates
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * function App() {
111
+ * const { status, isAvailable, checkForUpdate, applyUpdate } = useUpdates({
112
+ * checkOnMount: true,
113
+ * showAlert: true,
114
+ * });
115
+ *
116
+ * if (status === 'ready') {
117
+ * return (
118
+ * <Button onPress={applyUpdate}>
119
+ * Restart to update
120
+ * </Button>
121
+ * );
122
+ * }
123
+ *
124
+ * return <App />;
125
+ * }
126
+ * ```
127
+ */
128
+ export function useUpdates(options: UseUpdatesOptions = {}): UseUpdatesReturn {
129
+ const {
130
+ checkOnMount = true,
131
+ checkOnForeground = true,
132
+ showAlert = true,
133
+ autoUpdate = false,
134
+ onUpdateAvailable,
135
+ onUpdateReady,
136
+ onError,
137
+ } = options;
138
+
139
+ const [status, setStatus] = useState<UpdateStatus>("idle");
140
+ const [progress, setProgress] = useState(0);
141
+ const [error, setError] = useState<string | null>(null);
142
+ const [manifest, setManifest] = useState<Updates.Manifest | null>(null);
143
+
144
+ const isAvailable =
145
+ status === "available" || status === "downloading" || status === "ready";
146
+
147
+ /**
148
+ * Download the available update
149
+ */
150
+ const downloadUpdate = useCallback(async (): Promise<void> => {
151
+ if (IS_DEV || !Updates.isEnabled) {
152
+ return;
153
+ }
154
+
155
+ try {
156
+ setStatus("downloading");
157
+ setProgress(0);
158
+
159
+ const progressInterval = setInterval(() => {
160
+ setProgress((p) => Math.min(p + 10, 90));
161
+ }, 200);
162
+
163
+ await Updates.fetchUpdateAsync();
164
+
165
+ clearInterval(progressInterval);
166
+ setProgress(100);
167
+ setStatus("ready");
168
+ onUpdateReady?.();
169
+ } catch (e) {
170
+ const err = e as Error;
171
+ setStatus("error");
172
+ setError(err.message);
173
+ onError?.(err);
174
+ }
175
+ }, [onUpdateReady, onError]);
176
+
177
+ /**
178
+ * Apply the downloaded update (restarts the app)
179
+ */
180
+ const applyUpdate = useCallback(async (): Promise<void> => {
181
+ if (IS_DEV || !Updates.isEnabled) {
182
+ return;
183
+ }
184
+
185
+ try {
186
+ await Updates.reloadAsync();
187
+ } catch (e) {
188
+ const err = e as Error;
189
+ setStatus("error");
190
+ setError(err.message);
191
+ onError?.(err);
192
+ }
193
+ }, [onError]);
194
+
195
+ /**
196
+ * Check for available updates
197
+ */
198
+ const checkForUpdate = useCallback(async (): Promise<boolean> => {
199
+ // Skip in development
200
+ if (IS_DEV || !Updates.isEnabled) {
201
+ return false;
202
+ }
203
+
204
+ try {
205
+ setStatus("checking");
206
+ setError(null);
207
+
208
+ const update = await Updates.checkForUpdateAsync();
209
+
210
+ if (update.isAvailable) {
211
+ setStatus("available");
212
+ setManifest(update.manifest);
213
+ onUpdateAvailable?.(update.manifest);
214
+
215
+ if (showAlert && !autoUpdate) {
216
+ Alert.alert(
217
+ "Update Available",
218
+ "A new version of the app is available. Would you like to update now?",
219
+ [
220
+ { text: "Later", style: "cancel" },
221
+ {
222
+ text: "Update",
223
+ onPress: () => downloadUpdate(),
224
+ },
225
+ ]
226
+ );
227
+ }
228
+
229
+ if (autoUpdate) {
230
+ await downloadUpdate();
231
+ }
232
+
233
+ return true;
234
+ } else {
235
+ setStatus("no-update");
236
+ return false;
237
+ }
238
+ } catch (e) {
239
+ const err = e as Error;
240
+ setStatus("error");
241
+ setError(err.message);
242
+ onError?.(err);
243
+ return false;
244
+ }
245
+ }, [
246
+ showAlert,
247
+ autoUpdate,
248
+ onUpdateAvailable,
249
+ onError,
250
+ downloadUpdate,
251
+ applyUpdate,
252
+ ]);
253
+
254
+ /**
255
+ * Reset update state
256
+ */
257
+ const reset = useCallback(() => {
258
+ setStatus("idle");
259
+ setProgress(0);
260
+ setError(null);
261
+ setManifest(null);
262
+ }, []);
263
+
264
+ // Check on mount
265
+ useEffect(() => {
266
+ if (checkOnMount) {
267
+ checkForUpdate();
268
+ }
269
+ // eslint-disable-next-line react-hooks/exhaustive-deps
270
+ }, [checkOnMount]);
271
+
272
+ // Check when app returns to foreground
273
+ useEffect(() => {
274
+ if (!checkOnForeground) return;
275
+
276
+ const handleAppStateChange = (nextState: AppStateStatus) => {
277
+ if (nextState === "active") {
278
+ checkForUpdate();
279
+ }
280
+ };
281
+
282
+ const subscription = AppState.addEventListener(
283
+ "change",
284
+ handleAppStateChange
285
+ );
286
+ return () => subscription.remove();
287
+ }, [checkOnForeground, checkForUpdate]);
288
+
289
+ return {
290
+ isAvailable,
291
+ status,
292
+ progress,
293
+ error,
294
+ manifest,
295
+ checkForUpdate,
296
+ downloadUpdate,
297
+ applyUpdate,
298
+ reset,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Get current update info
304
+ */
305
+ export function getUpdateInfo(): {
306
+ isEnabled: boolean;
307
+ channel: string | null;
308
+ runtimeVersion: string | null;
309
+ updateId: string | null;
310
+ createdAt: Date | null;
311
+ } {
312
+ if (IS_DEV || !Updates.isEnabled) {
313
+ return {
314
+ isEnabled: false,
315
+ channel: null,
316
+ runtimeVersion: null,
317
+ updateId: null,
318
+ createdAt: null,
319
+ };
320
+ }
321
+
322
+ return {
323
+ isEnabled: Updates.isEnabled,
324
+ channel: Updates.channel,
325
+ runtimeVersion: Updates.runtimeVersion,
326
+ updateId: Updates.updateId,
327
+ createdAt: Updates.createdAt,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Force check and apply update (useful for settings screen)
333
+ */
334
+ export async function forceUpdate(): Promise<void> {
335
+ if (IS_DEV || !Updates.isEnabled) {
336
+ Alert.alert(
337
+ "Development Mode",
338
+ "Updates are not available in development."
339
+ );
340
+ return;
341
+ }
342
+
343
+ try {
344
+ const update = await Updates.checkForUpdateAsync();
345
+ if (update.isAvailable) {
346
+ Alert.alert("Updating...", "Downloading the latest version.");
347
+ await Updates.fetchUpdateAsync();
348
+ await Updates.reloadAsync();
349
+ } else {
350
+ Alert.alert("Up to Date", "You're running the latest version.");
351
+ }
352
+ } catch (error) {
353
+ Alert.alert(
354
+ "Update Failed",
355
+ "Could not check for updates. Please try again."
356
+ );
357
+ }
358
+ }
package/i18n/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+ import * as Localization from "expo-localization";
4
+ import { storage } from "@/services/storage";
5
+
6
+ import en from "./locales/en.json";
7
+ import fr from "./locales/fr.json";
8
+
9
+ export const LANGUAGES = {
10
+ en: { name: "English", nativeName: "English" },
11
+ fr: { name: "French", nativeName: "Français" },
12
+ } as const;
13
+
14
+ export type LanguageCode = keyof typeof LANGUAGES;
15
+
16
+ const LANGUAGE_STORAGE_KEY = "app_language";
17
+
18
+ const resources = {
19
+ en: { translation: en },
20
+ fr: { translation: fr },
21
+ };
22
+
23
+ /**
24
+ * Get the device's preferred language
25
+ * Falls back to 'en' if not supported
26
+ */
27
+ function getDeviceLanguage(): LanguageCode {
28
+ const locale = Localization.getLocales()[0];
29
+ const languageCode = locale?.languageCode || "en";
30
+
31
+ // Check if we support this language
32
+ if (languageCode in LANGUAGES) {
33
+ return languageCode as LanguageCode;
34
+ }
35
+
36
+ return "en";
37
+ }
38
+
39
+ /**
40
+ * Initialize i18n with the saved language or device language
41
+ */
42
+ export async function initI18n(): Promise<void> {
43
+ // Try to get saved language preference
44
+ const savedLanguage = await storage.get<LanguageCode>(LANGUAGE_STORAGE_KEY);
45
+ const initialLanguage = savedLanguage || getDeviceLanguage();
46
+
47
+ await i18n.use(initReactI18next).init({
48
+ resources,
49
+ lng: initialLanguage,
50
+ fallbackLng: "en",
51
+ compatibilityJSON: "v3",
52
+ interpolation: {
53
+ escapeValue: false, // React already escapes values
54
+ },
55
+ react: {
56
+ useSuspense: false, // Prevents issues with SSR/async loading
57
+ },
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Change the app language
63
+ */
64
+ export async function changeLanguage(language: LanguageCode): Promise<void> {
65
+ await i18n.changeLanguage(language);
66
+ await storage.set(LANGUAGE_STORAGE_KEY, language);
67
+ }
68
+
69
+ /**
70
+ * Get current language
71
+ */
72
+ export function getCurrentLanguage(): LanguageCode {
73
+ return (i18n.language || "en") as LanguageCode;
74
+ }
75
+
76
+ export { i18n };
77
+ export default i18n;
@@ -0,0 +1,101 @@
1
+ {
2
+ "common": {
3
+ "loading": "Loading...",
4
+ "error": "Error",
5
+ "success": "Success",
6
+ "cancel": "Cancel",
7
+ "confirm": "Confirm",
8
+ "save": "Save",
9
+ "delete": "Delete",
10
+ "edit": "Edit",
11
+ "close": "Close",
12
+ "back": "Back",
13
+ "next": "Next",
14
+ "done": "Done",
15
+ "retry": "Retry",
16
+ "search": "Search",
17
+ "noResults": "No results found",
18
+ "offline": "You are offline",
19
+ "online": "Back online"
20
+ },
21
+ "auth": {
22
+ "signIn": "Sign In",
23
+ "signUp": "Sign Up",
24
+ "signOut": "Sign Out",
25
+ "email": "Email",
26
+ "password": "Password",
27
+ "confirmPassword": "Confirm Password",
28
+ "name": "Name",
29
+ "forgotPassword": "Forgot Password?",
30
+ "resetPassword": "Reset Password",
31
+ "noAccount": "Don't have an account?",
32
+ "haveAccount": "Already have an account?",
33
+ "welcomeBack": "Welcome back!",
34
+ "signInToContinue": "Sign in to continue",
35
+ "createAccount": "Create Account",
36
+ "joinUs": "Join us today",
37
+ "enterEmail": "Enter your email",
38
+ "enterPassword": "Enter your password",
39
+ "enterName": "Enter your name",
40
+ "passwordHint": "At least 8 characters",
41
+ "invalidCredentials": "Invalid email or password",
42
+ "accountCreated": "Account created successfully!",
43
+ "sessionExpired": "Session expired. Please sign in again.",
44
+ "biometric": {
45
+ "title": "Biometric Authentication",
46
+ "prompt": "Authenticate to continue",
47
+ "fallback": "Use passcode",
48
+ "enabled": "Biometric login enabled",
49
+ "disabled": "Biometric login disabled",
50
+ "notAvailable": "Biometric authentication not available"
51
+ }
52
+ },
53
+ "navigation": {
54
+ "home": "Home",
55
+ "profile": "Profile",
56
+ "settings": "Settings",
57
+ "notifications": "Notifications"
58
+ },
59
+ "profile": {
60
+ "title": "Profile",
61
+ "editProfile": "Edit Profile",
62
+ "changePhoto": "Change Photo",
63
+ "personalInfo": "Personal Information",
64
+ "memberSince": "Member since {{date}}"
65
+ },
66
+ "settings": {
67
+ "title": "Settings",
68
+ "appearance": "Appearance",
69
+ "theme": "Theme",
70
+ "themeLight": "Light",
71
+ "themeDark": "Dark",
72
+ "themeSystem": "System",
73
+ "language": "Language",
74
+ "notifications": "Notifications",
75
+ "pushNotifications": "Push Notifications",
76
+ "emailNotifications": "Email Notifications",
77
+ "security": "Security",
78
+ "biometricAuth": "Biometric Authentication",
79
+ "changePassword": "Change Password",
80
+ "privacy": "Privacy Policy",
81
+ "terms": "Terms of Service",
82
+ "about": "About",
83
+ "version": "Version",
84
+ "deleteAccount": "Delete Account"
85
+ },
86
+ "errors": {
87
+ "generic": "Something went wrong",
88
+ "network": "Network error. Please check your connection.",
89
+ "timeout": "Request timed out. Please try again.",
90
+ "unauthorized": "You are not authorized to perform this action.",
91
+ "notFound": "Resource not found.",
92
+ "validation": "Please check your input and try again."
93
+ },
94
+ "validation": {
95
+ "required": "This field is required",
96
+ "email": "Please enter a valid email",
97
+ "passwordMin": "Password must be at least 8 characters",
98
+ "passwordMatch": "Passwords do not match",
99
+ "nameMin": "Name must be at least 2 characters"
100
+ }
101
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "common": {
3
+ "loading": "Chargement...",
4
+ "error": "Erreur",
5
+ "success": "Succès",
6
+ "cancel": "Annuler",
7
+ "confirm": "Confirmer",
8
+ "save": "Enregistrer",
9
+ "delete": "Supprimer",
10
+ "edit": "Modifier",
11
+ "close": "Fermer",
12
+ "back": "Retour",
13
+ "next": "Suivant",
14
+ "done": "Terminé",
15
+ "retry": "Réessayer",
16
+ "search": "Rechercher",
17
+ "noResults": "Aucun résultat trouvé",
18
+ "offline": "Vous êtes hors ligne",
19
+ "online": "De retour en ligne"
20
+ },
21
+ "auth": {
22
+ "signIn": "Se connecter",
23
+ "signUp": "S'inscrire",
24
+ "signOut": "Se déconnecter",
25
+ "email": "Email",
26
+ "password": "Mot de passe",
27
+ "confirmPassword": "Confirmer le mot de passe",
28
+ "name": "Nom",
29
+ "forgotPassword": "Mot de passe oublié ?",
30
+ "resetPassword": "Réinitialiser le mot de passe",
31
+ "noAccount": "Vous n'avez pas de compte ?",
32
+ "haveAccount": "Vous avez déjà un compte ?",
33
+ "welcomeBack": "Bon retour !",
34
+ "signInToContinue": "Connectez-vous pour continuer",
35
+ "createAccount": "Créer un compte",
36
+ "joinUs": "Rejoignez-nous",
37
+ "enterEmail": "Entrez votre email",
38
+ "enterPassword": "Entrez votre mot de passe",
39
+ "enterName": "Entrez votre nom",
40
+ "passwordHint": "Au moins 8 caractères",
41
+ "invalidCredentials": "Email ou mot de passe invalide",
42
+ "accountCreated": "Compte créé avec succès !",
43
+ "sessionExpired": "Session expirée. Veuillez vous reconnecter.",
44
+ "biometric": {
45
+ "title": "Authentification biométrique",
46
+ "prompt": "Authentifiez-vous pour continuer",
47
+ "fallback": "Utiliser le code",
48
+ "enabled": "Connexion biométrique activée",
49
+ "disabled": "Connexion biométrique désactivée",
50
+ "notAvailable": "Authentification biométrique non disponible"
51
+ }
52
+ },
53
+ "navigation": {
54
+ "home": "Accueil",
55
+ "profile": "Profil",
56
+ "settings": "Paramètres",
57
+ "notifications": "Notifications"
58
+ },
59
+ "profile": {
60
+ "title": "Profil",
61
+ "editProfile": "Modifier le profil",
62
+ "changePhoto": "Changer la photo",
63
+ "personalInfo": "Informations personnelles",
64
+ "memberSince": "Membre depuis {{date}}"
65
+ },
66
+ "settings": {
67
+ "title": "Paramètres",
68
+ "appearance": "Apparence",
69
+ "theme": "Thème",
70
+ "themeLight": "Clair",
71
+ "themeDark": "Sombre",
72
+ "themeSystem": "Système",
73
+ "language": "Langue",
74
+ "notifications": "Notifications",
75
+ "pushNotifications": "Notifications push",
76
+ "emailNotifications": "Notifications par email",
77
+ "security": "Sécurité",
78
+ "biometricAuth": "Authentification biométrique",
79
+ "changePassword": "Changer le mot de passe",
80
+ "privacy": "Politique de confidentialité",
81
+ "terms": "Conditions d'utilisation",
82
+ "about": "À propos",
83
+ "version": "Version",
84
+ "deleteAccount": "Supprimer le compte"
85
+ },
86
+ "errors": {
87
+ "generic": "Une erreur s'est produite",
88
+ "network": "Erreur réseau. Vérifiez votre connexion.",
89
+ "timeout": "Délai dépassé. Veuillez réessayer.",
90
+ "unauthorized": "Vous n'êtes pas autorisé à effectuer cette action.",
91
+ "notFound": "Ressource introuvable.",
92
+ "validation": "Veuillez vérifier vos informations et réessayer."
93
+ },
94
+ "validation": {
95
+ "required": "Ce champ est requis",
96
+ "email": "Veuillez entrer un email valide",
97
+ "passwordMin": "Le mot de passe doit contenir au moins 8 caractères",
98
+ "passwordMatch": "Les mots de passe ne correspondent pas",
99
+ "nameMin": "Le nom doit contenir au moins 2 caractères"
100
+ }
101
+ }
package/jest.config.js ADDED
@@ -0,0 +1,32 @@
1
+ module.exports = {
2
+ preset: "jest-expo",
3
+ setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
4
+ transformIgnorePatterns: [
5
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|nativewind|tailwindcss)",
6
+ ],
7
+ moduleNameMapper: {
8
+ "^@/(.*)$": "<rootDir>/$1",
9
+ },
10
+ collectCoverageFrom: [
11
+ "**/*.{ts,tsx}",
12
+ "!**/node_modules/**",
13
+ "!**/coverage/**",
14
+ "!**/.expo/**",
15
+ "!**/babel.config.js",
16
+ "!**/jest.config.js",
17
+ "!**/metro.config.js",
18
+ "!**/tailwind.config.js",
19
+ "!**/*.stories.tsx",
20
+ "!**/.storybook/**",
21
+ ],
22
+ coverageThreshold: {
23
+ global: {
24
+ branches: 50,
25
+ functions: 50,
26
+ lines: 50,
27
+ statements: 50,
28
+ },
29
+ },
30
+ testPathIgnorePatterns: ["/node_modules/", "/.expo/"],
31
+ moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
32
+ };