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"
]
}