diff --git a/.idea/tailwindcss.xml b/.idea/tailwindcss.xml new file mode 100644 index 0000000..78a6bc1 --- /dev/null +++ b/.idea/tailwindcss.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index b4bfed3..439e6bd 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,4 @@ { - "plugins": ["prettier-plugin-tailwindcss"] + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindFunctions": ["cva", "cx", "cn"] } diff --git a/localization/ar/settings.json b/localization/ar/settings.json new file mode 100644 index 0000000..5e86b27 --- /dev/null +++ b/localization/ar/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "الملف الشخصي", + "user": "المستخدم", + "appearance": "المظهر", + "notifications": "الإشعارات", + "financial": "المالية", + "limits": "القيود", + "income": "الدخل", + "bankAccounts": "الحسابات المصرفية" +} diff --git a/localization/de/settings.json b/localization/de/settings.json new file mode 100644 index 0000000..228ff95 --- /dev/null +++ b/localization/de/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Profil", + "user": "Benutzer", + "appearance": "Erscheinungsbild", + "notifications": "Benachrichtigungen", + "financial": "Finanzen", + "limits": "Limits", + "income": "Einkommen", + "bankAccounts": "Bankkonten" +} diff --git a/localization/en/settings.json b/localization/en/settings.json index 320c67f..ab8c4fa 100644 --- a/localization/en/settings.json +++ b/localization/en/settings.json @@ -1,3 +1,13 @@ { - "profile": "Profile" + "profile": "Profile", + "user": "User", + "appearance": "Appearance", + "notifications": "Notifications", + "financial": "Financial", + "limits": "Limits", + "income": "Income", + "bankAccounts": "Bank Accounts", + "saveChanges": "Save Changes", + "changePassword": "Change Password", + "changeEmail": "Change Email" } diff --git a/localization/es/settings.json b/localization/es/settings.json new file mode 100644 index 0000000..6726fa5 --- /dev/null +++ b/localization/es/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Perfil", + "user": "Usuario", + "appearance": "Apariencia", + "notifications": "Notificaciones", + "financial": "Finanzas", + "limits": "Límites", + "income": "Ingresos", + "bankAccounts": "Cuentas bancarias" +} diff --git a/localization/fa/settings.json b/localization/fa/settings.json new file mode 100644 index 0000000..04892e0 --- /dev/null +++ b/localization/fa/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "پروفایل", + "user": "کاربر", + "appearance": "ظاهر", + "notifications": "اطلاعیه‌ها", + "financial": "مالی", + "limits": "محدودیت‌ها", + "income": "درآمد", + "bankAccounts": "حساب‌های بانکی" +} diff --git a/localization/fr/settings.json b/localization/fr/settings.json new file mode 100644 index 0000000..4f314bf --- /dev/null +++ b/localization/fr/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Profil", + "user": "Utilisateur", + "appearance": "Apparence", + "notifications": "Notifications", + "financial": "Financier", + "limits": "Limites", + "income": "Revenu", + "bankAccounts": "Comptes bancaires" +} diff --git a/localization/hi/settings.json b/localization/hi/settings.json new file mode 100644 index 0000000..012171e --- /dev/null +++ b/localization/hi/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "प्रोफ़ाइल", + "user": "उपयोगकर्ता", + "appearance": "दिखावट", + "notifications": "सूचनाएँ", + "financial": "वित्तीय", + "limits": "सीमाएँ", + "income": "आय", + "bankAccounts": "बैंक खाते" +} diff --git a/localization/it/settings.json b/localization/it/settings.json new file mode 100644 index 0000000..2b6df2c --- /dev/null +++ b/localization/it/settings.json @@ -0,0 +1,13 @@ +{ + "profile": "Profilo", + "user": "Utente", + "appearance": "Aspetto", + "notifications": "Notifiche", + "financial": "Finanziario", + "limits": "Limiti", + "income": "Reddito", + "bankAccounts": "Conti Bancari", + "saveChanges": "Salva Modifiche", + "changePassword": "Cambia Password", + "changeEmail": "Cambia Email" +} diff --git a/localization/ja/settings.json b/localization/ja/settings.json new file mode 100644 index 0000000..529596e --- /dev/null +++ b/localization/ja/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "プロフィール", + "user": "ユーザー", + "appearance": "外観", + "notifications": "通知", + "financial": "財務", + "limits": "制限", + "income": "収入", + "bankAccounts": "銀行口座" +} diff --git a/localization/ko/settings.json b/localization/ko/settings.json new file mode 100644 index 0000000..cc4095d --- /dev/null +++ b/localization/ko/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "프로필", + "user": "사용자", + "appearance": "모양", + "notifications": "알림", + "financial": "재무", + "limits": "한도", + "income": "수입", + "bankAccounts": "은행 계좌" +} diff --git a/localization/ps/settings.json b/localization/ps/settings.json new file mode 100644 index 0000000..0f4a228 --- /dev/null +++ b/localization/ps/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "پروفایل", + "user": "کارن", + "appearance": "بڼه", + "notifications": "خبرتیاوې", + "financial": "مالي", + "limits": "حدونه", + "income": "عاید", + "bankAccounts": "بانکي حسابونه" +} diff --git a/localization/pt/settings.json b/localization/pt/settings.json new file mode 100644 index 0000000..f3e6133 --- /dev/null +++ b/localization/pt/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Perfil", + "user": "Usuário", + "appearance": "Aparência", + "notifications": "Notificações", + "financial": "Financeiro", + "limits": "Limites", + "income": "Renda", + "bankAccounts": "Contas bancárias" +} diff --git a/localization/ru/settings.json b/localization/ru/settings.json new file mode 100644 index 0000000..f94aae6 --- /dev/null +++ b/localization/ru/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Профиль", + "user": "Пользователь", + "appearance": "Внешний вид", + "notifications": "Уведомления", + "financial": "Финансы", + "limits": "Ограничения", + "income": "Доход", + "bankAccounts": "Банковские счета" +} diff --git a/localization/tr/settings.json b/localization/tr/settings.json new file mode 100644 index 0000000..12d3e75 --- /dev/null +++ b/localization/tr/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "Profil", + "user": "Kullanıcı", + "appearance": "Görünüm", + "notifications": "Bildirimler", + "financial": "Finans", + "limits": "Limitler", + "income": "Gelir", + "bankAccounts": "Banka Hesapları" +} diff --git a/localization/ur/settings.json b/localization/ur/settings.json new file mode 100644 index 0000000..ca0b16d --- /dev/null +++ b/localization/ur/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "پروفائل", + "user": "صارف", + "appearance": "ظاہری شکل", + "notifications": "اطلاعات", + "financial": "مالی", + "limits": "حدود", + "income": "آمدنی", + "bankAccounts": "بینک اکاؤنٹس" +} diff --git a/localization/zh/settings.json b/localization/zh/settings.json new file mode 100644 index 0000000..6e64b01 --- /dev/null +++ b/localization/zh/settings.json @@ -0,0 +1,10 @@ +{ + "profile": "个人资料", + "user": "用户", + "appearance": "外观", + "notifications": "通知", + "financial": "财务", + "limits": "限额", + "income": "收入", + "bankAccounts": "银行账户" +} diff --git a/package.json b/package.json index ef223d9..f739fea 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "format:check": "prettier --check .", "format:write": "prettier --write .", "generate:i18n": "node scripts/generate-i18n-resources.js", + "preinstall": "npx only-allow pnpm", "prestart": "pnpm run generate:i18n", "prebuild": "pnpm run generate:i18n" }, @@ -32,6 +33,7 @@ "@trpc/react-query": "^11.6.0", "@trpc/server": "^11.6.0", "clsx": "^2.1.1", + "cva": "npm:class-variance-authority@^0.7.1", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.6", "expo": "~54.0.12", @@ -74,7 +76,7 @@ "tailwind-merge": "^3.3.1", "tailwind-variants": "^3.1.1", "tailwindcss": "^3.4.18", - "zod": "^4.1.11" + "zod": "^4.1.12" }, "devDependencies": { "@babel/core": "^7.28.4", @@ -96,5 +98,5 @@ "typescript": "^5.9.3" }, "private": true, - "packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa" + "packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94df5b9..e8eb9e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.17.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@clerk/clerk-expo': specifier: ^2.15.4 - version: 2.15.4(@types/react@19.1.17)(expo-auth-session@7.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-secure-store@15.0.7(expo@54.0.12))(expo-web-browser@15.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11) + version: 2.15.4(@types/react@19.1.17)(expo-auth-session@7.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-secure-store@15.0.7(expo@54.0.12))(expo-web-browser@15.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12) '@expo/server': specifier: ^0.7.5 version: 0.7.5 @@ -28,7 +28,7 @@ importers: version: 1.0.1 '@t3-oss/env-core': specifier: ^0.13.6 - version: 0.13.8(typescript@5.9.3)(zod@4.1.11) + version: 0.13.8(typescript@5.9.3)(zod@4.1.12) '@tanstack/react-query': specifier: ^5.90.2 version: 5.90.2(react@19.1.0) @@ -44,6 +44,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cva: + specifier: npm:class-variance-authority@^0.7.1 + version: class-variance-authority@0.7.1 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -171,8 +174,8 @@ importers: specifier: ^3.4.18 version: 3.4.18(tsx@4.20.6)(yaml@2.8.1) zod: - specifier: ^4.1.11 - version: 4.1.11 + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@babel/core': specifier: ^7.28.4 @@ -2983,6 +2986,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-cursor@2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} engines: {node: '>=4'} @@ -7028,8 +7034,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.11: - resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} zustand@5.0.3: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} @@ -7696,15 +7702,15 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@base-org/account@2.0.1(@types/react@19.1.17)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11)': + '@base-org/account@2.0.1(@types/react@19.1.17)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@4.1.11) + ox: 0.6.9(typescript@5.9.3)(zod@4.1.12) preact: 10.24.2 - viem: 2.37.12(typescript@5.9.3)(zod@4.1.11) + viem: 2.37.12(typescript@5.9.3)(zod@4.1.12) zustand: 5.0.3(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) transitivePeerDependencies: - '@types/react' @@ -7729,9 +7735,9 @@ snapshots: - react - react-dom - '@clerk/clerk-expo@2.15.4(@types/react@19.1.17)(expo-auth-session@7.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-secure-store@15.0.7(expo@54.0.12))(expo-web-browser@15.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11)': + '@clerk/clerk-expo@2.15.4(@types/react@19.1.17)(expo-auth-session@7.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(expo-secure-store@15.0.7(expo@54.0.12))(expo-web-browser@15.0.8(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12)': dependencies: - '@clerk/clerk-js': 5.97.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11) + '@clerk/clerk-js': 5.97.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12) '@clerk/clerk-react': 5.49.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@clerk/shared': 3.27.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@clerk/types': 4.90.0 @@ -7755,9 +7761,9 @@ snapshots: - utf-8-validate - zod - '@clerk/clerk-js@5.97.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11)': + '@clerk/clerk-js@5.97.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12)': dependencies: - '@base-org/account': 2.0.1(@types/react@19.1.17)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.11) + '@base-org/account': 2.0.1(@types/react@19.1.17)(react@19.1.0)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.0))(zod@4.1.12) '@clerk/localizations': 3.25.5 '@clerk/shared': 3.27.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@clerk/types': 4.90.0 @@ -9356,10 +9362,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@t3-oss/env-core@0.13.8(typescript@5.9.3)(zod@4.1.11)': + '@t3-oss/env-core@0.13.8(typescript@5.9.3)(zod@4.1.12)': optionalDependencies: typescript: 5.9.3 - zod: 4.1.11 + zod: 4.1.12 '@tanstack/query-core@5.90.2': {} @@ -9747,15 +9753,15 @@ snapshots: abab@2.0.6: {} - abitype@1.1.0(typescript@5.9.3)(zod@4.1.11): + abitype@1.1.0(typescript@5.9.3)(zod@4.1.12): optionalDependencies: typescript: 5.9.3 - zod: 4.1.11 + zod: 4.1.12 - abitype@1.1.1(typescript@5.9.3)(zod@4.1.11): + abitype@1.1.1(typescript@5.9.3)(zod@4.1.12): optionalDependencies: typescript: 5.9.3 - zod: 4.1.11 + zod: 4.1.12 abort-controller@3.0.0: dependencies: @@ -10255,6 +10261,10 @@ snapshots: cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + cli-cursor@2.1.0: dependencies: restore-cursor: 2.0.0 @@ -13292,21 +13302,21 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - ox@0.6.9(typescript@5.9.3)(zod@4.1.11): + ox@0.6.9(typescript@5.9.3)(zod@4.1.12): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.1(typescript@5.9.3)(zod@4.1.11) + abitype: 1.1.1(typescript@5.9.3)(zod@4.1.12) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.9.3)(zod@4.1.11): + ox@0.9.6(typescript@5.9.3)(zod@4.1.12): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -13314,7 +13324,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@4.1.11) + abitype: 1.1.0(typescript@5.9.3)(zod@4.1.12) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -14670,15 +14680,15 @@ snapshots: - '@types/react' - '@types/react-dom' - viem@2.37.12(typescript@5.9.3)(zod@4.1.11): + viem@2.37.12(typescript@5.9.3)(zod@4.1.12): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.9.3)(zod@4.1.11) + abitype: 1.1.0(typescript@5.9.3)(zod@4.1.12) isows: 1.0.7(ws@8.18.3) - ox: 0.9.6(typescript@5.9.3)(zod@4.1.11) + ox: 0.9.6(typescript@5.9.3)(zod@4.1.12) ws: 8.18.3 optionalDependencies: typescript: 5.9.3 @@ -14914,7 +14924,7 @@ snapshots: zod@3.25.76: {} - zod@4.1.11: {} + zod@4.1.12: {} zustand@5.0.3(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0d110a0..8fe03ce 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,9 @@ packages: - - "." + - . onlyBuiltDependencies: - "@clerk/shared" + - "@swc/core" - browser-tabs-lock - core-js - esbuild diff --git a/src/app/(auth)/sign-in.tsx b/src/app/(auth)/sign-in.tsx index b9be1f5..cf992c0 100644 --- a/src/app/(auth)/sign-in.tsx +++ b/src/app/(auth)/sign-in.tsx @@ -2,7 +2,6 @@ import { useSignIn } from "@clerk/clerk-expo"; import { useRouter } from "expo-router"; import React, { useCallback, useState, useEffect } from "react"; import { - TextInput, View, TouchableOpacity, KeyboardAvoidingView, @@ -15,6 +14,7 @@ import "~/i18n"; import { languageService } from "~/services/languageService"; import Button from "~/components/ui/button"; import AppImage from "~/components/ui/AppImage"; +import { TextInput } from "~/components/ui/text-input"; export default function Page() { const { signIn, setActive, isLoaded } = useSignIn(); @@ -161,10 +161,8 @@ export default function Page() { Finance.io - - {t("email")} - )} - - {t("password")} - { const initLanguage = async () => { try { @@ -204,10 +204,8 @@ export default function SignUpScreen() { {!pendingVerification ? ( <> - - {t("firstName")} - )} - - {t("lastName")} - - {t("email")} - )} - - {t("password")} - { const { colors } = useTheme(); const navigationOptions: NavigationOption[] = [ { - name: "banking/index", + name: "banking", title: t(NavigationItems.BANKING), icon: CreditCardIcon, }, { - name: "insights/index", + name: "insights", title: t(NavigationItems.INSIGHTS), icon: ChartColumnIcon, }, { - name: "settings/index", + name: "settings", title: t(NavigationItems.SETTINGS), icon: SettingsIcon, }, diff --git a/src/app/(tabs)/banking/_layout.tsx b/src/app/(tabs)/banking/_layout.tsx new file mode 100644 index 0000000..ecc26e3 --- /dev/null +++ b/src/app/(tabs)/banking/_layout.tsx @@ -0,0 +1,15 @@ +import { Stack } from "expo-router"; + +export const unstable_settings = { + initialRouteName: "index", +}; + +export default function BankingLayout() { + return ( + + ); +} diff --git a/src/app/(tabs)/insights/_layout.tsx b/src/app/(tabs)/insights/_layout.tsx new file mode 100644 index 0000000..6af99ac --- /dev/null +++ b/src/app/(tabs)/insights/_layout.tsx @@ -0,0 +1,15 @@ +import { Stack } from "expo-router"; + +export const unstable_settings = { + initialRouteName: "index", +}; + +export default function InsightsLayout() { + return ( + + ); +} diff --git a/src/app/(tabs)/insights/index.tsx b/src/app/(tabs)/insights/index.tsx index 45e203d..697d772 100644 --- a/src/app/(tabs)/insights/index.tsx +++ b/src/app/(tabs)/insights/index.tsx @@ -14,21 +14,14 @@ export default function InsightsScreen() { const { t } = useTranslation(); return ( - + - + {/* Header */} -
+
diff --git a/src/app/(tabs)/settings/(profile)/change-email.tsx b/src/app/(tabs)/settings/(profile)/change-email.tsx new file mode 100644 index 0000000..0e49281 --- /dev/null +++ b/src/app/(tabs)/settings/(profile)/change-email.tsx @@ -0,0 +1,224 @@ +import { SafeAreaView } from "react-native-safe-area-context"; +import { Header } from "~/components/Header"; +import { NavigationItems } from "~/types"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TextInput } from "~/components/ui/text-input"; +import Button from "~/components/ui/button"; +import { View, Alert, ActivityIndicator } from "react-native"; +import { useUser } from "@clerk/clerk-expo"; +import AppText from "~/components/ui/AppText"; +import { useRouter } from "expo-router"; + +const ChangeEmailPage = () => { + const { t: tCommon } = useTranslation("common"); + const { t: tSettings } = useTranslation("settings"); + const { user, isLoaded } = useUser(); + const router = useRouter(); + + const [newEmail, setNewEmail] = useState(""); + const [verificationCode, setVerificationCode] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + const [showVerification, setShowVerification] = useState(false); + const [emailAddressId, setEmailAddressId] = useState(null); + + const handleSendVerification = async () => { + if (!user) { + Alert.alert("Error", "User not found. Please sign in again."); + return; + } + + if (!newEmail) { + Alert.alert("Error", "Please enter a new email address."); + return; + } + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(newEmail)) { + Alert.alert("Error", "Please enter a valid email address."); + return; + } + + // Check if the new email is the same as the current one + if (newEmail === user.primaryEmailAddress?.emailAddress) { + Alert.alert("Error", "This is already your current email address."); + return; + } + + setIsUpdating(true); + try { + const emailAddress = await user.createEmailAddress({ email: newEmail }); + await emailAddress.prepareVerification({ strategy: "email_code" }); + + setEmailAddressId(emailAddress.id); + setShowVerification(true); + + Alert.alert( + "Verification Sent", + `A verification code has been sent to ${newEmail}. Please check your inbox.`, + ); + } catch (error: any) { + console.error("Error sending verification:", error); + Alert.alert( + "Error", + error?.errors?.[0]?.message || + "Failed to send verification email. Please try again.", + ); + } finally { + setIsUpdating(false); + } + }; + + const handleVerifyEmail = async () => { + if (!user || !emailAddressId) { + Alert.alert("Error", "Session expired. Please start over."); + setShowVerification(false); + return; + } + + if (!verificationCode) { + Alert.alert("Error", "Please enter the verification code."); + return; + } + + setIsUpdating(true); + try { + const emailAddress = user.emailAddresses.find( + (e) => e.id === emailAddressId, + ); + + if (!emailAddress) { + throw new Error("Email address not found."); + } + + await emailAddress.attemptVerification({ code: verificationCode }); + + // Set as primary email + await user.update({ + primaryEmailAddressId: emailAddressId, + }); + + Alert.alert( + "Success", + "Your email address has been changed successfully!", + [ + { + text: "OK", + onPress: () => router.back(), + }, + ], + ); + } catch (error: any) { + console.error("Error verifying email:", error); + Alert.alert( + "Error", + error?.errors?.[0]?.message || + "Invalid verification code. Please try again.", + ); + } finally { + setIsUpdating(false); + } + }; + + const handleCancel = () => { + router.back(); + }; + + if (!isLoaded) { + return ( + + + + ); + } + + if (!user) { + return ( + + + Please sign in to change your email. + + + ); + } + + return ( + +
+ + + {tSettings("changeEmail") || "Change Email"} + + + + Current email: {user.primaryEmailAddress?.emailAddress} + + + {!showVerification ? ( + <> + + + + + + + ) : ( + <> + + A verification code has been sent to {newEmail}. Please enter it + below to confirm your new email address. + + + + + + + + + + + )} + + + ); +}; + +export default ChangeEmailPage; diff --git a/src/app/(tabs)/settings/(profile)/change-password.tsx b/src/app/(tabs)/settings/(profile)/change-password.tsx new file mode 100644 index 0000000..29e2c3f --- /dev/null +++ b/src/app/(tabs)/settings/(profile)/change-password.tsx @@ -0,0 +1,147 @@ +import { SafeAreaView } from "react-native-safe-area-context"; +import { Header } from "~/components/Header"; +import { NavigationItems } from "~/types"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { TextInput } from "~/components/ui/text-input"; +import Button from "~/components/ui/button"; +import { View, Alert, ActivityIndicator } from "react-native"; +import { useUser } from "@clerk/clerk-expo"; +import AppText from "~/components/ui/AppText"; +import { useRouter } from "expo-router"; + +const ChangePasswordPage = () => { + const { t: tSettings } = useTranslation("settings"); + const { user, isLoaded } = useUser(); + const router = useRouter(); + + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + + const handleChangePassword = async () => { + if (!user) { + Alert.alert("Error", "User not found. Please sign in again."); + return; + } + + // Validation + if (!currentPassword || !newPassword || !confirmPassword) { + Alert.alert("Error", "Please fill in all fields."); + return; + } + + if (newPassword !== confirmPassword) { + Alert.alert("Error", "New passwords do not match."); + return; + } + + if (newPassword.length < 8) { + Alert.alert("Error", "Password must be at least 8 characters long."); + return; + } + + setIsUpdating(true); + try { + await user.updatePassword({ + currentPassword: currentPassword, + newPassword: newPassword, + }); + + Alert.alert("Success", "Password updated successfully!", [ + { + text: "OK", + onPress: () => router.back(), + }, + ]); + + // Clear fields + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } catch (error: any) { + console.error("Error updating password:", error); + const errorMessage = + error?.errors?.[0]?.message || + "Failed to update password. Please check your current password and try again."; + Alert.alert("Error", errorMessage); + } finally { + setIsUpdating(false); + } + }; + + if (!isLoaded) { + return ( + + + + ); + } + + if (!user) { + return ( + + + Please sign in to change your password. + + + ); + } + + return ( + +
+ + + {tSettings("changePassword")} + + + + + + + + + + Password must be at least 8 characters long + + + + + + + + ); +}; + +export default ChangePasswordPage; diff --git a/src/app/(tabs)/settings/(profile)/profile.tsx b/src/app/(tabs)/settings/(profile)/profile.tsx new file mode 100644 index 0000000..dafb965 --- /dev/null +++ b/src/app/(tabs)/settings/(profile)/profile.tsx @@ -0,0 +1,109 @@ +import { SafeAreaView } from "react-native-safe-area-context"; +import { Header } from "~/components/Header"; +import { NavigationItems } from "~/types"; +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { TextInput } from "~/components/ui/text-input"; +import Button from "~/components/ui/button"; +import { View, Alert, ActivityIndicator } from "react-native"; +import { useUser } from "@clerk/clerk-expo"; +import AppText from "~/components/ui/AppText"; + +const ProfilePage = () => { + const { t: tCommon } = useTranslation("common"); + const { t: tSettings } = useTranslation("settings"); + const { user, isLoaded } = useUser(); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + + // Load user data when component mounts + useEffect(() => { + if (user) { + setFirstName(user.firstName || ""); + setLastName(user.lastName || ""); + setEmail(user.primaryEmailAddress?.emailAddress || ""); + } + }, [user]); + + const handleSaveChanges = async () => { + if (!user) { + Alert.alert("Error", "User not found. Please sign in again."); + return; + } + + setIsUpdating(true); + try { + await user.update({ + firstName: firstName, + lastName: lastName, + }); + + Alert.alert("Success", "Profile updated successfully!"); + } catch (error) { + console.error("Error updating profile:", error); + Alert.alert("Error", "Failed to update profile. Please try again."); + } finally { + setIsUpdating(false); + } + }; + + if (!isLoaded) { + return ( + + + + ); + } + + if (!user) { + return ( + + + Please sign in to view your profile. + + + ); + } + + return ( + +
+ + + + + + + + + ); +}; + +export default ProfilePage; diff --git a/src/app/(tabs)/settings/_layout.tsx b/src/app/(tabs)/settings/_layout.tsx new file mode 100644 index 0000000..b6358c7 --- /dev/null +++ b/src/app/(tabs)/settings/_layout.tsx @@ -0,0 +1,15 @@ +import { Stack } from "expo-router"; + +export const unstable_settings = { + initialRouteName: "index", +}; + +export default function SettingsLayout() { + return ( + + ); +} diff --git a/src/app/(tabs)/settings/appearance.tsx b/src/app/(tabs)/settings/appearance.tsx new file mode 100644 index 0000000..aeca802 --- /dev/null +++ b/src/app/(tabs)/settings/appearance.tsx @@ -0,0 +1,12 @@ +import AppText from "~/components/ui/AppText"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const ProfilePage = () => { + return ( + + Hiiii + + ); +}; + +export default ProfilePage; diff --git a/src/app/(tabs)/settings/bank-accounts.tsx b/src/app/(tabs)/settings/bank-accounts.tsx new file mode 100644 index 0000000..aeca802 --- /dev/null +++ b/src/app/(tabs)/settings/bank-accounts.tsx @@ -0,0 +1,12 @@ +import AppText from "~/components/ui/AppText"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const ProfilePage = () => { + return ( + + Hiiii + + ); +}; + +export default ProfilePage; diff --git a/src/app/(tabs)/settings/income.tsx b/src/app/(tabs)/settings/income.tsx new file mode 100644 index 0000000..aeca802 --- /dev/null +++ b/src/app/(tabs)/settings/income.tsx @@ -0,0 +1,12 @@ +import AppText from "~/components/ui/AppText"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const ProfilePage = () => { + return ( + + Hiiii + + ); +}; + +export default ProfilePage; diff --git a/src/app/(tabs)/settings/index.tsx b/src/app/(tabs)/settings/index.tsx index fa96cbc..d0e4c29 100644 --- a/src/app/(tabs)/settings/index.tsx +++ b/src/app/(tabs)/settings/index.tsx @@ -1,43 +1,140 @@ import React from "react"; +import { ScrollView, StatusBar, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; import { useTheme } from "~/contexts/ThemeContext"; -import { ScrollView, StatusBar } from "react-native"; import { Header } from "~/components/Header"; import { SectionHeader } from "~/components/SectionHeader"; import { NavigationItems } from "~/types"; -import { useUser } from "@clerk/clerk-expo"; import { useTranslation } from "react-i18next"; -import { SafeAreaView } from "react-native-safe-area-context"; +import AppText from "~/components/ui/AppText"; +import { Container } from "~/components/ui/container"; +import { + BellRingIcon, + LandmarkIcon, + ShieldXIcon, + SunMoonIcon, + UsersRoundIcon, +} from "lucide-react-native"; +import MoneyIcon from "~/assets/Icons/money.png"; +import { Link } from "expo-router"; export default function SettingsScreen() { const { colors, isDark } = useTheme(); - const { user, isLoaded } = useUser(); - const { t } = useTranslation(); + const { t } = useTranslation("settings"); + return ( - + - + {/* Header */} -
+
+ + + + + {t("profile")} + + + + + + + {t("appearance")} + + + + + + + {t("notifications")} + + + + + + + + + + {t("limits")} + + + + + + + + {t("income")} + + + + + + + + {t("bankAccounts")} + + + + ); diff --git a/src/app/(tabs)/settings/limits.tsx b/src/app/(tabs)/settings/limits.tsx new file mode 100644 index 0000000..aeca802 --- /dev/null +++ b/src/app/(tabs)/settings/limits.tsx @@ -0,0 +1,12 @@ +import AppText from "~/components/ui/AppText"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const ProfilePage = () => { + return ( + + Hiiii + + ); +}; + +export default ProfilePage; diff --git a/src/app/(tabs)/settings/notifications.tsx b/src/app/(tabs)/settings/notifications.tsx new file mode 100644 index 0000000..aeca802 --- /dev/null +++ b/src/app/(tabs)/settings/notifications.tsx @@ -0,0 +1,12 @@ +import AppText from "~/components/ui/AppText"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const ProfilePage = () => { + return ( + + Hiiii + + ); +}; + +export default ProfilePage; diff --git a/src/app/start/index.tsx b/src/app/start/index.tsx index 6fef10f..4776bc2 100644 --- a/src/app/start/index.tsx +++ b/src/app/start/index.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react"; import { ScrollView, View, - TextInput, KeyboardAvoidingView, Platform, Alert, @@ -10,7 +9,7 @@ import { } from "react-native"; import AppText from "~/components/ui/AppText"; import { useUser } from "@clerk/clerk-expo"; -import { Link } from "lucide-react-native"; +import { LandmarkIcon, Link } from "lucide-react-native"; import { trpc } from "~/utils/trpc"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -21,6 +20,7 @@ import Button from "~/components/ui/button"; import { useRouter } from "expo-router"; import AppImage from "~/components/ui/AppImage"; import { WelcomeFormValues, welcomeSchema } from "~/schemas/welcomeSchema"; +import { TextInput } from "~/components/ui/text-input"; const Home = () => { const { user, isLoaded } = useUser(); @@ -85,7 +85,7 @@ const Home = () => { behavior={Platform.OS === "ios" ? "padding" : "height"} className="flex-1" > - + { {/*Link Icon*/} - + { {/*Input Fields*/} - - {t("bankName")} - ( - - - - - - + )} /> {errors.bankName && ( @@ -159,60 +147,48 @@ const Home = () => { )} - - {t("currentAmount")} - ( - - - - - { - // Only allow numbers and one decimal point - const sanitized = text.replace(/[^0-9.]/g, ""); - // Ensure only one decimal point is allowed - const parts = sanitized.split("."); - const formattedText = - parts.length > 2 - ? parts[0] + "." + parts.slice(1).join("") - : sanitized; + { + // Only allow numbers and one decimal point + const sanitized = text.replace(/[^0-9.]/g, ""); + // Ensure only one decimal point is allowed + const parts = sanitized.split("."); + const formattedText = + parts.length > 2 + ? parts[0] + "." + parts.slice(1).join("") + : sanitized; - // Limit decimal places to 2 - let finalText = formattedText; - if (parts.length === 2 && parts[1].length > 2) { - finalText = parts[0] + "." + parts[1].slice(0, 2); - } + // Limit decimal places to 2 + let finalText = formattedText; + if (parts.length === 2 && parts[1].length > 2) { + finalText = parts[0] + "." + parts[1].slice(0, 2); + } - setAmountDisplay(finalText); + setAmountDisplay(finalText); - // Update form value - if (finalText === "" || finalText === ".") { - onChange(0); - } else { - onChange(parseFloat(finalText) || 0); - } - }} - /> - + // Update form value + if (finalText === "" || finalText === ".") { + onChange(0); + } else { + onChange(parseFloat(finalText) || 0); + } + }} + /> )} /> {errors.currentAmount && ( @@ -221,36 +197,24 @@ const Home = () => { )} - - {t("reference")} - ( - - - - - - + )} /> {errors.reference && ( @@ -259,36 +223,24 @@ const Home = () => { )} - - {t("usage")} - ( - - - - - - + )} /> {errors.usage && ( diff --git a/src/assets/Icons/bank.png b/src/assets/Icons/bank.png deleted file mode 100644 index 7dbebf1..0000000 Binary files a/src/assets/Icons/bank.png and /dev/null differ diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 3967d14..b75ac23 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -9,18 +9,19 @@ import AppText from "./ui/AppText"; import AppImage from "~/components/ui/AppImage"; import { useTranslation } from "react-i18next"; import { NavigationItems } from "~/types"; +import { useUser } from "@clerk/clerk-expo"; const AnimatedView = Animated.createAnimatedComponent(Animated.View); interface HeaderProps { - name: string; type: NavigationItems; // enum key, not a translated string } -export const Header: React.FC = ({ name, type }) => { +export const Header: React.FC = ({ type }) => { const { colors } = useTheme(); const headerOpacity = useSharedValue(0); const { t } = useTranslation(); + const { user, isLoaded } = useUser(); const headerAnimatedStyle = useAnimatedStyle(() => ({ opacity: withSpring(headerOpacity.value), @@ -32,19 +33,22 @@ export const Header: React.FC = ({ name, type }) => { return ( - {name} - {t(type)} + {isLoaded && user + ? user.firstName || t("defaultUser") + : t("defaultUser")}{" "} + - {t(type)} ); diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx index aa48a06..e247d65 100644 --- a/src/components/SectionHeader.tsx +++ b/src/components/SectionHeader.tsx @@ -1,32 +1,37 @@ import type React from "react"; import Animated, { FadeInDown } from "react-native-reanimated"; -import { useTheme } from "../contexts/ThemeContext"; +import { useTheme } from "~/contexts/ThemeContext"; import AppText from "./ui/AppText"; +import { cn } from "~/utils/lib"; interface SectionHeaderProps { title: string; delay?: number; - size?: "large" | "medium" | "small"; + size?: "large" | "medium" | "small" | "xl"; + className?: string; } export const SectionHeader: React.FC = ({ title, delay = 0, size = "large", + className, }) => { const { colors } = useTheme(); const sizeClassMap = { - large: "text-xl mt-6 font-bold", - medium: "text-base mt-4 font-semibold", - small: "text-sm mt-3 font-medium", + xl: "text-2xl mt-8", + large: "text-xl mt-6", + medium: "text-base mt-4", + small: "text-sm mt-3", }; return ( {title} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e1cf0ac..f8fb971 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -8,6 +8,7 @@ import { import { cn } from "~/utils/lib"; import { Href, useRouter } from "expo-router"; import * as Linking from "expo-linking"; +import { cva, VariantProps } from "cva"; /** * Button component for navigation and external linking. @@ -22,12 +23,37 @@ import * as Linking from "expo-linking"; interface ButtonProps extends TouchableOpacityProps { /** * Internal route (Href) or external URL (string). - * If string starts with "http" or "mailto", opens externally. + * If the string starts with "http" or "mailto", it opens externally. * Otherwise, uses Expo Router for navigation. */ href?: Href; + /** + * An optional property that defines the variants of a button. + * It determines the style variations of the button, such as size, color, or theme. + * The `ButtonVariants` type specifies the allowed options for this property. + */ + variants?: ButtonVariants; } +const buttonVariants = cva( + "disabled:bg-gray-400 shadow-cyan-500/50 mt-5 self-center rounded-xl px-8 py-2", + { + variants: { + intent: { + // TODO: Shadow (like the one we have in Figma) is somehow not working, didnt get it working, will skip this for a later PR + default: "bg-accent", + secondary: + "border-2 border-stroke bg-secondary dark:border-dark-stroke dark:bg-dark-secondary", + }, + }, + defaultVariants: { + intent: "default", + }, + }, +); + +export type ButtonVariants = VariantProps; + const Button: React.FC = ({ href, ...props }) => { const router = useRouter(); @@ -69,11 +95,7 @@ const Button: React.FC = ({ href, ...props }) => { {props.children} diff --git a/src/components/ui/container.tsx b/src/components/ui/container.tsx new file mode 100644 index 0000000..49b02a5 --- /dev/null +++ b/src/components/ui/container.tsx @@ -0,0 +1,88 @@ +/** + * Container component + * + * A reusable UI wrapper that: + * - Renders a leading icon within a colored circular badge + * - Shows arbitrary children content next to the icon + * - Displays a trailing chevron/arrow for affordance + * + * Styling is done via Tailwind classes interpreted by NativeWind. + * Icon input can be either: + * - A Lucide icon component (function, forwardRef, or memo) + * - A static image source (ImageSourcePropType) rendered via AppImage + */ + +import { ImageSourcePropType, View } from "react-native"; +import { cn, isLucideIcon } from "~/utils/lib"; +import { CircleArrowRight, LucideIcon } from "lucide-react-native"; +import { cssInterop } from "nativewind"; +import AppImage from "~/components/ui/AppImage"; + +/** + * Container props + * + * children: React content to render next to the icon + * className: Additional Tailwind classes to merge with the default + * icon: Either a Lucide icon component or an image source for AppImage + */ +export const Container = ({ + children, + className, + icon: Icon, +}: { + children: React.ReactNode; + className?: string; + icon: LucideIcon | ImageSourcePropType; +}) => { + // Create a NativeWind-interoperable version of the trailing arrow icon + // so we can style it with `className`. + const StyledCircleArrowRight = cssInterop(CircleArrowRight, { + className: { target: "style" }, + }); + + // Resolve the "icon" prop into a concrete React node: + // - If it's a Lucide component, wrap with cssInterop for className support. + // - Otherwise, treat it as an image source and render via AppImage. + let IconNode: React.ReactNode; + if (isLucideIcon(Icon)) { + // Adapt the incoming Lucide icon component to accept `className` + const StyledIcon = cssInterop(Icon, { className: { target: "style" } }); + IconNode = ; + } else { + // Render provided an image source with constrained size and containment + IconNode = ( + + ); + } + + return ( + + {/* Left side: circular icon badge + children content */} + + {/* Circular badge that hosts the icon node */} + + {IconNode} + + + {/* Consumer-provided content (text, actions, etc.) */} + {children} + + + {/* Right side: affordance icon (e.g., navigation or action hint) */} + + + ); +}; diff --git a/src/components/ui/text-input.tsx b/src/components/ui/text-input.tsx new file mode 100644 index 0000000..d6b1ee4 --- /dev/null +++ b/src/components/ui/text-input.tsx @@ -0,0 +1,97 @@ +/** + * TextInput wrapper component for React Native with a labeled header. + * - Uses NativeWind Tailwind classes via `className`. + * - Enforces project rule to use `AppText` instead of `Text`. + * - Provides sensible defaults for accessibility and keyboard behavior. + * - Accepts all React Native `TextInputProps` and a required `name` for the label. + */ + +import AppText from "~/components/ui/AppText"; +import React from "react"; +import { + TextInputProps, + TextInput as RNTextInput, + View, + ImageSourcePropType, +} from "react-native"; +import { cn, isLucideIcon } from "~/utils/lib"; +import { LucideIcon } from "lucide-react-native"; +import AppImage from "~/components/ui/AppImage"; +import { cssInterop } from "nativewind"; + +/** + * Props: + * - `name`: Visible field label and base for accessibility labels. + * - `required`: If true, shows an asterisk (*) in the accent colour next to the label. + * - `icon`: Optional icon (LucideIcon or ImageSourcePropType) to display inside the input. + * - `className`: + * Additional Tailwind classes for the input container to merge with the default NOT applying to the input itself. + * - `...TextInputProps`: Forwarded to RN TextInput. + * Explicit props override defaults. + */ +export const TextInput = ({ + name, + required, + icon, + className, + ...props +}: { + name: string; + required?: boolean; + icon?: LucideIcon | ImageSourcePropType; +} & TextInputProps) => { + // Resolve the "icon" prop into a concrete React node + let IconNode: React.ReactNode | null = null; + if (icon) { + if (isLucideIcon(icon)) { + const StyledIcon = cssInterop(icon, { className: { target: "style" } }); + IconNode = ( + + ); + } else { + IconNode = ( + + ); + } + } + + return ( + <> + + {required && *} + + {name} + + + + + {IconNode && ( + + {IconNode} + + )} + + + + ); +}; diff --git a/src/i18n-resources.ts b/src/i18n-resources.ts index 88ba5e5..bff90e2 100644 --- a/src/i18n-resources.ts +++ b/src/i18n-resources.ts @@ -2,22 +2,37 @@ // Generated by generate-i18n-resources.js import ar_common from "../localization/ar/common.json"; +import ar_settings from "../localization/ar/settings.json"; import de_common from "../localization/de/common.json"; +import de_settings from "../localization/de/settings.json"; import en_common from "../localization/en/common.json"; import en_settings from "../localization/en/settings.json"; import es_common from "../localization/es/common.json"; +import es_settings from "../localization/es/settings.json"; import fa_common from "../localization/fa/common.json"; +import fa_settings from "../localization/fa/settings.json"; import fr_common from "../localization/fr/common.json"; +import fr_settings from "../localization/fr/settings.json"; import hi_common from "../localization/hi/common.json"; +import hi_settings from "../localization/hi/settings.json"; import it_common from "../localization/it/common.json"; +import it_settings from "../localization/it/settings.json"; import ja_common from "../localization/ja/common.json"; +import ja_settings from "../localization/ja/settings.json"; import ko_common from "../localization/ko/common.json"; +import ko_settings from "../localization/ko/settings.json"; import ps_common from "../localization/ps/common.json"; +import ps_settings from "../localization/ps/settings.json"; import pt_common from "../localization/pt/common.json"; +import pt_settings from "../localization/pt/settings.json"; import ru_common from "../localization/ru/common.json"; +import ru_settings from "../localization/ru/settings.json"; import tr_common from "../localization/tr/common.json"; +import tr_settings from "../localization/tr/settings.json"; import ur_common from "../localization/ur/common.json"; +import ur_settings from "../localization/ur/settings.json"; import zh_common from "../localization/zh/common.json"; +import zh_settings from "../localization/zh/settings.json"; import th_common from "../localization/th/common.json"; import id_common from "../localization/id/common.json"; import vi_common from "../localization/vi/common.json"; @@ -28,11 +43,11 @@ import bn_common from "../localization/bn/common.json"; export const resources = { ar: { common: ar_common, - settings: {}, + settings: ar_settings, }, de: { common: de_common, - settings: {}, + settings: de_settings, }, en: { common: en_common, @@ -40,55 +55,55 @@ export const resources = { }, es: { common: es_common, - settings: {}, + settings: es_settings, }, fa: { common: fa_common, - settings: {}, + settings: fa_settings, }, fr: { common: fr_common, - settings: {}, + settings: fr_settings, }, hi: { common: hi_common, - settings: {}, + settings: hi_settings, }, it: { common: it_common, - settings: {}, + settings: it_settings, }, ja: { common: ja_common, - settings: {}, + settings: ja_settings, }, ko: { common: ko_common, - settings: {}, + settings: ko_settings, }, ps: { common: ps_common, - settings: {}, + settings: ps_settings, }, pt: { common: pt_common, - settings: {}, + settings: pt_settings, }, ru: { common: ru_common, - settings: {}, + settings: ru_settings, }, tr: { common: tr_common, - settings: {}, + settings: tr_settings, }, ur: { common: ur_common, - settings: {}, + settings: ur_settings, }, zh: { common: zh_common, - settings: {}, + settings: zh_settings, }, th: { common: th_common, diff --git a/src/types/images.d.ts b/src/types/images.d.ts new file mode 100644 index 0000000..76db7f9 --- /dev/null +++ b/src/types/images.d.ts @@ -0,0 +1,4 @@ +declare module "*.png" { + const value: import("react-native").ImageSourcePropType; + export default value; +} diff --git a/src/types/index.ts b/src/types/index.ts index a93d057..1a7294d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -117,6 +117,7 @@ export enum NavigationItems { INSIGHTS = "navigationInsights", /** Settings/preferences area: profile, security, language. */ SETTINGS = "navigationSettings", + PROFILE = "navigationProfile", /** Banking/accounts area: balances, transfers, cards. */ BANKING = "navigationBankAccounts", } diff --git a/src/utils/lib.ts b/src/utils/lib.ts index a5ef193..0102a13 100644 --- a/src/utils/lib.ts +++ b/src/utils/lib.ts @@ -1,6 +1,39 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { LucideIcon } from "lucide-react-native"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** + * Type guard to determine whether a value is a Lucide icon component. + * + * Accepts: + * - Function components (common case for Lucide icons) + * - React.forwardRef(...) wrapped components + * - React.memo(...) wrapped components + * + * Notes: + * - We peek at the internal React $$$typeof symbol to differentiate memo/forwardRef. + * - This avoids false positives when an image source (object) is provided instead. + */ +export const isLucideIcon = (val: unknown): val is LucideIcon => { + // Fast-fail nullish values + if (!val) return false; + + // Plain function component (most Lucide exports) + if (typeof val === "function") return true; + + // Object-wrapped components (forwardRef or memo) + if (typeof val === "object") { + const t = (val as any).$$typeof; + // Detect React.forwardRef / React.memo component types + return ( + t === Symbol.for("react.forward_ref") || t === Symbol.for("react.memo") + ); + } + + // Anything else (e.g., numbers, strings) is not a component + return false; +}; diff --git a/tailwind.config.js b/tailwind.config.js index 36d6876..aadc89b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -10,7 +10,7 @@ module.exports = { theme: { // Need to be the same as in Figma, keep these in sync. - // Also keep everything with /src/contexts/ThemeContext.tsx in sync. + // Also, keep everything with /src/contexts/ThemeContext.tsx in sync. colors: { background: "hsl(240, 60%, 99.02%)", primary: "hsl(243.33, 40.91%, 91.37%)", diff --git a/tsconfig.json b/tsconfig.json index 9d66740..d0b801a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "**/*.tsx", "nativewind-env.d.ts", ".expo/types/**/*.ts", - "expo-env.d.ts" + "expo-env.d.ts", + "src", + "**/*.d.ts" ] }