From 8bde3d4f21811eb45e8c6cf68af7de59f00d38b9 Mon Sep 17 00:00:00 2001 From: Arthur Lempereur Date: Thu, 23 Oct 2025 14:36:36 +0200 Subject: [PATCH] Initial commit: WalletTracker app with Firebase integration - Setup Expo project with TypeScript - Implement authentication (Login/Signup/Logout) - Create Dashboard, Transactions, Subscriptions, and Analysis screens - Add Firebase services (Auth, Firestore, Storage) - Implement real-time synchronization - Add charts and analytics - Create reusable components (Button, InputText, TransactionCard, SubscriptionCard) - Configure React Navigation with bottom tabs - Add Firestore security rules - Create comprehensive documentation (README, FIREBASE_SETUP, TESTING) --- App.tsx | 18 +- FIREBASE_SETUP.md | 158 +++ README.md | 191 ++++ TESTING.md | 243 +++++ app.json | 23 +- firestore.rules | 92 ++ package-lock.json | 1534 ++++++++++++++++++++++++++- package.json | 15 +- src/components/Button.tsx | 103 ++ src/components/InputText.tsx | 51 + src/components/SubscriptionCard.tsx | 160 +++ src/components/TransactionCard.tsx | 125 +++ src/config/firebase.ts | 27 + src/hooks/useAuth.ts | 105 ++ src/navigation/AppNavigator.tsx | 117 ++ src/screens/AnalysisScreen.tsx | 480 +++++++++ src/screens/DashboardScreen.tsx | 317 ++++++ src/screens/LoginScreen.tsx | 150 +++ src/screens/SignupScreen.tsx | 202 ++++ src/screens/SubscriptionScreen.tsx | 444 ++++++++ src/screens/TransactionScreen.tsx | 414 ++++++++ src/services/authService.ts | 96 ++ src/services/categoryService.ts | 156 +++ src/services/subscriptionService.ts | 149 +++ src/services/transactionService.ts | 188 ++++ src/types/index.ts | 81 ++ 26 files changed, 5622 insertions(+), 17 deletions(-) create mode 100644 FIREBASE_SETUP.md create mode 100644 README.md create mode 100644 TESTING.md create mode 100644 firestore.rules create mode 100644 src/components/Button.tsx create mode 100644 src/components/InputText.tsx create mode 100644 src/components/SubscriptionCard.tsx create mode 100644 src/components/TransactionCard.tsx create mode 100644 src/config/firebase.ts create mode 100644 src/hooks/useAuth.ts create mode 100644 src/navigation/AppNavigator.tsx create mode 100644 src/screens/AnalysisScreen.tsx create mode 100644 src/screens/DashboardScreen.tsx create mode 100644 src/screens/LoginScreen.tsx create mode 100644 src/screens/SignupScreen.tsx create mode 100644 src/screens/SubscriptionScreen.tsx create mode 100644 src/screens/TransactionScreen.tsx create mode 100644 src/services/authService.ts create mode 100644 src/services/categoryService.ts create mode 100644 src/services/subscriptionService.ts create mode 100644 src/services/transactionService.ts create mode 100644 src/types/index.ts diff --git a/App.tsx b/App.tsx index 0329d0c..b15d967 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,20 @@ +import React from 'react'; import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { StyleSheet } from 'react-native'; +import { AppNavigator } from './src/navigation/AppNavigator'; export default function App() { return ( - - Open up App.tsx to start working on your app! + + - + ); } const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, + flex: 1 + } }); diff --git a/FIREBASE_SETUP.md b/FIREBASE_SETUP.md new file mode 100644 index 0000000..4e100f3 --- /dev/null +++ b/FIREBASE_SETUP.md @@ -0,0 +1,158 @@ +# đŸ”„ Guide de configuration Firebase + +Ce guide vous aidera Ă  configurer Firebase pour WalletTracker. + +## Étape 1 : CrĂ©er un projet Firebase + +1. Allez sur [Firebase Console](https://console.firebase.google.com/) +2. Cliquez sur "Ajouter un projet" +3. Entrez le nom du projet : **WalletTracker** +4. DĂ©sactivez Google Analytics (optionnel) +5. Cliquez sur "CrĂ©er le projet" + +## Étape 2 : Ajouter une application Web + +1. Dans la console Firebase, cliquez sur l'icĂŽne **Web** () +2. Enregistrez l'application avec le nom : **WalletTracker** +3. Cochez "Configurer Firebase Hosting" (optionnel) +4. Cliquez sur "Enregistrer l'application" +5. **Copiez les identifiants de configuration** qui s'affichent + +## Étape 3 : Configurer l'application + +1. Ouvrez le fichier `src/config/firebase.ts` +2. Remplacez les valeurs par dĂ©faut par vos identifiants Firebase : + +```typescript +const firebaseConfig = { + apiKey: "AIzaSy...", // Votre clĂ© API + authDomain: "wallettracker-xxx.firebaseapp.com", + projectId: "wallettracker-xxx", + storageBucket: "wallettracker-xxx.appspot.com", + messagingSenderId: "123456789", + appId: "1:123456789:web:abc123" +}; +``` + +## Étape 4 : Activer Authentication + +1. Dans la console Firebase, allez dans **Authentication** +2. Cliquez sur "Commencer" +3. Dans l'onglet **Sign-in method**, activez : + - **Email/Password** : Activez cette mĂ©thode + - Cliquez sur "Enregistrer" + +## Étape 5 : CrĂ©er la base de donnĂ©es Firestore + +1. Dans la console Firebase, allez dans **Firestore Database** +2. Cliquez sur "CrĂ©er une base de donnĂ©es" +3. SĂ©lectionnez **Mode production** +4. Choisissez un emplacement (par exemple : `europe-west1` pour l'Europe) +5. Cliquez sur "Activer" + +## Étape 6 : Configurer les rĂšgles Firestore + +1. Dans Firestore Database, allez dans l'onglet **RĂšgles** +2. Copiez le contenu du fichier `firestore.rules` de ce projet +3. Collez-le dans l'Ă©diteur de rĂšgles Firebase +4. Cliquez sur "Publier" + +Les rĂšgles configurĂ©es permettent : +- ✅ Chaque utilisateur peut lire/Ă©crire uniquement ses propres donnĂ©es +- ✅ Protection contre les accĂšs non autorisĂ©s +- ✅ Validation des champs requis lors de la crĂ©ation + +## Étape 7 : Activer Storage (optionnel) + +Pour stocker les photos de tickets : + +1. Dans la console Firebase, allez dans **Storage** +2. Cliquez sur "Commencer" +3. SĂ©lectionnez **Mode production** +4. Cliquez sur "Suivant" puis "TerminĂ©" +5. Dans l'onglet **RĂšgles**, utilisez ces rĂšgles : + +``` +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /users/{userId}/{allPaths=**} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + } +} +``` + +## Étape 8 : CrĂ©er les index Firestore (si nĂ©cessaire) + +Si vous rencontrez des erreurs de requĂȘte, Firebase vous fournira un lien pour crĂ©er automatiquement les index nĂ©cessaires. + +Les index recommandĂ©s : + +### Collection `transactions` +- Champs : `userId` (Ascending), `date` (Descending) +- Champs : `userId` (Ascending), `type` (Ascending), `date` (Descending) +- Champs : `userId` (Ascending), `category` (Ascending), `date` (Descending) + +### Collection `subscriptions` +- Champs : `userId` (Ascending), `nextPaymentDate` (Ascending) + +## Étape 9 : Tester la configuration + +1. Lancez l'application : `npm start` +2. CrĂ©ez un compte de test +3. VĂ©rifiez dans la console Firebase : + - **Authentication** : Votre utilisateur doit apparaĂźtre + - **Firestore** : Les collections doivent se crĂ©er automatiquement + +## 🔐 SĂ©curitĂ© + +### Bonnes pratiques + +1. **Ne jamais commiter vos identifiants Firebase** dans Git +2. Utilisez des variables d'environnement pour la production +3. Activez l'authentification Ă  deux facteurs sur votre compte Firebase +4. Surveillez l'utilisation dans la console Firebase +5. Configurez des alertes de budget + +### Limites du plan gratuit (Spark) + +- **Firestore** : 1 Go de stockage, 50k lectures/jour, 20k Ă©critures/jour +- **Authentication** : 10k vĂ©rifications/mois +- **Storage** : 5 Go de stockage, 1 Go de tĂ©lĂ©chargement/jour + +Pour une utilisation en production avec plusieurs utilisateurs, envisagez le plan **Blaze** (paiement Ă  l'usage). + +## 🆘 DĂ©pannage + +### Erreur : "Firebase: Error (auth/invalid-api-key)" +- VĂ©rifiez que vous avez bien copiĂ© l'`apiKey` correctement +- Assurez-vous qu'il n'y a pas d'espaces avant/aprĂšs + +### Erreur : "Missing or insufficient permissions" +- VĂ©rifiez que les rĂšgles Firestore sont bien publiĂ©es +- Assurez-vous que l'utilisateur est bien authentifiĂ© + +### Les catĂ©gories ne s'affichent pas +- VĂ©rifiez que l'utilisateur est connectĂ© +- Les catĂ©gories par dĂ©faut se crĂ©ent automatiquement au premier ajout de transaction + +## 📚 Ressources + +- [Documentation Firebase](https://firebase.google.com/docs) +- [Firestore Security Rules](https://firebase.google.com/docs/firestore/security/get-started) +- [Firebase Authentication](https://firebase.google.com/docs/auth) +- [React Native Firebase](https://rnfirebase.io/) + +## ✅ Checklist de configuration + +- [ ] Projet Firebase créé +- [ ] Application Web ajoutĂ©e +- [ ] Identifiants copiĂ©s dans `firebase.ts` +- [ ] Authentication activĂ©e (Email/Password) +- [ ] Firestore Database créée +- [ ] RĂšgles Firestore configurĂ©es +- [ ] Storage activĂ© (optionnel) +- [ ] Application testĂ©e avec succĂšs + +Une fois toutes ces Ă©tapes complĂ©tĂ©es, votre application WalletTracker est prĂȘte Ă  ĂȘtre utilisĂ©e ! 🎉 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7ace0f --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# 💰 WalletTracker + +Application mobile de gestion de budget dĂ©veloppĂ©e avec React Native et Firebase. + +## đŸ“± FonctionnalitĂ©s + +- **Authentification** : Inscription et connexion sĂ©curisĂ©es avec Firebase Auth +- **Gestion des transactions** : Ajout et suivi des dĂ©penses et revenus par catĂ©gorie +- **Abonnements rĂ©currents** : Gestion des abonnements avec rappels automatiques +- **Tableau de bord** : Vue d'ensemble mensuelle du budget avec statistiques +- **Analyses visuelles** : Graphiques et statistiques dĂ©taillĂ©es par catĂ©gorie +- **Synchronisation temps rĂ©el** : Partage des donnĂ©es entre plusieurs utilisateurs +- **Multi-plateforme** : Fonctionne sur iOS et Android + +## đŸ› ïž Stack Technique + +- **Frontend** : React Native avec Expo +- **Langage** : TypeScript +- **Backend** : Firebase (Authentication, Firestore, Storage) +- **Navigation** : React Navigation (Stack & Bottom Tabs) +- **Stockage local** : AsyncStorage +- **Graphiques** : react-native-chart-kit +- **Gestion d'Ă©tat** : React Hooks + +## 📩 Installation + +### PrĂ©requis + +- Node.js (v16 ou supĂ©rieur) +- npm ou yarn +- Expo CLI +- Compte Firebase + +### Étapes d'installation + +1. **Cloner le projet** + ```bash + git clone + cd WalletTracker + ``` + +2. **Installer les dĂ©pendances** + ```bash + npm install + ``` + +3. **Configurer Firebase** + + a. CrĂ©ez un projet sur [Firebase Console](https://console.firebase.google.com/) + + b. Activez les services suivants : + - Authentication (Email/Password) + - Firestore Database + - Storage + + c. Copiez vos identifiants Firebase dans `src/config/firebase.ts` : + ```typescript + const firebaseConfig = { + apiKey: "VOTRE_API_KEY", + authDomain: "VOTRE_AUTH_DOMAIN", + projectId: "VOTRE_PROJECT_ID", + storageBucket: "VOTRE_STORAGE_BUCKET", + messagingSenderId: "VOTRE_MESSAGING_SENDER_ID", + appId: "VOTRE_APP_ID" + }; + ``` + +4. **Configurer les rĂšgles Firestore** + + Copiez les rĂšgles du fichier `firestore.rules` dans la console Firebase + +5. **Lancer l'application** + ```bash + npm start + ``` + + Puis scannez le QR code avec l'application Expo Go sur votre tĂ©lĂ©phone + +## đŸ“± Utilisation + +### DĂ©marrage rapide + +1. **CrĂ©er un compte** : Utilisez l'Ă©cran d'inscription avec votre email et mot de passe +2. **Ajouter une transaction** : Cliquez sur le bouton "+" pour ajouter une dĂ©pense ou un revenu +3. **GĂ©rer les abonnements** : AccĂ©dez Ă  l'onglet "Abonnements" pour suivre vos dĂ©penses rĂ©currentes +4. **Consulter les analyses** : Visualisez vos dĂ©penses par catĂ©gorie dans l'onglet "Analyses" + +### CatĂ©gories par dĂ©faut + +**DĂ©penses** : +- Courses 🛒 +- Logement 🏠 +- Transport 🚗 +- Loisirs 🎼 +- Restaurant đŸœïž +- SantĂ© 💊 +- VĂȘtements 👕 +- Éducation 📚 +- Abonnements đŸ“± +- Autre 📩 + +**Revenus** : +- Salaire 💰 +- Freelance đŸ’Œ +- Investissement 📈 +- Cadeau 🎁 +- Autre đŸ’” + +## đŸ—‚ïž Structure du projet + +``` +WalletTracker/ +├── src/ +│ ├── components/ # Composants rĂ©utilisables +│ │ ├── Button.tsx +│ │ ├── InputText.tsx +│ │ ├── TransactionCard.tsx +│ │ └── SubscriptionCard.tsx +│ ├── config/ # Configuration Firebase +│ │ └── firebase.ts +│ ├── hooks/ # Hooks personnalisĂ©s +│ │ └── useAuth.ts +│ ├── navigation/ # Configuration de la navigation +│ │ └── AppNavigator.tsx +│ ├── screens/ # Écrans de l'application +│ │ ├── LoginScreen.tsx +│ │ ├── SignupScreen.tsx +│ │ ├── DashboardScreen.tsx +│ │ ├── TransactionScreen.tsx +│ │ ├── SubscriptionScreen.tsx +│ │ └── AnalysisScreen.tsx +│ ├── services/ # Services Firebase +│ │ ├── authService.ts +│ │ ├── transactionService.ts +│ │ ├── subscriptionService.ts +│ │ └── categoryService.ts +│ ├── types/ # DĂ©finitions TypeScript +│ │ └── index.ts +│ └── utils/ # Utilitaires +├── App.tsx # Point d'entrĂ©e +├── package.json +└── README.md +``` + +## 🔐 SĂ©curitĂ© + +- Les mots de passe sont gĂ©rĂ©s par Firebase Authentication +- Les rĂšgles Firestore protĂšgent les donnĂ©es de chaque utilisateur +- Les donnĂ©es sont synchronisĂ©es uniquement pour les utilisateurs autorisĂ©s +- Stockage sĂ©curisĂ© des tokens avec AsyncStorage + +## 🚀 DĂ©ploiement + +### Build pour production + +**iOS** : +```bash +eas build --platform ios +``` + +**Android** : +```bash +eas build --platform android +``` + +## 📝 FonctionnalitĂ©s futures + +- [ ] Notifications push pour les rappels d'abonnements +- [ ] Partage multi-utilisateurs avancĂ© +- [ ] Export des donnĂ©es en CSV/PDF +- [ ] Objectifs budgĂ©taires mensuels +- [ ] Mode sombre +- [ ] Support multilingue +- [ ] Reconnaissance de tickets avec OCR +- [ ] Widgets pour l'Ă©cran d'accueil + +## đŸ€ Contribution + +Les contributions sont les bienvenues ! N'hĂ©sitez pas Ă  ouvrir une issue ou une pull request. + +## 📄 Licence + +Ce projet est sous licence MIT. + +## đŸ‘šâ€đŸ’» Auteur + +DĂ©veloppĂ© avec ❀ pour une meilleure gestion de budget. + +## 📞 Support + +Pour toute question ou problĂšme, veuillez ouvrir une issue sur GitHub. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..d8edf78 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,243 @@ +# đŸ§Ș Guide de test - WalletTracker + +Ce guide vous aidera Ă  tester toutes les fonctionnalitĂ©s de l'application. + +## PrĂ©requis + +- Firebase configurĂ© (voir `FIREBASE_SETUP.md`) +- Application lancĂ©e avec `npm start` +- TĂ©lĂ©phone avec Expo Go ou Ă©mulateur + +## 📋 Checklist de tests + +### 1. Authentification + +#### Test d'inscription +- [ ] Ouvrir l'application +- [ ] Cliquer sur "CrĂ©er un compte" +- [ ] Tester les validations : + - [ ] Champ nom vide → Erreur affichĂ©e + - [ ] Email invalide → Erreur affichĂ©e + - [ ] Mot de passe < 6 caractĂšres → Erreur affichĂ©e + - [ ] Mots de passe diffĂ©rents → Erreur affichĂ©e +- [ ] Remplir tous les champs correctement +- [ ] CrĂ©er le compte +- [ ] VĂ©rifier la redirection vers le Dashboard + +#### Test de connexion +- [ ] Se dĂ©connecter +- [ ] Cliquer sur "J'ai dĂ©jĂ  un compte" +- [ ] Tester les validations : + - [ ] Email vide → Erreur affichĂ©e + - [ ] Mot de passe vide → Erreur affichĂ©e + - [ ] Mauvais identifiants → Message d'erreur +- [ ] Se connecter avec les bons identifiants +- [ ] VĂ©rifier la redirection vers le Dashboard + +#### Test de persistance +- [ ] Se connecter +- [ ] Fermer complĂštement l'application +- [ ] Rouvrir l'application +- [ ] VĂ©rifier que l'utilisateur est toujours connectĂ© + +### 2. Dashboard + +#### Affichage initial +- [ ] VĂ©rifier l'affichage du message de bienvenue +- [ ] VĂ©rifier l'affichage du mois actuel +- [ ] VĂ©rifier que le solde est Ă  0€ +- [ ] VĂ©rifier que les revenus sont Ă  0€ +- [ ] VĂ©rifier que les dĂ©penses sont Ă  0€ +- [ ] VĂ©rifier l'affichage de l'Ă©tat vide + +#### AprĂšs ajout de transactions +- [ ] Ajouter quelques transactions +- [ ] Revenir au Dashboard +- [ ] VĂ©rifier que les statistiques sont mises Ă  jour +- [ ] VĂ©rifier que les 5 derniĂšres transactions s'affichent +- [ ] VĂ©rifier le calcul du solde (revenus - dĂ©penses) + +#### Pull to refresh +- [ ] Tirer vers le bas pour rafraĂźchir +- [ ] VĂ©rifier que les donnĂ©es se rechargent + +### 3. Transactions + +#### Ajout d'une dĂ©pense +- [ ] Aller dans l'onglet "Transactions" +- [ ] Cliquer sur "+ Ajouter" +- [ ] SĂ©lectionner "DĂ©pense" +- [ ] Tester les validations : + - [ ] Montant vide → Erreur + - [ ] Montant = 0 → Erreur + - [ ] CatĂ©gorie non sĂ©lectionnĂ©e → Erreur +- [ ] Entrer un montant valide (ex: 50.00) +- [ ] SĂ©lectionner une catĂ©gorie (ex: Courses) +- [ ] Ajouter une note (optionnel) +- [ ] Cliquer sur "Ajouter la transaction" +- [ ] VĂ©rifier que la transaction apparaĂźt dans la liste +- [ ] VĂ©rifier l'affichage en rouge avec le signe "-" + +#### Ajout d'un revenu +- [ ] Cliquer sur "+ Ajouter" +- [ ] SĂ©lectionner "Revenu" +- [ ] Entrer un montant (ex: 2000.00) +- [ ] SĂ©lectionner une catĂ©gorie (ex: Salaire) +- [ ] Ajouter une note +- [ ] Cliquer sur "Ajouter la transaction" +- [ ] VĂ©rifier que la transaction apparaĂźt dans la liste +- [ ] VĂ©rifier l'affichage en vert avec le signe "+" + +#### Liste des transactions +- [ ] VĂ©rifier que les transactions sont triĂ©es par date (plus rĂ©centes en premier) +- [ ] VĂ©rifier l'affichage des icĂŽnes de catĂ©gorie +- [ ] VĂ©rifier l'affichage des couleurs par catĂ©gorie +- [ ] VĂ©rifier l'affichage de la date formatĂ©e +- [ ] VĂ©rifier l'affichage de la note si prĂ©sente + +### 4. Abonnements + +#### Ajout d'un abonnement +- [ ] Aller dans l'onglet "Abonnements" +- [ ] Cliquer sur "+ Ajouter" +- [ ] Tester les validations : + - [ ] Nom vide → Erreur + - [ ] Montant vide → Erreur + - [ ] Jour invalide → Erreur +- [ ] Entrer un nom (ex: Netflix) +- [ ] Entrer un montant (ex: 15.99) +- [ ] SĂ©lectionner "Mensuel" +- [ ] Entrer un jour du mois (ex: 15) +- [ ] SĂ©lectionner une catĂ©gorie (ex: Abonnements) +- [ ] Cliquer sur "Ajouter l'abonnement" +- [ ] VĂ©rifier que l'abonnement apparaĂźt dans la liste + +#### Affichage des abonnements +- [ ] VĂ©rifier l'affichage du nom +- [ ] VĂ©rifier l'affichage du montant +- [ ] VĂ©rifier l'affichage de la frĂ©quence +- [ ] VĂ©rifier l'affichage de la prochaine date de paiement +- [ ] VĂ©rifier l'affichage du nombre de jours restants + +#### Abonnement proche +- [ ] CrĂ©er un abonnement avec une date dans 2 jours +- [ ] VĂ©rifier que la carte a une bordure orange +- [ ] VĂ©rifier que le texte est en rouge + +#### Total mensuel +- [ ] Ajouter plusieurs abonnements +- [ ] VĂ©rifier que le total mensuel est correct +- [ ] Tester avec diffĂ©rentes frĂ©quences (hebdo, mensuel, annuel) + +### 5. Analyses + +#### SĂ©lection du mois +- [ ] Aller dans l'onglet "Analyses" +- [ ] Cliquer sur la flĂšche gauche +- [ ] VĂ©rifier le changement de mois +- [ ] Cliquer sur la flĂšche droite +- [ ] VĂ©rifier le changement de mois + +#### Basculer entre dĂ©penses et revenus +- [ ] Cliquer sur "DĂ©penses" +- [ ] VĂ©rifier l'affichage du graphique des dĂ©penses +- [ ] Cliquer sur "Revenus" +- [ ] VĂ©rifier l'affichage du graphique des revenus + +#### Graphique en camembert +- [ ] Ajouter plusieurs transactions dans diffĂ©rentes catĂ©gories +- [ ] VĂ©rifier l'affichage du graphique +- [ ] VĂ©rifier les couleurs par catĂ©gorie +- [ ] VĂ©rifier les montants affichĂ©s + +#### Statistiques par catĂ©gorie +- [ ] VĂ©rifier l'affichage de chaque catĂ©gorie +- [ ] VĂ©rifier le montant total par catĂ©gorie +- [ ] VĂ©rifier le nombre de transactions +- [ ] VĂ©rifier le pourcentage +- [ ] VĂ©rifier le tri par montant dĂ©croissant + +#### État vide +- [ ] SĂ©lectionner un mois sans transactions +- [ ] VĂ©rifier l'affichage de l'Ă©tat vide + +### 6. Navigation + +#### Onglets +- [ ] Tester la navigation entre tous les onglets +- [ ] VĂ©rifier que l'onglet actif est bien mis en Ă©vidence +- [ ] VĂ©rifier que les icĂŽnes changent de couleur + +#### Boutons d'action rapide (Dashboard) +- [ ] Cliquer sur "DĂ©pense" +- [ ] VĂ©rifier la navigation vers Transactions avec le type prĂ©-sĂ©lectionnĂ© +- [ ] Cliquer sur "Revenu" +- [ ] VĂ©rifier la navigation vers Transactions avec le type prĂ©-sĂ©lectionnĂ© + +### 7. Synchronisation temps rĂ©el + +#### Test avec deux appareils (si possible) +- [ ] Se connecter avec le mĂȘme compte sur deux appareils +- [ ] Ajouter une transaction sur l'appareil 1 +- [ ] VĂ©rifier que la transaction apparaĂźt sur l'appareil 2 +- [ ] Ajouter un abonnement sur l'appareil 2 +- [ ] VĂ©rifier que l'abonnement apparaĂźt sur l'appareil 1 + +#### Test de mise Ă  jour en temps rĂ©el +- [ ] Ouvrir le Dashboard +- [ ] Dans un autre onglet, ajouter une transaction +- [ ] Revenir au Dashboard +- [ ] VĂ©rifier que les statistiques sont mises Ă  jour + +### 8. DĂ©connexion + +- [ ] Cliquer sur "DĂ©connexion" dans le Dashboard +- [ ] VĂ©rifier la redirection vers l'Ă©cran de connexion +- [ ] VĂ©rifier que les donnĂ©es ne sont plus accessibles + +### 9. Tests de performance + +#### Chargement initial +- [ ] Mesurer le temps de chargement de l'application +- [ ] VĂ©rifier qu'il n'y a pas de lag + +#### Avec beaucoup de donnĂ©es +- [ ] Ajouter 50+ transactions +- [ ] VĂ©rifier que la liste dĂ©file correctement +- [ ] VĂ©rifier que les graphiques se chargent rapidement + +### 10. Tests d'erreur + +#### Pas de connexion Internet +- [ ] DĂ©sactiver le Wi-Fi et les donnĂ©es mobiles +- [ ] Essayer de se connecter +- [ ] VĂ©rifier le message d'erreur +- [ ] Essayer d'ajouter une transaction +- [ ] VĂ©rifier le message d'erreur + +#### Firebase non configurĂ© +- [ ] VĂ©rifier le message d'erreur si Firebase n'est pas configurĂ© + +## 🐛 Bugs connus + +Notez ici les bugs dĂ©couverts pendant les tests : + +1. +2. +3. + +## ✅ RĂ©sultat des tests + +- Date du test : ___________ +- Version testĂ©e : 1.0.0 +- Testeur : ___________ +- Appareil : ___________ +- RĂ©sultat global : ⬜ RĂ©ussi / ⬜ Échec + +## 📝 Notes + +Ajoutez vos observations ici : + +--- + +**Bon test ! 🚀** diff --git a/app.json b/app.json index 7a6b526..490e9d1 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "expo": { "name": "WalletTracker", - "slug": "WalletTracker", + "slug": "wallettracker", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -10,21 +10,36 @@ "splash": { "image": "./assets/splash-icon.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#4A90E2" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.wallettracker.app" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" + "backgroundColor": "#4A90E2" }, + "package": "com.wallettracker.app", "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false }, "web": { "favicon": "./assets/favicon.png" + }, + "plugins": [ + [ + "expo-image-picker", + { + "photosPermission": "L'application a besoin d'accĂ©der Ă  vos photos pour ajouter des tickets." + } + ] + ], + "extra": { + "eas": { + "projectId": "your-project-id" + } } } } diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..7e484ed --- /dev/null +++ b/firestore.rules @@ -0,0 +1,92 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + + // Fonction helper pour vĂ©rifier si l'utilisateur est authentifiĂ© + function isAuthenticated() { + return request.auth != null; + } + + // Fonction helper pour vĂ©rifier si l'utilisateur est propriĂ©taire + function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; + } + + // RĂšgles pour la collection users + match /users/{userId} { + // Lecture : l'utilisateur peut lire ses propres donnĂ©es ou celles partagĂ©es avec lui + allow read: if isOwner(userId) || + (isAuthenticated() && + resource.data.sharedWith != null && + request.auth.uid in resource.data.sharedWith); + + // Écriture : uniquement le propriĂ©taire + allow create: if isOwner(userId); + allow update: if isOwner(userId); + allow delete: if isOwner(userId); + } + + // RĂšgles pour la collection transactions + match /transactions/{transactionId} { + // Lecture : l'utilisateur peut lire ses propres transactions + allow read: if isAuthenticated() && + resource.data.userId == request.auth.uid; + + // CrĂ©ation : l'utilisateur peut crĂ©er ses propres transactions + allow create: if isAuthenticated() && + request.resource.data.userId == request.auth.uid && + request.resource.data.keys().hasAll(['userId', 'type', 'amount', 'category', 'date', 'createdAt', 'updatedAt']); + + // Mise Ă  jour : uniquement le propriĂ©taire + allow update: if isAuthenticated() && + resource.data.userId == request.auth.uid && + request.resource.data.userId == resource.data.userId; + + // Suppression : uniquement le propriĂ©taire + allow delete: if isAuthenticated() && + resource.data.userId == request.auth.uid; + } + + // RĂšgles pour la collection categories + match /categories/{categoryId} { + // Lecture : l'utilisateur peut lire ses propres catĂ©gories + allow read: if isAuthenticated() && + resource.data.userId == request.auth.uid; + + // CrĂ©ation : l'utilisateur peut crĂ©er ses propres catĂ©gories + allow create: if isAuthenticated() && + request.resource.data.userId == request.auth.uid && + request.resource.data.keys().hasAll(['userId', 'name', 'icon', 'color', 'type']); + + // Mise Ă  jour : uniquement le propriĂ©taire + allow update: if isAuthenticated() && + resource.data.userId == request.auth.uid && + request.resource.data.userId == resource.data.userId; + + // Suppression : uniquement le propriĂ©taire + allow delete: if isAuthenticated() && + resource.data.userId == request.auth.uid; + } + + // RĂšgles pour la collection subscriptions + match /subscriptions/{subscriptionId} { + // Lecture : l'utilisateur peut lire ses propres abonnements + allow read: if isAuthenticated() && + resource.data.userId == request.auth.uid; + + // CrĂ©ation : l'utilisateur peut crĂ©er ses propres abonnements + allow create: if isAuthenticated() && + request.resource.data.userId == request.auth.uid && + request.resource.data.keys().hasAll(['userId', 'name', 'amount', 'category', 'frequency', 'nextPaymentDate', 'reminderDaysBefore', 'isActive', 'createdAt', 'updatedAt']); + + // Mise Ă  jour : uniquement le propriĂ©taire + allow update: if isAuthenticated() && + resource.data.userId == request.auth.uid && + request.resource.data.userId == resource.data.userId; + + // Suppression : uniquement le propriĂ©taire + allow delete: if isAuthenticated() && + resource.data.userId == request.auth.uid; + } + } +} diff --git a/package-lock.json b/package-lock.json index 66c6afa..bb5722d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,23 @@ "": { "name": "wallettracker", "version": "1.0.0", - "license": "0BSD", "dependencies": { + "@react-native-async-storage/async-storage": "^1.24.0", + "@react-navigation/bottom-tabs": "^7.5.0", + "@react-navigation/native": "^7.1.18", + "@react-navigation/stack": "^7.5.0", "expo": "~54.0.18", + "expo-image-picker": "^17.0.8", "expo-status-bar": "~3.0.8", + "firebase": "^12.4.0", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-chart-kit": "^6.12.0", + "react-native-gesture-handler": "^2.29.0", + "react-native-reanimated": "^4.1.3", + "react-native-safe-area-context": "^5.6.1", + "react-native-screens": "^4.18.0", + "react-native-svg": "^15.14.0" }, "devDependencies": { "@types/react": "~19.1.0", @@ -1331,6 +1342,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", @@ -1520,6 +1547,18 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", @@ -2386,6 +2425,645 @@ "node": ">=8" } }, + "node_modules/@firebase/ai": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.4.0.tgz", + "integrity": "sha512-YilG6AJ/nYpCKtxZyvEzBRAQv5bU+2tBOKX4Ps0rNNSdxN39aT37kGhjATbk1kq1z5Lq7mkWglw/ajAF3lOWUg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.19", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", + "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", + "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.4.tgz", + "integrity": "sha512-pUxEGmR+uu21OG/icAovjlu1fcYJzyVhhT0rsCrn+zi+nHtrS43Bp9KPn9KGa4NMspCUE++nkyiqziuIvJdwzw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.4.tgz", + "integrity": "sha512-T7ifGmb+awJEcp542Ek4HtNfBxcBrnuk1ggUdqyFEdsXHdq7+wVlhvE6YukTL7NS8hIkEfL7TMAPx/uCNqt30g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2762,6 +3440,82 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -3016,6 +3770,138 @@ "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.5.0.tgz", + "integrity": "sha512-JY9yQDQTv7avXqXdrToyn6ogcBqY2gTXg7C1J6OWZGz7QhlnPZQm375T4nYBWqVWsODVNeNagkCPptZGOxI1rg==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.7.0", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.18", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.12.4", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.12.4.tgz", + "integrity": "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.1", + "escape-string-regexp": "^4.0.0", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.7.0.tgz", + "integrity": "sha512-lqlUUTqzKJrm3WYmiy901DSpa5wW8DWSmqNqWlRFWDVjx6SSjOUThQpdMnVXhydPtrTo74yVUPB27oe/jrvo4Q==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.18", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.18", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.18.tgz", + "integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.12.4", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz", + "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@react-navigation/stack": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.5.0.tgz", + "integrity": "sha512-1aGLudURsaOyyyktmaOxGbb9NGQfCtQ2Z4xt2mjMApMTQsP4d2os9D+w9qPPYUh6rwGMzoHb9A7lJ6NIk++6WA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.7.0", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.18", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-gesture-handler": ">= 2.0.0", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3090,6 +3976,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3671,6 +4563,12 @@ "node": ">=0.6" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -4040,6 +4938,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4055,6 +4966,34 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -4196,6 +5135,56 @@ "node": ">=8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -4220,6 +5209,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4287,6 +5285,61 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -4347,6 +5400,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -4478,6 +5543,27 @@ } } }, + "node_modules/expo-image-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz", + "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.8.tgz", + "integrity": "sha512-489ByhVs2XPoAu9zodivAKLv7hG4S/FOe8hO/C2U6jVxmRjpAKakKNjMml0IwWjf1+c/RYBqm1XxKaZ+vq/fDQ==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~6.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.18", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.18.tgz", @@ -5065,12 +6151,30 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -5092,6 +6196,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -5138,6 +6251,42 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.4.0.tgz", + "integrity": "sha512-/chNgDQ6ppPPGOQO4jctxOa/5JeQxuhaxA7Y90K0I+n/wPfoO8mRveedhVUdo7ExLcWUivnnow/ouSLYSI5Icw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.4.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.4", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.4", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/flow-enums-runtime": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", @@ -5323,6 +6472,21 @@ "hermes-estree": "0.32.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -5366,6 +6530,12 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -5379,6 +6549,12 @@ "node": ">= 14" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5464,6 +6640,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5512,6 +6694,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -6359,6 +7550,18 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6383,6 +7586,12 @@ "node": ">=4" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6419,12 +7628,30 @@ "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "license": "Apache-2.0" }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6991,6 +8218,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -7247,6 +8486,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/paths-js": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/paths-js/-/paths-js-0.4.11.tgz", + "integrity": "sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.11.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7297,6 +8545,12 @@ "node": ">=4.0.0" } }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -7403,6 +8657,30 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7420,6 +8698,24 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -7472,6 +8768,18 @@ "ws": "^7" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7535,6 +8843,37 @@ } } }, + "node_modules/react-native-chart-kit": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz", + "integrity": "sha512-nZLGyCFzZ7zmX0KjYeeSV1HKuPhl1wOMlTAqa0JhlyW62qV/1ZPXHgT8o9s8mkFaGxdqbspOeuaa6I9jUQDgnA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.13", + "paths-js": "^0.4.10", + "point-in-polygon": "^1.0.1" + }, + "peerDependencies": { + "react": "> 16.7.0", + "react-native": ">= 0.50.0", + "react-native-svg": "> 6.4.1" + } + }, + "node_modules/react-native-gesture-handler": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.29.0.tgz", + "integrity": "sha512-nxikN5b2ebSTPqqhIlTHQJqIHTu0Y5GAhST3w3/G1pm9BlqHVFcLFPZfIaT4A3TVKjQDcKElij1hhHKpAVUcOQ==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-is-edge-to-edge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", @@ -7545,6 +8884,111 @@ "react-native": "*" } }, + "node_modules/react-native-reanimated": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", + "integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.2.1", + "semver": "7.7.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*", + "react-native-worklets": ">=0.5.0" + } + }, + "node_modules/react-native-reanimated/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", + "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.18.0.tgz", + "integrity": "sha512-mRTLWL7Uc1p/RFNveEIIrhP22oxHduC2ZnLr/2iHwBeYpGXR0rJZ7Bgc0ktxQSHRjWTPT70qc/7yd4r9960PBQ==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.14.0.tgz", + "integrity": "sha512-B3gYc7WztcOT4N54AtUutbe0Nuqqh/nkresY0fAXzUHYLsWuIu/yGiCCD3DKfAs6GLv5LFtWTu7N333Q+e3bkg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-worklets": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.6.1.tgz", + "integrity": "sha512-URca8l7c7Uog7gv4mcg9KILdJlnbvwdS5yfXQYf5TDkD2W1VY1sduEKrD+sA3lUPXH/TG1vmXAvNxCNwPMYgGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0", + "semver": "7.7.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-worklets/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", @@ -8070,6 +9514,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -8131,6 +9584,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8194,6 +9656,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -8579,6 +10050,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8717,6 +10194,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8768,6 +10263,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -8777,6 +10278,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", @@ -8786,6 +10293,29 @@ "node": ">=8" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", diff --git a/package.json b/package.json index 40cdef3..df814bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "wallettracker", - "license": "0BSD", "version": "1.0.0", "main": "index.ts", "scripts": { @@ -10,10 +9,22 @@ "web": "expo start --web" }, "dependencies": { + "@react-native-async-storage/async-storage": "^1.24.0", + "@react-navigation/bottom-tabs": "^7.5.0", + "@react-navigation/native": "^7.1.18", + "@react-navigation/stack": "^7.5.0", "expo": "~54.0.18", + "expo-image-picker": "^17.0.8", "expo-status-bar": "~3.0.8", + "firebase": "^12.4.0", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-chart-kit": "^6.12.0", + "react-native-gesture-handler": "^2.29.0", + "react-native-reanimated": "^4.1.3", + "react-native-safe-area-context": "^5.6.1", + "react-native-screens": "^4.18.0", + "react-native-svg": "^15.14.0" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..31f1b53 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { + TouchableOpacity, + Text, + StyleSheet, + ActivityIndicator, + ViewStyle, + TextStyle +} from 'react-native'; + +interface ButtonProps { + title: string; + onPress: () => void; + variant?: 'primary' | 'secondary' | 'outline'; + loading?: boolean; + disabled?: boolean; + style?: ViewStyle; + textStyle?: TextStyle; +} + +export const Button: React.FC = ({ + title, + onPress, + variant = 'primary', + loading = false, + disabled = false, + style, + textStyle +}) => { + const getButtonStyle = () => { + switch (variant) { + case 'secondary': + return styles.secondaryButton; + case 'outline': + return styles.outlineButton; + default: + return styles.primaryButton; + } + }; + + const getTextStyle = () => { + switch (variant) { + case 'outline': + return styles.outlineText; + default: + return styles.buttonText; + } + }; + + return ( + + {loading ? ( + + ) : ( + {title} + )} + + ); +}; + +const styles = StyleSheet.create({ + button: { + borderRadius: 12, + padding: 16, + alignItems: 'center', + justifyContent: 'center', + minHeight: 52 + }, + primaryButton: { + backgroundColor: '#4A90E2' + }, + secondaryButton: { + backgroundColor: '#6C757D' + }, + outlineButton: { + backgroundColor: 'transparent', + borderWidth: 2, + borderColor: '#4A90E2' + }, + disabledButton: { + opacity: 0.5 + }, + buttonText: { + color: '#FFF', + fontSize: 16, + fontWeight: '600' + }, + outlineText: { + color: '#4A90E2', + fontSize: 16, + fontWeight: '600' + } +}); diff --git a/src/components/InputText.tsx b/src/components/InputText.tsx new file mode 100644 index 0000000..0548210 --- /dev/null +++ b/src/components/InputText.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { TextInput, StyleSheet, View, Text, TextInputProps } from 'react-native'; + +interface InputTextProps extends TextInputProps { + label?: string; + error?: string; +} + +export const InputText: React.FC = ({ label, error, style, ...props }) => { + return ( + + {label && {label}} + + {error && {error}} + + ); +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: 16 + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333', + marginBottom: 8 + }, + input: { + backgroundColor: '#F5F5F5', + borderRadius: 12, + padding: 16, + fontSize: 16, + color: '#333', + borderWidth: 1, + borderColor: '#E0E0E0' + }, + inputError: { + borderColor: '#FF6B6B' + }, + errorText: { + color: '#FF6B6B', + fontSize: 12, + marginTop: 4, + marginLeft: 4 + } +}); diff --git a/src/components/SubscriptionCard.tsx b/src/components/SubscriptionCard.tsx new file mode 100644 index 0000000..1fc299d --- /dev/null +++ b/src/components/SubscriptionCard.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { Subscription } from '../types'; + +interface SubscriptionCardProps { + subscription: Subscription; + onPress?: () => void; + categoryIcon?: string; + categoryColor?: string; +} + +export const SubscriptionCard: React.FC = ({ + subscription, + onPress, + categoryIcon = 'đŸ“±', + categoryColor = '#F8B739' +}) => { + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: 'short' + }).format(date); + }; + + const getFrequencyLabel = (frequency: string) => { + switch (frequency) { + case 'daily': + return 'Quotidien'; + case 'weekly': + return 'Hebdomadaire'; + case 'monthly': + return 'Mensuel'; + case 'yearly': + return 'Annuel'; + default: + return frequency; + } + }; + + const getDaysUntilPayment = () => { + const today = new Date(); + const daysUntil = Math.ceil( + (subscription.nextPaymentDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) + ); + + if (daysUntil < 0) return 'En retard'; + if (daysUntil === 0) return 'Aujourd\'hui'; + if (daysUntil === 1) return 'Demain'; + return `Dans ${daysUntil} jours`; + }; + + const isUpcoming = () => { + const today = new Date(); + const daysUntil = Math.ceil( + (subscription.nextPaymentDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24) + ); + return daysUntil <= subscription.reminderDaysBefore && daysUntil >= 0; + }; + + return ( + + + + {categoryIcon} + + + {subscription.name} + {getFrequencyLabel(subscription.frequency)} + + {getDaysUntilPayment()} ‱ {formatDate(subscription.nextPaymentDate)} + + + + + {subscription.amount.toFixed(2)} € + {!subscription.isActive && ( + Inactif + )} + + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: '#FFF', + borderRadius: 12, + padding: 16, + marginBottom: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3 + }, + upcomingCard: { + borderWidth: 2, + borderColor: '#FFA07A' + }, + leftSection: { + flexDirection: 'row', + alignItems: 'center', + flex: 1 + }, + iconContainer: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12 + }, + icon: { + fontSize: 24 + }, + infoContainer: { + flex: 1 + }, + name: { + fontSize: 16, + fontWeight: '600', + color: '#333', + marginBottom: 4 + }, + frequency: { + fontSize: 13, + color: '#999', + marginBottom: 4 + }, + nextPayment: { + fontSize: 12, + color: '#666' + }, + upcomingText: { + color: '#FF6B6B', + fontWeight: '600' + }, + rightSection: { + alignItems: 'flex-end' + }, + amount: { + fontSize: 18, + fontWeight: '700', + color: '#333' + }, + inactiveLabel: { + fontSize: 11, + color: '#999', + marginTop: 4, + fontStyle: 'italic' + } +}); diff --git a/src/components/TransactionCard.tsx b/src/components/TransactionCard.tsx new file mode 100644 index 0000000..b9fb825 --- /dev/null +++ b/src/components/TransactionCard.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { Transaction } from '../types'; + +interface TransactionCardProps { + transaction: Transaction; + onPress?: () => void; + categoryIcon?: string; + categoryColor?: string; +} + +export const TransactionCard: React.FC = ({ + transaction, + onPress, + categoryIcon = '📩', + categoryColor = '#95A5A6' +}) => { + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat('fr-FR', { + day: '2-digit', + month: 'short', + year: 'numeric' + }).format(date); + }; + + const formatAmount = (amount: number, type: 'income' | 'expense') => { + const sign = type === 'income' ? '+' : '-'; + return `${sign}${amount.toFixed(2)} €`; + }; + + return ( + + + + {categoryIcon} + + + {transaction.category} + {formatDate(transaction.date)} + {transaction.note && {transaction.note}} + + + + + {formatAmount(transaction.amount, transaction.type)} + + + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: '#FFF', + borderRadius: 12, + padding: 16, + marginBottom: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3 + }, + leftSection: { + flexDirection: 'row', + alignItems: 'center', + flex: 1 + }, + iconContainer: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12 + }, + icon: { + fontSize: 24 + }, + infoContainer: { + flex: 1 + }, + category: { + fontSize: 16, + fontWeight: '600', + color: '#333', + marginBottom: 4 + }, + date: { + fontSize: 13, + color: '#999' + }, + note: { + fontSize: 12, + color: '#666', + marginTop: 4, + fontStyle: 'italic' + }, + rightSection: { + alignItems: 'flex-end' + }, + amount: { + fontSize: 18, + fontWeight: '700' + }, + incomeAmount: { + color: '#52C41A' + }, + expenseAmount: { + color: '#FF6B6B' + } +}); diff --git a/src/config/firebase.ts b/src/config/firebase.ts new file mode 100644 index 0000000..8df2b39 --- /dev/null +++ b/src/config/firebase.ts @@ -0,0 +1,27 @@ +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; +import { getFirestore } from 'firebase/firestore'; +import { getStorage } from 'firebase/storage'; + +// Configuration Firebase +// IMPORTANT: Remplacez ces valeurs par celles de votre projet Firebase +// Allez sur https://console.firebase.google.com/ > ParamĂštres du projet > Vos applications +const firebaseConfig = { + apiKey: "AIzaSyCwPKnHnU2O_ABm6gi-pnvGB8PQZ3l4y5o", + authDomain: "wallettracket-a4738.firebaseapp.com", + projectId: "wallettracket-a4738", + storageBucket: "wallettracket-a4738.firebasestorage.app", + messagingSenderId: "21315540695", + appId: "1:21315540695:web:e7bffb54e26d3290b1c292", + measurementId: "G-VXMLZBRPEK" +}; + +// Initialisation de Firebase +const app = initializeApp(firebaseConfig); + +// Services Firebase +export const auth = getAuth(app); +export const db = getFirestore(app); +export const storage = getStorage(app); + +export default app; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 0000000..e94252c --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react'; +import { User as FirebaseUser, onAuthStateChanged } from 'firebase/auth'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { auth } from '../config/firebase'; +import { authService } from '../services/authService'; +import { User } from '../types'; + +const USER_STORAGE_KEY = '@wallettracker_user'; + +export const useAuth = () => { + const [user, setUser] = useState(null); + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + const [initializing, setInitializing] = useState(true); + + useEffect(() => { + // Charger l'utilisateur depuis AsyncStorage au dĂ©marrage + loadUserFromStorage(); + + // Écouter les changements d'Ă©tat d'authentification + const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => { + setUser(firebaseUser); + + if (firebaseUser) { + // Sauvegarder l'utilisateur dans AsyncStorage + await AsyncStorage.setItem(USER_STORAGE_KEY, JSON.stringify(firebaseUser.uid)); + + // RĂ©cupĂ©rer les donnĂ©es utilisateur depuis Firestore + const data = await authService.getUserData(firebaseUser.uid); + setUserData(data); + } else { + // Supprimer l'utilisateur d'AsyncStorage + await AsyncStorage.removeItem(USER_STORAGE_KEY); + setUserData(null); + } + + if (initializing) { + setInitializing(false); + } + setLoading(false); + }); + + return unsubscribe; + }, []); + + const loadUserFromStorage = async () => { + try { + const storedUid = await AsyncStorage.getItem(USER_STORAGE_KEY); + if (storedUid) { + // L'utilisateur sera chargĂ© par onAuthStateChanged + setLoading(true); + } else { + setLoading(false); + setInitializing(false); + } + } catch (error) { + console.error('Erreur lors du chargement de l\'utilisateur:', error); + setLoading(false); + setInitializing(false); + } + }; + + const signup = async (email: string, password: string, displayName?: string) => { + setLoading(true); + try { + await authService.signup(email, password, displayName); + } catch (error) { + throw error; + } finally { + setLoading(false); + } + }; + + const login = async (email: string, password: string) => { + setLoading(true); + try { + await authService.login(email, password); + } catch (error) { + throw error; + } finally { + setLoading(false); + } + }; + + const logout = async () => { + setLoading(true); + try { + await authService.logout(); + } catch (error) { + throw error; + } finally { + setLoading(false); + } + }; + + return { + user, + userData, + loading, + initializing, + signup, + login, + logout + }; +}; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..ecbfe29 --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createStackNavigator } from '@react-navigation/stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { ActivityIndicator, View, StyleSheet } from 'react-native'; + +import { useAuth } from '../hooks/useAuth'; +import { LoginScreen } from '../screens/LoginScreen'; +import { SignupScreen } from '../screens/SignupScreen'; +import { DashboardScreen } from '../screens/DashboardScreen'; +import { TransactionScreen } from '../screens/TransactionScreen'; +import { SubscriptionScreen } from '../screens/SubscriptionScreen'; +import { AnalysisScreen } from '../screens/AnalysisScreen'; + +import { RootStackParamList, MainTabParamList } from '../types'; + +const Stack = createStackNavigator(); +const Tab = createBottomTabNavigator(); + +const MainTabs = () => { + return ( + + + }} + /> + + }} + /> + + }} + /> + + }} + /> + + ); +}; + +const TabIcon = ({ icon, color }: { icon: string; color: string }) => ( + + {icon} + +); + +export const AppNavigator = () => { + const { user, initializing } = useAuth(); + + if (initializing) { + return ( + + + + ); + } + + return ( + + + {user ? ( + + ) : ( + <> + + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F8F9FA' + } +}); diff --git a/src/screens/AnalysisScreen.tsx b/src/screens/AnalysisScreen.tsx new file mode 100644 index 0000000..4e70127 --- /dev/null +++ b/src/screens/AnalysisScreen.tsx @@ -0,0 +1,480 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + Dimensions, + TouchableOpacity +} from 'react-native'; +import { PieChart, BarChart } from 'react-native-chart-kit'; +import { useAuth } from '../hooks/useAuth'; +import { transactionService } from '../services/transactionService'; +import { categoryService } from '../services/categoryService'; +import { Transaction, Category, CategoryStats } from '../types'; + +const screenWidth = Dimensions.get('window').width; + +export const AnalysisScreen = () => { + const { user } = useAuth(); + const [transactions, setTransactions] = useState([]); + const [categories, setCategories] = useState([]); + const [selectedMonth, setSelectedMonth] = useState(new Date()); + const [viewType, setViewType] = useState<'expense' | 'income'>('expense'); + + useEffect(() => { + if (!user) return; + + loadData(); + }, [user, selectedMonth]); + + const loadData = async () => { + if (!user) return; + + try { + // Charger les catĂ©gories + const userCategories = await categoryService.getCategories(user.uid); + setCategories(userCategories); + + // Charger les transactions du mois sĂ©lectionnĂ© + const monthTransactions = await transactionService.getMonthlyTransactions( + user.uid, + selectedMonth.getFullYear(), + selectedMonth.getMonth() + ); + setTransactions(monthTransactions); + } catch (error) { + console.error('Erreur lors du chargement des donnĂ©es:', error); + } + }; + + const getCategoryStats = (): CategoryStats[] => { + const filteredTransactions = transactions.filter((t) => t.type === viewType); + const total = filteredTransactions.reduce((sum, t) => sum + t.amount, 0); + + const categoryMap = new Map(); + + filteredTransactions.forEach((t) => { + const current = categoryMap.get(t.category) || { total: 0, count: 0 }; + categoryMap.set(t.category, { + total: current.total + t.amount, + count: current.count + 1 + }); + }); + + const stats: CategoryStats[] = []; + categoryMap.forEach((value, category) => { + stats.push({ + category, + total: value.total, + count: value.count, + percentage: total > 0 ? (value.total / total) * 100 : 0 + }); + }); + + return stats.sort((a, b) => b.total - a.total); + }; + + const getPieChartData = () => { + const stats = getCategoryStats(); + + return stats.map((stat) => { + const category = categories.find((c) => c.name === stat.category); + return { + name: stat.category, + amount: stat.total, + color: category?.color || '#95A5A6', + legendFontColor: '#333', + legendFontSize: 12 + }; + }); + }; + + const getMonthlyTrend = () => { + const months: string[] = []; + const incomeData: number[] = []; + const expenseData: number[] = []; + + // RĂ©cupĂ©rer les 6 derniers mois + for (let i = 5; i >= 0; i--) { + const date = new Date(); + date.setMonth(date.getMonth() - i); + + const monthName = new Intl.DateTimeFormat('fr-FR', { month: 'short' }).format(date); + months.push(monthName); + + const monthTransactions = transactions.filter((t) => { + const tDate = new Date(t.date); + return ( + tDate.getMonth() === date.getMonth() && + tDate.getFullYear() === date.getFullYear() + ); + }); + + const income = monthTransactions + .filter((t) => t.type === 'income') + .reduce((sum, t) => sum + t.amount, 0); + + const expense = monthTransactions + .filter((t) => t.type === 'expense') + .reduce((sum, t) => sum + t.amount, 0); + + incomeData.push(income); + expenseData.push(expense); + } + + return { months, incomeData, expenseData }; + }; + + const stats = getCategoryStats(); + const pieData = getPieChartData(); + const totalAmount = stats.reduce((sum, s) => sum + s.total, 0); + + const chartConfig = { + backgroundColor: '#FFF', + backgroundGradientFrom: '#FFF', + backgroundGradientTo: '#FFF', + decimalPlaces: 0, + color: (opacity = 1) => `rgba(74, 144, 226, ${opacity})`, + labelColor: (opacity = 1) => `rgba(51, 51, 51, ${opacity})`, + style: { + borderRadius: 16 + }, + propsForLabels: { + fontSize: 12 + } + }; + + const getMonthName = () => { + return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format( + selectedMonth + ); + }; + + const changeMonth = (direction: 'prev' | 'next') => { + const newDate = new Date(selectedMonth); + if (direction === 'prev') { + newDate.setMonth(newDate.getMonth() - 1); + } else { + newDate.setMonth(newDate.getMonth() + 1); + } + setSelectedMonth(newDate); + }; + + return ( + + + Analyses + + + + changeMonth('prev')} style={styles.monthButton}> + ← + + {getMonthName()} + changeMonth('next')} style={styles.monthButton}> + → + + + + + setViewType('expense')} + > + + DĂ©penses + + + setViewType('income')} + > + + Revenus + + + + + {pieData.length > 0 ? ( + <> + + RĂ©partition par catĂ©gorie + + + + + + + Total {viewType === 'expense' ? 'dĂ©penses' : 'revenus'} + + + {totalAmount.toFixed(2)} € + + + + {stats.map((stat, index) => { + const category = categories.find((c) => c.name === stat.category); + return ( + + + + {category?.icon || '📩'} + + + {stat.category} + {stat.count} transaction(s) + + + + {stat.total.toFixed(2)} € + {stat.percentage.toFixed(1)}% + + + ); + })} + + + ) : ( + + 📊 + Aucune donnĂ©e + + Ajoutez des transactions pour voir vos analyses + + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F8F9FA' + }, + header: { + padding: 24, + paddingTop: 60, + backgroundColor: '#FFF' + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#333' + }, + monthSelector: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 24, + paddingTop: 16 + }, + monthButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#FFF', + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 + }, + monthButtonText: { + fontSize: 20, + color: '#4A90E2' + }, + monthText: { + fontSize: 18, + fontWeight: '600', + color: '#333', + textTransform: 'capitalize' + }, + typeSelector: { + flexDirection: 'row', + gap: 12, + paddingHorizontal: 24, + marginBottom: 24 + }, + typeButton: { + flex: 1, + padding: 12, + borderRadius: 8, + borderWidth: 2, + borderColor: '#E0E0E0', + alignItems: 'center', + backgroundColor: '#FFF' + }, + typeButtonActive: { + borderColor: '#4A90E2', + backgroundColor: '#F0F7FF' + }, + typeButtonText: { + fontSize: 16, + fontWeight: '600', + color: '#666' + }, + typeButtonTextActive: { + color: '#4A90E2' + }, + chartContainer: { + backgroundColor: '#FFF', + marginHorizontal: 24, + marginBottom: 24, + padding: 16, + borderRadius: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3 + }, + chartTitle: { + fontSize: 16, + fontWeight: '600', + color: '#333', + marginBottom: 16 + }, + statsContainer: { + paddingHorizontal: 24, + paddingBottom: 24 + }, + totalCard: { + backgroundColor: '#4A90E2', + padding: 20, + borderRadius: 12, + marginBottom: 16, + alignItems: 'center' + }, + totalLabel: { + fontSize: 14, + color: '#FFF', + opacity: 0.9, + marginBottom: 8 + }, + totalAmount: { + fontSize: 32, + fontWeight: 'bold', + color: '#FFF' + }, + incomeColor: { + color: '#FFF' + }, + expenseColor: { + color: '#FFF' + }, + statCard: { + backgroundColor: '#FFF', + padding: 16, + borderRadius: 12, + marginBottom: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2 + }, + statLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1 + }, + statIcon: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12 + }, + statEmoji: { + fontSize: 24 + }, + statInfo: { + flex: 1 + }, + statCategory: { + fontSize: 16, + fontWeight: '600', + color: '#333', + marginBottom: 4 + }, + statCount: { + fontSize: 12, + color: '#999' + }, + statRight: { + alignItems: 'flex-end' + }, + statAmount: { + fontSize: 16, + fontWeight: '700', + color: '#333', + marginBottom: 4 + }, + statPercentage: { + fontSize: 12, + color: '#4A90E2', + fontWeight: '600' + }, + emptyState: { + alignItems: 'center', + paddingVertical: 60 + }, + emptyIcon: { + fontSize: 64, + marginBottom: 16 + }, + emptyText: { + fontSize: 18, + fontWeight: '600', + color: '#666', + marginBottom: 8 + }, + emptySubtext: { + fontSize: 14, + color: '#999', + textAlign: 'center' + } +}); diff --git a/src/screens/DashboardScreen.tsx b/src/screens/DashboardScreen.tsx new file mode 100644 index 0000000..bd87424 --- /dev/null +++ b/src/screens/DashboardScreen.tsx @@ -0,0 +1,317 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + RefreshControl, + TouchableOpacity +} from 'react-native'; +import { useAuth } from '../hooks/useAuth'; +import { transactionService } from '../services/transactionService'; +import { Transaction } from '../types'; +import { TransactionCard } from '../components/TransactionCard'; + +export const DashboardScreen = ({ navigation }: any) => { + const { user, logout } = useAuth(); + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + useEffect(() => { + if (!user) return; + + const unsubscribe = transactionService.subscribeToTransactions( + user.uid, + (newTransactions) => { + setTransactions(newTransactions); + setLoading(false); + setRefreshing(false); + } + ); + + return () => unsubscribe(); + }, [user]); + + const onRefresh = () => { + setRefreshing(true); + }; + + const getCurrentMonthStats = () => { + const now = new Date(); + const currentMonth = now.getMonth(); + const currentYear = now.getFullYear(); + + const monthlyTransactions = transactions.filter((t) => { + const transactionDate = new Date(t.date); + return ( + transactionDate.getMonth() === currentMonth && + transactionDate.getFullYear() === currentYear + ); + }); + + const totalIncome = monthlyTransactions + .filter((t) => t.type === 'income') + .reduce((sum, t) => sum + t.amount, 0); + + const totalExpenses = monthlyTransactions + .filter((t) => t.type === 'expense') + .reduce((sum, t) => sum + t.amount, 0); + + const balance = totalIncome - totalExpenses; + + return { totalIncome, totalExpenses, balance }; + }; + + const stats = getCurrentMonthStats(); + const recentTransactions = transactions.slice(0, 5); + + const getMonthName = () => { + return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format( + new Date() + ); + }; + + return ( + } + > + + + Bonjour 👋 + {getMonthName()} + + + DĂ©connexion + + + + + Solde du mois + = 0 ? styles.positiveBalance : styles.negativeBalance + ]} + > + {stats.balance >= 0 ? '+' : ''}{stats.balance.toFixed(2)} € + + + + Revenus + +{stats.totalIncome.toFixed(2)} € + + + + DĂ©penses + -{stats.totalExpenses.toFixed(2)} € + + + + + + + Transactions rĂ©centes + {transactions.length > 5 && ( + navigation.navigate('Transactions')}> + Voir tout + + )} + + + {loading ? ( + Chargement... + ) : recentTransactions.length === 0 ? ( + + 📊 + Aucune transaction + + Ajoutez votre premiĂšre transaction pour commencer + + + ) : ( + recentTransactions.map((transaction) => ( + + )) + )} + + + + navigation.navigate('Transactions', { type: 'expense' })} + > + ➖ + DĂ©pense + + navigation.navigate('Transactions', { type: 'income' })} + > + ➕ + Revenu + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F8F9FA' + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 24, + paddingTop: 60 + }, + greeting: { + fontSize: 28, + fontWeight: 'bold', + color: '#333' + }, + monthLabel: { + fontSize: 14, + color: '#666', + marginTop: 4, + textTransform: 'capitalize' + }, + logoutButton: { + padding: 8 + }, + logoutText: { + color: '#4A90E2', + fontSize: 14, + fontWeight: '600' + }, + balanceCard: { + backgroundColor: '#4A90E2', + margin: 24, + marginTop: 0, + padding: 24, + borderRadius: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 5 + }, + balanceLabel: { + fontSize: 14, + color: '#FFF', + opacity: 0.9, + marginBottom: 8 + }, + balanceAmount: { + fontSize: 36, + fontWeight: 'bold', + marginBottom: 20 + }, + positiveBalance: { + color: '#FFF' + }, + negativeBalance: { + color: '#FFE0E0' + }, + statsRow: { + flexDirection: 'row', + justifyContent: 'space-around' + }, + statItem: { + flex: 1, + alignItems: 'center' + }, + statDivider: { + width: 1, + backgroundColor: '#FFF', + opacity: 0.3 + }, + statLabel: { + fontSize: 12, + color: '#FFF', + opacity: 0.8, + marginBottom: 4 + }, + incomeText: { + fontSize: 16, + fontWeight: '600', + color: '#FFF' + }, + expenseText: { + fontSize: 16, + fontWeight: '600', + color: '#FFF' + }, + section: { + padding: 24, + paddingTop: 0 + }, + sectionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 16 + }, + sectionTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#333' + }, + seeAllText: { + fontSize: 14, + color: '#4A90E2', + fontWeight: '600' + }, + emptyState: { + alignItems: 'center', + paddingVertical: 40 + }, + emptyIcon: { + fontSize: 48, + marginBottom: 12 + }, + emptyText: { + fontSize: 16, + color: '#666', + textAlign: 'center' + }, + emptySubtext: { + fontSize: 14, + color: '#999', + textAlign: 'center', + marginTop: 8 + }, + quickActions: { + flexDirection: 'row', + padding: 24, + paddingTop: 0, + gap: 12 + }, + actionButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + padding: 16, + borderRadius: 12, + gap: 8 + }, + addExpenseButton: { + backgroundColor: '#FF6B6B' + }, + addIncomeButton: { + backgroundColor: '#52C41A' + }, + actionIcon: { + fontSize: 20 + }, + actionText: { + color: '#FFF', + fontSize: 16, + fontWeight: '600' + } +}); diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx new file mode 100644 index 0000000..d9662d9 --- /dev/null +++ b/src/screens/LoginScreen.tsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + KeyboardAvoidingView, + Platform, + ScrollView, + Alert +} from 'react-native'; +import { InputText } from '../components/InputText'; +import { Button } from '../components/Button'; +import { useAuth } from '../hooks/useAuth'; + +export const LoginScreen = ({ navigation }: any) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); + const { login, loading } = useAuth(); + + const validateForm = () => { + const newErrors: { email?: string; password?: string } = {}; + + if (!email.trim()) { + newErrors.email = 'L\'email est requis'; + } else if (!/\S+@\S+\.\S+/.test(email)) { + newErrors.email = 'Email invalide'; + } + + if (!password) { + newErrors.password = 'Le mot de passe est requis'; + } else if (password.length < 6) { + newErrors.password = 'Le mot de passe doit contenir au moins 6 caractĂšres'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleLogin = async () => { + if (!validateForm()) return; + + try { + await login(email.trim(), password); + } catch (error: any) { + Alert.alert('Erreur de connexion', error.message); + } + }; + + return ( + + + + 💰 + WalletTracker + GĂ©rez votre budget facilement + + + + { + setEmail(text); + setErrors({ ...errors, email: undefined }); + }} + error={errors.email} + keyboardType="email-address" + autoCapitalize="none" + autoComplete="email" + /> + + { + setPassword(text); + setErrors({ ...errors, password: undefined }); + }} + error={errors.password} + secureTextEntry + autoCapitalize="none" + autoComplete="password" + /> + +