@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
package/i18n/index.ts CHANGED
@@ -1,77 +1,194 @@
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;
1
+ /**
2
+ * @fileoverview Internationalization (i18n) setup with RTL support
3
+ * Provides multi-language support with automatic device language detection.
4
+ * @module i18n
5
+ */
6
+
7
+ import i18n from "i18next";
8
+ import { initReactI18next } from "react-i18next";
9
+ import * as Localization from "expo-localization";
10
+ import { I18nManager } from "react-native";
11
+ import { storage } from "@/services/storage";
12
+
13
+ import en from "./locales/en.json";
14
+ import fr from "./locales/fr.json";
15
+ import es from "./locales/es.json";
16
+ import de from "./locales/de.json";
17
+ import ar from "./locales/ar.json";
18
+
19
+ /**
20
+ * Supported languages configuration.
21
+ * Each language includes its English name, native name, and RTL flag.
22
+ */
23
+ export const LANGUAGES = {
24
+ en: { name: "English", nativeName: "English", rtl: false },
25
+ fr: { name: "French", nativeName: "Français", rtl: false },
26
+ es: { name: "Spanish", nativeName: "Español", rtl: false },
27
+ de: { name: "German", nativeName: "Deutsch", rtl: false },
28
+ ar: { name: "Arabic", nativeName: "العربية", rtl: true },
29
+ } as const;
30
+
31
+ export type LanguageCode = keyof typeof LANGUAGES;
32
+
33
+ const LANGUAGE_STORAGE_KEY = "app_language";
34
+
35
+ const resources = {
36
+ en: { translation: en },
37
+ fr: { translation: fr },
38
+ es: { translation: es },
39
+ de: { translation: de },
40
+ ar: { translation: ar },
41
+ };
42
+
43
+ /**
44
+ * Get the device's preferred language
45
+ * Falls back to 'en' if not supported
46
+ */
47
+ function getDeviceLanguage(): LanguageCode {
48
+ const locale = Localization.getLocales()[0];
49
+ const languageCode = locale?.languageCode || "en";
50
+
51
+ // Check if we support this language
52
+ if (languageCode in LANGUAGES) {
53
+ return languageCode as LanguageCode;
54
+ }
55
+
56
+ return "en";
57
+ }
58
+
59
+ /**
60
+ * Initialize i18n with the saved language or device language
61
+ */
62
+ export async function initI18n(): Promise<void> {
63
+ // Try to get saved language preference
64
+ const savedLanguage = await storage.get<LanguageCode>(LANGUAGE_STORAGE_KEY);
65
+ const initialLanguage = savedLanguage || getDeviceLanguage();
66
+
67
+ await i18n.use(initReactI18next).init({
68
+ resources,
69
+ lng: initialLanguage,
70
+ fallbackLng: "en",
71
+ compatibilityJSON: "v3",
72
+ interpolation: {
73
+ escapeValue: false, // React already escapes values
74
+ },
75
+ react: {
76
+ useSuspense: false, // Prevents issues with SSR/async loading
77
+ },
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Change the app language and apply RTL settings if needed.
83
+ * Note: RTL changes require an app restart to take full effect.
84
+ *
85
+ * @param language - The language code to switch to
86
+ * @returns Promise that resolves when the language is changed
87
+ */
88
+ export async function changeLanguage(language: LanguageCode): Promise<void> {
89
+ await i18n.changeLanguage(language);
90
+ await storage.set(LANGUAGE_STORAGE_KEY, language);
91
+
92
+ // Handle RTL layout direction
93
+ const isRTL = LANGUAGES[language].rtl;
94
+ if (I18nManager.isRTL !== isRTL) {
95
+ I18nManager.allowRTL(isRTL);
96
+ I18nManager.forceRTL(isRTL);
97
+ // Note: App restart is required for RTL changes to take full effect
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Check if the current language is RTL
103
+ */
104
+ export function isCurrentLanguageRTL(): boolean {
105
+ const currentLang = getCurrentLanguage();
106
+ return LANGUAGES[currentLang]?.rtl ?? false;
107
+ }
108
+
109
+ /**
110
+ * Get all available languages as an array for UI selectors
111
+ */
112
+ export function getAvailableLanguages(): Array<{
113
+ code: LanguageCode;
114
+ name: string;
115
+ nativeName: string;
116
+ rtl: boolean;
117
+ }> {
118
+ return Object.entries(LANGUAGES).map(([code, config]) => ({
119
+ code: code as LanguageCode,
120
+ ...config,
121
+ }));
122
+ }
123
+
124
+ /**
125
+ * Get current language
126
+ */
127
+ export function getCurrentLanguage(): LanguageCode {
128
+ return (i18n.language || "en") as LanguageCode;
129
+ }
130
+
131
+ /**
132
+ * Hook-friendly RTL detection
133
+ * Returns current RTL state from I18nManager
134
+ */
135
+ export function isRTL(): boolean {
136
+ return I18nManager.isRTL;
137
+ }
138
+
139
+ /**
140
+ * Get text alignment based on RTL
141
+ * Useful for styling text components
142
+ */
143
+ export function getTextAlign(): "left" | "right" {
144
+ return I18nManager.isRTL ? "right" : "left";
145
+ }
146
+
147
+ /**
148
+ * Get flex direction based on RTL
149
+ * Useful for horizontal layouts
150
+ */
151
+ export function getFlexDirection(): "row" | "row-reverse" {
152
+ return I18nManager.isRTL ? "row-reverse" : "row";
153
+ }
154
+
155
+ /**
156
+ * Get start/end values swapped for RTL
157
+ * Useful for margins, paddings, and positioning
158
+ */
159
+ export function getStartEnd(): { start: "left" | "right"; end: "left" | "right" } {
160
+ return I18nManager.isRTL
161
+ ? { start: "right", end: "left" }
162
+ : { start: "left", end: "right" };
163
+ }
164
+
165
+ /**
166
+ * Transform a value for RTL (e.g., for translateX animations)
167
+ * @param value - The original value
168
+ * @returns The transformed value (negated for RTL)
169
+ */
170
+ export function rtlTransform(value: number): number {
171
+ return I18nManager.isRTL ? -value : value;
172
+ }
173
+
174
+ /**
175
+ * Check if a specific language requires RTL
176
+ */
177
+ export function isLanguageRTL(languageCode: string): boolean {
178
+ const lang = LANGUAGES[languageCode as LanguageCode];
179
+ return lang?.rtl ?? false;
180
+ }
181
+
182
+ /**
183
+ * Force app restart for RTL changes to take effect
184
+ * Call this after changing to/from an RTL language
185
+ */
186
+ export async function applyRTLAndRestart(isRTL: boolean): Promise<void> {
187
+ I18nManager.allowRTL(isRTL);
188
+ I18nManager.forceRTL(isRTL);
189
+ // Note: In production, use expo-updates to reload the app
190
+ // await Updates.reloadAsync();
191
+ }
192
+
193
+ export { i18n };
194
+ export default i18n;
@@ -0,0 +1,101 @@
1
+ {
2
+ "common": {
3
+ "loading": "جاري التحميل...",
4
+ "error": "خطأ",
5
+ "success": "نجاح",
6
+ "cancel": "إلغاء",
7
+ "confirm": "تأكيد",
8
+ "save": "حفظ",
9
+ "delete": "حذف",
10
+ "edit": "تعديل",
11
+ "close": "إغلاق",
12
+ "back": "رجوع",
13
+ "next": "التالي",
14
+ "done": "تم",
15
+ "retry": "إعادة المحاولة",
16
+ "search": "بحث",
17
+ "noResults": "لم يتم العثور على نتائج",
18
+ "offline": "أنت غير متصل",
19
+ "online": "متصل مرة أخرى"
20
+ },
21
+ "auth": {
22
+ "signIn": "تسجيل الدخول",
23
+ "signUp": "إنشاء حساب",
24
+ "signOut": "تسجيل الخروج",
25
+ "email": "البريد الإلكتروني",
26
+ "password": "كلمة المرور",
27
+ "confirmPassword": "تأكيد كلمة المرور",
28
+ "name": "الاسم",
29
+ "forgotPassword": "نسيت كلمة المرور؟",
30
+ "resetPassword": "إعادة تعيين كلمة المرور",
31
+ "noAccount": "ليس لديك حساب؟",
32
+ "haveAccount": "لديك حساب بالفعل؟",
33
+ "welcomeBack": "مرحباً بعودتك!",
34
+ "signInToContinue": "سجل الدخول للمتابعة",
35
+ "createAccount": "إنشاء حساب",
36
+ "joinUs": "انضم إلينا اليوم",
37
+ "enterEmail": "أدخل بريدك الإلكتروني",
38
+ "enterPassword": "أدخل كلمة المرور",
39
+ "enterName": "أدخل اسمك",
40
+ "passwordHint": "8 أحرف على الأقل",
41
+ "invalidCredentials": "البريد الإلكتروني أو كلمة المرور غير صحيحة",
42
+ "accountCreated": "تم إنشاء الحساب بنجاح!",
43
+ "sessionExpired": "انتهت الجلسة. يرجى تسجيل الدخول مرة أخرى.",
44
+ "biometric": {
45
+ "title": "المصادقة البيومترية",
46
+ "prompt": "قم بالمصادقة للمتابعة",
47
+ "fallback": "استخدام رمز المرور",
48
+ "enabled": "تم تفعيل تسجيل الدخول البيومتري",
49
+ "disabled": "تم تعطيل تسجيل الدخول البيومتري",
50
+ "notAvailable": "المصادقة البيومترية غير متاحة"
51
+ }
52
+ },
53
+ "navigation": {
54
+ "home": "الرئيسية",
55
+ "profile": "الملف الشخصي",
56
+ "settings": "الإعدادات",
57
+ "notifications": "الإشعارات"
58
+ },
59
+ "profile": {
60
+ "title": "الملف الشخصي",
61
+ "editProfile": "تعديل الملف الشخصي",
62
+ "changePhoto": "تغيير الصورة",
63
+ "personalInfo": "المعلومات الشخصية",
64
+ "memberSince": "عضو منذ {{date}}"
65
+ },
66
+ "settings": {
67
+ "title": "الإعدادات",
68
+ "appearance": "المظهر",
69
+ "theme": "السمة",
70
+ "themeLight": "فاتح",
71
+ "themeDark": "داكن",
72
+ "themeSystem": "النظام",
73
+ "language": "اللغة",
74
+ "notifications": "الإشعارات",
75
+ "pushNotifications": "إشعارات الدفع",
76
+ "emailNotifications": "إشعارات البريد الإلكتروني",
77
+ "security": "الأمان",
78
+ "biometricAuth": "المصادقة البيومترية",
79
+ "changePassword": "تغيير كلمة المرور",
80
+ "privacy": "سياسة الخصوصية",
81
+ "terms": "شروط الخدمة",
82
+ "about": "حول",
83
+ "version": "الإصدار",
84
+ "deleteAccount": "حذف الحساب"
85
+ },
86
+ "errors": {
87
+ "generic": "حدث خطأ ما",
88
+ "network": "خطأ في الشبكة. يرجى التحقق من اتصالك.",
89
+ "timeout": "انتهت مهلة الطلب. يرجى المحاولة مرة أخرى.",
90
+ "unauthorized": "غير مصرح لك بتنفيذ هذا الإجراء.",
91
+ "notFound": "المورد غير موجود.",
92
+ "validation": "يرجى التحقق من المدخلات والمحاولة مرة أخرى."
93
+ },
94
+ "validation": {
95
+ "required": "هذا الحقل مطلوب",
96
+ "email": "يرجى إدخال بريد إلكتروني صالح",
97
+ "passwordMin": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
98
+ "passwordMatch": "كلمات المرور غير متطابقة",
99
+ "nameMin": "يجب أن يكون الاسم حرفين على الأقل"
100
+ }
101
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "common": {
3
+ "loading": "Wird geladen...",
4
+ "error": "Fehler",
5
+ "success": "Erfolg",
6
+ "cancel": "Abbrechen",
7
+ "confirm": "Bestätigen",
8
+ "save": "Speichern",
9
+ "delete": "Löschen",
10
+ "edit": "Bearbeiten",
11
+ "close": "Schließen",
12
+ "back": "Zurück",
13
+ "next": "Weiter",
14
+ "done": "Fertig",
15
+ "retry": "Erneut versuchen",
16
+ "search": "Suchen",
17
+ "noResults": "Keine Ergebnisse gefunden",
18
+ "offline": "Du bist offline",
19
+ "online": "Wieder online"
20
+ },
21
+ "auth": {
22
+ "signIn": "Anmelden",
23
+ "signUp": "Registrieren",
24
+ "signOut": "Abmelden",
25
+ "email": "E-Mail",
26
+ "password": "Passwort",
27
+ "confirmPassword": "Passwort bestätigen",
28
+ "name": "Name",
29
+ "forgotPassword": "Passwort vergessen?",
30
+ "resetPassword": "Passwort zurücksetzen",
31
+ "noAccount": "Noch kein Konto?",
32
+ "haveAccount": "Bereits ein Konto?",
33
+ "welcomeBack": "Willkommen zurück!",
34
+ "signInToContinue": "Melde dich an, um fortzufahren",
35
+ "createAccount": "Konto erstellen",
36
+ "joinUs": "Werde noch heute Mitglied",
37
+ "enterEmail": "E-Mail eingeben",
38
+ "enterPassword": "Passwort eingeben",
39
+ "enterName": "Name eingeben",
40
+ "passwordHint": "Mindestens 8 Zeichen",
41
+ "invalidCredentials": "Ungültige E-Mail oder Passwort",
42
+ "accountCreated": "Konto erfolgreich erstellt!",
43
+ "sessionExpired": "Sitzung abgelaufen. Bitte erneut anmelden.",
44
+ "biometric": {
45
+ "title": "Biometrische Authentifizierung",
46
+ "prompt": "Authentifiziere dich, um fortzufahren",
47
+ "fallback": "Code verwenden",
48
+ "enabled": "Biometrische Anmeldung aktiviert",
49
+ "disabled": "Biometrische Anmeldung deaktiviert",
50
+ "notAvailable": "Biometrische Authentifizierung nicht verfügbar"
51
+ }
52
+ },
53
+ "navigation": {
54
+ "home": "Startseite",
55
+ "profile": "Profil",
56
+ "settings": "Einstellungen",
57
+ "notifications": "Benachrichtigungen"
58
+ },
59
+ "profile": {
60
+ "title": "Profil",
61
+ "editProfile": "Profil bearbeiten",
62
+ "changePhoto": "Foto ändern",
63
+ "personalInfo": "Persönliche Informationen",
64
+ "memberSince": "Mitglied seit {{date}}"
65
+ },
66
+ "settings": {
67
+ "title": "Einstellungen",
68
+ "appearance": "Erscheinungsbild",
69
+ "theme": "Design",
70
+ "themeLight": "Hell",
71
+ "themeDark": "Dunkel",
72
+ "themeSystem": "System",
73
+ "language": "Sprache",
74
+ "notifications": "Benachrichtigungen",
75
+ "pushNotifications": "Push-Benachrichtigungen",
76
+ "emailNotifications": "E-Mail-Benachrichtigungen",
77
+ "security": "Sicherheit",
78
+ "biometricAuth": "Biometrische Authentifizierung",
79
+ "changePassword": "Passwort ändern",
80
+ "privacy": "Datenschutzrichtlinie",
81
+ "terms": "Nutzungsbedingungen",
82
+ "about": "Über",
83
+ "version": "Version",
84
+ "deleteAccount": "Konto löschen"
85
+ },
86
+ "errors": {
87
+ "generic": "Etwas ist schief gelaufen",
88
+ "network": "Netzwerkfehler. Bitte überprüfe deine Verbindung.",
89
+ "timeout": "Zeitüberschreitung. Bitte erneut versuchen.",
90
+ "unauthorized": "Du bist nicht berechtigt, diese Aktion auszuführen.",
91
+ "notFound": "Ressource nicht gefunden.",
92
+ "validation": "Bitte überprüfe deine Eingaben und versuche es erneut."
93
+ },
94
+ "validation": {
95
+ "required": "Dieses Feld ist erforderlich",
96
+ "email": "Bitte gib eine gültige E-Mail-Adresse ein",
97
+ "passwordMin": "Das Passwort muss mindestens 8 Zeichen lang sein",
98
+ "passwordMatch": "Passwörter stimmen nicht überein",
99
+ "nameMin": "Der Name muss mindestens 2 Zeichen lang sein"
100
+ }
101
+ }