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"
+ />
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#F8F9FA'
+ },
+ scrollContent: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ padding: 24
+ },
+ header: {
+ alignItems: 'center',
+ marginBottom: 48
+ },
+ logo: {
+ fontSize: 64,
+ marginBottom: 16
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: 'bold',
+ color: '#333',
+ marginBottom: 8
+ },
+ subtitle: {
+ fontSize: 16,
+ color: '#666'
+ },
+ form: {
+ width: '100%'
+ },
+ loginButton: {
+ marginTop: 8
+ },
+ signupButton: {
+ marginTop: 12
+ }
+});
diff --git a/src/screens/SignupScreen.tsx b/src/screens/SignupScreen.tsx
new file mode 100644
index 0000000..0d54472
--- /dev/null
+++ b/src/screens/SignupScreen.tsx
@@ -0,0 +1,202 @@
+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';
+import { categoryService } from '../services/categoryService';
+
+export const SignupScreen = ({ navigation }: any) => {
+ const [displayName, setDisplayName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [errors, setErrors] = useState<{
+ displayName?: string;
+ email?: string;
+ password?: string;
+ confirmPassword?: string;
+ }>({});
+ const { signup, loading } = useAuth();
+
+ const validateForm = () => {
+ const newErrors: any = {};
+
+ if (!displayName.trim()) {
+ newErrors.displayName = 'Le nom est requis';
+ }
+
+ 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';
+ }
+
+ if (!confirmPassword) {
+ newErrors.confirmPassword = 'Veuillez confirmer le mot de passe';
+ } else if (password !== confirmPassword) {
+ newErrors.confirmPassword = 'Les mots de passe ne correspondent pas';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSignup = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const user = await signup(email.trim(), password, displayName.trim());
+
+ // Initialiser les catégories par défaut pour le nouvel utilisateur
+ // Note: user sera disponible via le hook useAuth aprÚs la création
+ Alert.alert(
+ 'Compte créé',
+ 'Votre compte a été créé avec succÚs !',
+ [{ text: 'OK' }]
+ );
+ } catch (error: any) {
+ Alert.alert('Erreur d\'inscription', error.message);
+ }
+ };
+
+ return (
+
+
+
+ đ°
+ Créer un compte
+ Commencez à gérer votre budget
+
+
+
+ {
+ setDisplayName(text);
+ setErrors({ ...errors, displayName: undefined });
+ }}
+ error={errors.displayName}
+ autoCapitalize="words"
+ />
+
+ {
+ 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"
+ />
+
+ {
+ setConfirmPassword(text);
+ setErrors({ ...errors, confirmPassword: undefined });
+ }}
+ error={errors.confirmPassword}
+ secureTextEntry
+ autoCapitalize="none"
+ autoComplete="password"
+ />
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#F8F9FA'
+ },
+ scrollContent: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ padding: 24
+ },
+ header: {
+ alignItems: 'center',
+ marginBottom: 48
+ },
+ logo: {
+ fontSize: 64,
+ marginBottom: 16
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: 'bold',
+ color: '#333',
+ marginBottom: 8
+ },
+ subtitle: {
+ fontSize: 16,
+ color: '#666'
+ },
+ form: {
+ width: '100%'
+ },
+ signupButton: {
+ marginTop: 8
+ },
+ loginButton: {
+ marginTop: 12
+ }
+});
diff --git a/src/screens/SubscriptionScreen.tsx b/src/screens/SubscriptionScreen.tsx
new file mode 100644
index 0000000..efff19d
--- /dev/null
+++ b/src/screens/SubscriptionScreen.tsx
@@ -0,0 +1,444 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Modal,
+ Alert,
+ Platform
+} from 'react-native';
+import { useAuth } from '../hooks/useAuth';
+import { subscriptionService } from '../services/subscriptionService';
+import { categoryService } from '../services/categoryService';
+import { Subscription, SubscriptionFrequency, Category } from '../types';
+import { SubscriptionCard } from '../components/SubscriptionCard';
+import { InputText } from '../components/InputText';
+import { Button } from '../components/Button';
+
+export const SubscriptionScreen = () => {
+ const { user } = useAuth();
+ const [subscriptions, setSubscriptions] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ // Form state
+ const [name, setName] = useState('');
+ const [amount, setAmount] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('');
+ const [frequency, setFrequency] = useState('monthly');
+ const [dayOfMonth, setDayOfMonth] = useState('1');
+
+ useEffect(() => {
+ if (!user) return;
+
+ // Charger les catégories
+ loadCategories();
+
+ // Ăcouter les abonnements
+ const unsubscribe = subscriptionService.subscribeToSubscriptions(
+ user.uid,
+ (newSubscriptions) => {
+ setSubscriptions(newSubscriptions);
+ }
+ );
+
+ return () => unsubscribe();
+ }, [user]);
+
+ const loadCategories = async () => {
+ if (!user) return;
+
+ try {
+ const userCategories = await categoryService.getCategories(user.uid);
+ const expenseCategories = userCategories.filter((c) => c.type === 'expense');
+ setCategories(expenseCategories);
+
+ if (expenseCategories.length > 0) {
+ setSelectedCategory(expenseCategories[0].name);
+ }
+ } catch (error) {
+ console.error('Erreur lors du chargement des catégories:', error);
+ }
+ };
+
+ const handleAddSubscription = async () => {
+ if (!user) return;
+
+ if (!name.trim()) {
+ Alert.alert('Erreur', 'Veuillez entrer un nom pour l\'abonnement');
+ return;
+ }
+
+ if (!amount || parseFloat(amount) <= 0) {
+ Alert.alert('Erreur', 'Veuillez entrer un montant valide');
+ return;
+ }
+
+ if (!selectedCategory) {
+ Alert.alert('Erreur', 'Veuillez sélectionner une catégorie');
+ return;
+ }
+
+ const day = parseInt(dayOfMonth);
+ if (isNaN(day) || day < 1 || day > 31) {
+ Alert.alert('Erreur', 'Veuillez entrer un jour valide (1-31)');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // Calculer la prochaine date de paiement
+ const now = new Date();
+ const nextPaymentDate = new Date(now.getFullYear(), now.getMonth(), day);
+
+ // Si la date est déjà passée ce mois-ci, passer au mois suivant
+ if (nextPaymentDate < now) {
+ nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
+ }
+
+ await subscriptionService.addSubscription(
+ user.uid,
+ name.trim(),
+ parseFloat(amount),
+ selectedCategory,
+ frequency,
+ nextPaymentDate,
+ 3 // Rappel 3 jours avant
+ );
+
+ // Réinitialiser le formulaire
+ setName('');
+ setAmount('');
+ setDayOfMonth('1');
+ setFrequency('monthly');
+ setModalVisible(false);
+
+ Alert.alert('SuccÚs', 'Abonnement ajouté avec succÚs');
+ } catch (error: any) {
+ Alert.alert('Erreur', error.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getTotalMonthly = () => {
+ return subscriptions
+ .filter((s) => s.isActive)
+ .reduce((sum, s) => {
+ // Convertir en coût mensuel
+ switch (s.frequency) {
+ case 'daily':
+ return sum + s.amount * 30;
+ case 'weekly':
+ return sum + s.amount * 4;
+ case 'monthly':
+ return sum + s.amount;
+ case 'yearly':
+ return sum + s.amount / 12;
+ default:
+ return sum;
+ }
+ }, 0);
+ };
+
+ const getCategoryInfo = (categoryName: string) => {
+ const category = categories.find((c) => c.name === categoryName);
+ return category || { icon: 'đ±', color: '#F8B739' };
+ };
+
+ const frequencyOptions: { value: SubscriptionFrequency; label: string }[] = [
+ { value: 'monthly', label: 'Mensuel' },
+ { value: 'yearly', label: 'Annuel' },
+ { value: 'weekly', label: 'Hebdomadaire' }
+ ];
+
+ return (
+
+
+
+ Abonnements
+
+ Total mensuel: {getTotalMonthly().toFixed(2)} âŹ
+
+
+ setModalVisible(true)}
+ >
+ + Ajouter
+
+
+
+
+ {subscriptions.length === 0 ? (
+
+ đ±
+ Aucun abonnement
+
+ Ajoutez vos abonnements pour suivre vos dépenses récurrentes
+
+
+ ) : (
+ subscriptions.map((subscription) => {
+ const categoryInfo = getCategoryInfo(subscription.category);
+ return (
+
+ );
+ })
+ )}
+
+
+ setModalVisible(false)}
+ >
+
+
+
+ Nouvel abonnement
+ setModalVisible(false)}>
+ â
+
+
+
+
+
+
+
+
+ Fréquence
+
+ {frequencyOptions.map((option) => (
+ setFrequency(option.value)}
+ >
+
+ {option.label}
+
+
+ ))}
+
+
+ {frequency === 'monthly' && (
+
+ )}
+
+ Catégorie
+
+ {categories.map((category) => (
+ setSelectedCategory(category.name)}
+ >
+ {category.icon}
+ {category.name}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#F8F9FA'
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 24,
+ paddingTop: 60,
+ backgroundColor: '#FFF'
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: '#333'
+ },
+ totalText: {
+ fontSize: 14,
+ color: '#666',
+ marginTop: 4
+ },
+ addButton: {
+ backgroundColor: '#4A90E2',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 8
+ },
+ addButtonText: {
+ color: '#FFF',
+ fontSize: 14,
+ fontWeight: '600'
+ },
+ content: {
+ flex: 1,
+ padding: 24
+ },
+ 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',
+ paddingHorizontal: 40
+ },
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'flex-end'
+ },
+ modalContent: {
+ backgroundColor: '#FFF',
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ padding: 24,
+ maxHeight: '90%'
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 24
+ },
+ modalTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#333'
+ },
+ closeButton: {
+ fontSize: 24,
+ color: '#999'
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 12
+ },
+ frequencySelector: {
+ flexDirection: 'row',
+ gap: 8,
+ marginBottom: 24
+ },
+ frequencyButton: {
+ flex: 1,
+ padding: 12,
+ borderRadius: 8,
+ borderWidth: 2,
+ borderColor: '#E0E0E0',
+ alignItems: 'center'
+ },
+ frequencyButtonActive: {
+ borderColor: '#4A90E2',
+ backgroundColor: '#F0F7FF'
+ },
+ frequencyButtonText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#666'
+ },
+ frequencyButtonTextActive: {
+ color: '#4A90E2'
+ },
+ categoryGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 12,
+ marginBottom: 24
+ },
+ categoryItem: {
+ width: '30%',
+ aspectRatio: 1,
+ borderRadius: 12,
+ borderWidth: 2,
+ borderColor: '#E0E0E0',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F8F9FA'
+ },
+ categoryItemActive: {
+ backgroundColor: '#FFF',
+ borderWidth: 2
+ },
+ categoryIcon: {
+ fontSize: 28,
+ marginBottom: 4
+ },
+ categoryName: {
+ fontSize: 11,
+ color: '#666',
+ textAlign: 'center'
+ },
+ submitButton: {
+ marginTop: 8,
+ marginBottom: Platform.OS === 'ios' ? 20 : 0
+ }
+});
diff --git a/src/screens/TransactionScreen.tsx b/src/screens/TransactionScreen.tsx
new file mode 100644
index 0000000..08048be
--- /dev/null
+++ b/src/screens/TransactionScreen.tsx
@@ -0,0 +1,414 @@
+import React, { useState, useEffect } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Modal,
+ Alert,
+ Platform
+} from 'react-native';
+import { useAuth } from '../hooks/useAuth';
+import { transactionService } from '../services/transactionService';
+import { categoryService } from '../services/categoryService';
+import { Transaction, Category, TransactionType } from '../types';
+import { TransactionCard } from '../components/TransactionCard';
+import { InputText } from '../components/InputText';
+import { Button } from '../components/Button';
+
+export const TransactionScreen = ({ route }: any) => {
+ const { user } = useAuth();
+ const [transactions, setTransactions] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [modalVisible, setModalVisible] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ // Form state
+ const [type, setType] = useState(route?.params?.type || 'expense');
+ const [amount, setAmount] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('');
+ const [note, setNote] = useState('');
+ const [date, setDate] = useState(new Date());
+
+ useEffect(() => {
+ if (!user) return;
+
+ // Charger les catégories
+ loadCategories();
+
+ // Ăcouter les transactions
+ const unsubscribe = transactionService.subscribeToTransactions(
+ user.uid,
+ (newTransactions) => {
+ setTransactions(newTransactions);
+ }
+ );
+
+ return () => unsubscribe();
+ }, [user]);
+
+ const loadCategories = async () => {
+ if (!user) return;
+
+ try {
+ let userCategories = await categoryService.getCategories(user.uid);
+
+ // Si l'utilisateur n'a pas de catégories, initialiser les catégories par défaut
+ if (userCategories.length === 0) {
+ await categoryService.initializeDefaultCategories(user.uid);
+ userCategories = await categoryService.getCategories(user.uid);
+ }
+
+ setCategories(userCategories);
+
+ // Sélectionner la premiÚre catégorie du type approprié
+ const defaultCategory = userCategories.find((c) => c.type === type);
+ if (defaultCategory) {
+ setSelectedCategory(defaultCategory.name);
+ }
+ } catch (error) {
+ console.error('Erreur lors du chargement des catégories:', error);
+ }
+ };
+
+ const handleAddTransaction = async () => {
+ if (!user) return;
+
+ if (!amount || parseFloat(amount) <= 0) {
+ Alert.alert('Erreur', 'Veuillez entrer un montant valide');
+ return;
+ }
+
+ if (!selectedCategory) {
+ Alert.alert('Erreur', 'Veuillez sélectionner une catégorie');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ await transactionService.addTransaction(
+ user.uid,
+ type,
+ parseFloat(amount),
+ selectedCategory,
+ date,
+ note
+ );
+
+ // Réinitialiser le formulaire
+ setAmount('');
+ setNote('');
+ setDate(new Date());
+ setModalVisible(false);
+
+ Alert.alert('SuccÚs', 'Transaction ajoutée avec succÚs');
+ } catch (error: any) {
+ Alert.alert('Erreur', error.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const filteredCategories = categories.filter((c) => c.type === type);
+
+ const getCategoryInfo = (categoryName: string) => {
+ const category = categories.find((c) => c.name === categoryName);
+ return category || { icon: 'đŠ', color: '#95A5A6' };
+ };
+
+ return (
+
+
+ Transactions
+ setModalVisible(true)}
+ >
+ + Ajouter
+
+
+
+
+ {transactions.length === 0 ? (
+
+ đž
+ Aucune transaction
+
+ Ajoutez votre premiĂšre transaction pour commencer
+
+
+ ) : (
+ transactions.map((transaction) => {
+ const categoryInfo = getCategoryInfo(transaction.category);
+ return (
+
+ );
+ })
+ )}
+
+
+ setModalVisible(false)}
+ >
+
+
+
+ Nouvelle transaction
+ setModalVisible(false)}>
+ â
+
+
+
+
+
+ {
+ setType('expense');
+ const defaultCategory = filteredCategories.find((c) => c.type === 'expense');
+ if (defaultCategory) setSelectedCategory(defaultCategory.name);
+ }}
+ >
+
+ Dépense
+
+
+ {
+ setType('income');
+ const defaultCategory = filteredCategories.find((c) => c.type === 'income');
+ if (defaultCategory) setSelectedCategory(defaultCategory.name);
+ }}
+ >
+
+ Revenu
+
+
+
+
+
+
+ Catégorie
+
+ {filteredCategories.map((category) => (
+ setSelectedCategory(category.name)}
+ >
+ {category.icon}
+ {category.name}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#F8F9FA'
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 24,
+ paddingTop: 60,
+ backgroundColor: '#FFF'
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: '#333'
+ },
+ addButton: {
+ backgroundColor: '#4A90E2',
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 8
+ },
+ addButtonText: {
+ color: '#FFF',
+ fontSize: 14,
+ fontWeight: '600'
+ },
+ content: {
+ flex: 1,
+ padding: 24
+ },
+ 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'
+ },
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'flex-end'
+ },
+ modalContent: {
+ backgroundColor: '#FFF',
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ padding: 24,
+ maxHeight: '90%'
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 24
+ },
+ modalTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#333'
+ },
+ closeButton: {
+ fontSize: 24,
+ color: '#999'
+ },
+ typeSelector: {
+ flexDirection: 'row',
+ gap: 12,
+ marginBottom: 24
+ },
+ typeButton: {
+ flex: 1,
+ padding: 12,
+ borderRadius: 8,
+ borderWidth: 2,
+ borderColor: '#E0E0E0',
+ alignItems: 'center'
+ },
+ typeButtonActive: {
+ borderWidth: 2
+ },
+ expenseButton: {
+ borderColor: '#FF6B6B'
+ },
+ incomeButton: {
+ borderColor: '#52C41A'
+ },
+ typeButtonText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#666'
+ },
+ typeButtonTextActive: {
+ color: '#333'
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 12
+ },
+ categoryGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 12,
+ marginBottom: 24
+ },
+ categoryItem: {
+ width: '30%',
+ aspectRatio: 1,
+ borderRadius: 12,
+ borderWidth: 2,
+ borderColor: '#E0E0E0',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F8F9FA'
+ },
+ categoryItemActive: {
+ backgroundColor: '#FFF',
+ borderWidth: 2
+ },
+ categoryIcon: {
+ fontSize: 28,
+ marginBottom: 4
+ },
+ categoryName: {
+ fontSize: 11,
+ color: '#666',
+ textAlign: 'center'
+ },
+ submitButton: {
+ marginTop: 8,
+ marginBottom: Platform.OS === 'ios' ? 20 : 0
+ }
+});
diff --git a/src/services/authService.ts b/src/services/authService.ts
new file mode 100644
index 0000000..df6e83b
--- /dev/null
+++ b/src/services/authService.ts
@@ -0,0 +1,96 @@
+import {
+ createUserWithEmailAndPassword,
+ signInWithEmailAndPassword,
+ signOut,
+ User as FirebaseUser,
+ updateProfile
+} from 'firebase/auth';
+import { doc, setDoc, getDoc } from 'firebase/firestore';
+import { auth, db } from '../config/firebase';
+import { User } from '../types';
+
+export const authService = {
+ // Inscription
+ async signup(email: string, password: string, displayName?: string): Promise {
+ try {
+ const userCredential = await createUserWithEmailAndPassword(auth, email, password);
+ const user = userCredential.user;
+
+ // Mettre Ă jour le profil avec le nom d'affichage
+ if (displayName) {
+ await updateProfile(user, { displayName });
+ }
+
+ // Créer le document utilisateur dans Firestore
+ const userData: Omit = {
+ email: user.email!,
+ displayName: displayName || undefined,
+ createdAt: new Date(),
+ sharedWith: []
+ };
+
+ await setDoc(doc(db, 'users', user.uid), userData);
+
+ return user;
+ } catch (error: any) {
+ throw new Error(this.getErrorMessage(error.code));
+ }
+ },
+
+ // Connexion
+ async login(email: string, password: string): Promise {
+ try {
+ const userCredential = await signInWithEmailAndPassword(auth, email, password);
+ return userCredential.user;
+ } catch (error: any) {
+ throw new Error(this.getErrorMessage(error.code));
+ }
+ },
+
+ // Déconnexion
+ async logout(): Promise {
+ try {
+ await signOut(auth);
+ } catch (error: any) {
+ throw new Error('Erreur lors de la déconnexion');
+ }
+ },
+
+ // Récupérer les données utilisateur depuis Firestore
+ async getUserData(uid: string): Promise {
+ try {
+ const userDoc = await getDoc(doc(db, 'users', uid));
+ if (userDoc.exists()) {
+ return { uid, ...userDoc.data() } as User;
+ }
+ return null;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des données utilisateur:', error);
+ return null;
+ }
+ },
+
+ // Messages d'erreur en français
+ getErrorMessage(errorCode: string): string {
+ switch (errorCode) {
+ case 'auth/email-already-in-use':
+ return 'Cette adresse email est déjà utilisée';
+ case 'auth/invalid-email':
+ return 'Adresse email invalide';
+ case 'auth/operation-not-allowed':
+ return 'Opération non autorisée';
+ case 'auth/weak-password':
+ return 'Le mot de passe doit contenir au moins 6 caractĂšres';
+ case 'auth/user-disabled':
+ return 'Ce compte a été désactivé';
+ case 'auth/user-not-found':
+ return 'Aucun compte trouvé avec cette adresse email';
+ case 'auth/wrong-password':
+ return 'Mot de passe incorrect';
+ case 'auth/invalid-credential':
+ return 'Identifiants invalides';
+ default:
+ return 'Une erreur est survenue. Veuillez réessayer.';
+ }
+ }
+};
diff --git a/src/services/categoryService.ts b/src/services/categoryService.ts
new file mode 100644
index 0000000..a92ffa3
--- /dev/null
+++ b/src/services/categoryService.ts
@@ -0,0 +1,156 @@
+import {
+ collection,
+ addDoc,
+ updateDoc,
+ deleteDoc,
+ doc,
+ query,
+ where,
+ getDocs,
+ onSnapshot
+} from 'firebase/firestore';
+import { db } from '../config/firebase';
+import { Category, TransactionType } from '../types';
+
+// Catégories par défaut
+export const DEFAULT_CATEGORIES: Omit[] = [
+ // Dépenses
+ { name: 'Courses', icon: 'đ', color: '#FF6B6B', type: 'expense' },
+ { name: 'Logement', icon: 'đ ', color: '#4ECDC4', type: 'expense' },
+ { name: 'Transport', icon: 'đ', color: '#45B7D1', type: 'expense' },
+ { name: 'Loisirs', icon: 'đź', color: '#FFA07A', type: 'expense' },
+ { name: 'Restaurant', icon: 'đœïž', color: '#98D8C8', type: 'expense' },
+ { name: 'SantĂ©', icon: 'đ', color: '#F7DC6F', type: 'expense' },
+ { name: 'VĂȘtements', icon: 'đ', color: '#BB8FCE', type: 'expense' },
+ { name: 'Ăducation', icon: 'đ', color: '#85C1E2', type: 'expense' },
+ { name: 'Abonnements', icon: 'đ±', color: '#F8B739', type: 'expense' },
+ { name: 'Autre', icon: 'đŠ', color: '#95A5A6', type: 'expense' },
+
+ // Revenus
+ { name: 'Salaire', icon: 'đ°', color: '#52C41A', type: 'income' },
+ { name: 'Freelance', icon: 'đŒ', color: '#13C2C2', type: 'income' },
+ { name: 'Investissement', icon: 'đ', color: '#1890FF', type: 'income' },
+ { name: 'Cadeau', icon: 'đ', color: '#EB2F96', type: 'income' },
+ { name: 'Autre', icon: 'đ”', color: '#52C41A', type: 'income' }
+];
+
+export const categoryService = {
+ // Initialiser les catégories par défaut pour un utilisateur
+ async initializeDefaultCategories(userId: string): Promise {
+ try {
+ const promises = DEFAULT_CATEGORIES.map((category) =>
+ addDoc(collection(db, 'categories'), {
+ ...category,
+ userId
+ })
+ );
+
+ await Promise.all(promises);
+ } catch (error) {
+ console.error('Erreur lors de l\'initialisation des catégories:', error);
+ throw new Error('Impossible d\'initialiser les catégories');
+ }
+ },
+
+ // Ajouter une catégorie personnalisée
+ async addCategory(
+ userId: string,
+ name: string,
+ icon: string,
+ color: string,
+ type: TransactionType
+ ): Promise {
+ try {
+ const categoryData = {
+ userId,
+ name,
+ icon,
+ color,
+ type
+ };
+
+ const docRef = await addDoc(collection(db, 'categories'), categoryData);
+ return docRef.id;
+ } catch (error) {
+ console.error('Erreur lors de l\'ajout de la catégorie:', error);
+ throw new Error('Impossible d\'ajouter la catégorie');
+ }
+ },
+
+ // Mettre à jour une catégorie
+ async updateCategory(
+ categoryId: string,
+ updates: Partial>
+ ): Promise {
+ try {
+ const categoryRef = doc(db, 'categories', categoryId);
+ await updateDoc(categoryRef, updates);
+ } catch (error) {
+ console.error('Erreur lors de la mise à jour de la catégorie:', error);
+ throw new Error('Impossible de mettre à jour la catégorie');
+ }
+ },
+
+ // Supprimer une catégorie
+ async deleteCategory(categoryId: string): Promise {
+ try {
+ await deleteDoc(doc(db, 'categories', categoryId));
+ } catch (error) {
+ console.error('Erreur lors de la suppression de la catégorie:', error);
+ throw new Error('Impossible de supprimer la catégorie');
+ }
+ },
+
+ // Récupérer les catégories d'un utilisateur
+ async getCategories(userId: string): Promise {
+ try {
+ const q = query(collection(db, 'categories'), where('userId', '==', userId));
+ const snapshot = await getDocs(q);
+
+ const categories: Category[] = [];
+ snapshot.forEach((doc) => {
+ const data = doc.data();
+ categories.push({
+ id: doc.id,
+ userId: data.userId,
+ name: data.name,
+ icon: data.icon,
+ color: data.color,
+ type: data.type
+ });
+ });
+
+ return categories;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des catégories:', error);
+ throw new Error('Impossible de récupérer les catégories');
+ }
+ },
+
+ // Ăcouter les catĂ©gories en temps rĂ©el
+ subscribeToCategories(
+ userId: string,
+ callback: (categories: Category[]) => void
+ ): () => void {
+ const q = query(collection(db, 'categories'), where('userId', '==', userId));
+
+ const unsubscribe = onSnapshot(q, (snapshot) => {
+ const categories: Category[] = [];
+ snapshot.forEach((doc) => {
+ const data = doc.data();
+ categories.push({
+ id: doc.id,
+ userId: data.userId,
+ name: data.name,
+ icon: data.icon,
+ color: data.color,
+ type: data.type
+ });
+ });
+
+ callback(categories);
+ });
+
+ return unsubscribe;
+ }
+};
diff --git a/src/services/subscriptionService.ts b/src/services/subscriptionService.ts
new file mode 100644
index 0000000..29e0b97
--- /dev/null
+++ b/src/services/subscriptionService.ts
@@ -0,0 +1,149 @@
+import {
+ collection,
+ addDoc,
+ updateDoc,
+ deleteDoc,
+ doc,
+ query,
+ where,
+ orderBy,
+ onSnapshot,
+ Timestamp
+} from 'firebase/firestore';
+import { db } from '../config/firebase';
+import { Subscription, SubscriptionFrequency } from '../types';
+
+export const subscriptionService = {
+ // Ajouter un abonnement
+ async addSubscription(
+ userId: string,
+ name: string,
+ amount: number,
+ category: string,
+ frequency: SubscriptionFrequency,
+ nextPaymentDate: Date,
+ reminderDaysBefore: number = 3
+ ): Promise {
+ try {
+ const subscriptionData = {
+ userId,
+ name,
+ amount,
+ category,
+ frequency,
+ nextPaymentDate: Timestamp.fromDate(nextPaymentDate),
+ reminderDaysBefore,
+ isActive: true,
+ createdAt: Timestamp.fromDate(new Date()),
+ updatedAt: Timestamp.fromDate(new Date())
+ };
+
+ const docRef = await addDoc(collection(db, 'subscriptions'), subscriptionData);
+ return docRef.id;
+ } catch (error) {
+ console.error('Erreur lors de l\'ajout de l\'abonnement:', error);
+ throw new Error('Impossible d\'ajouter l\'abonnement');
+ }
+ },
+
+ // Mettre Ă jour un abonnement
+ async updateSubscription(
+ subscriptionId: string,
+ updates: Partial>
+ ): Promise {
+ try {
+ const subscriptionRef = doc(db, 'subscriptions', subscriptionId);
+ const updateData: any = {
+ ...updates,
+ updatedAt: Timestamp.fromDate(new Date())
+ };
+
+ if (updates.nextPaymentDate) {
+ updateData.nextPaymentDate = Timestamp.fromDate(updates.nextPaymentDate);
+ }
+
+ await updateDoc(subscriptionRef, updateData);
+ } catch (error) {
+ console.error('Erreur lors de la mise Ă jour de l\'abonnement:', error);
+ throw new Error('Impossible de mettre Ă jour l\'abonnement');
+ }
+ },
+
+ // Supprimer un abonnement
+ async deleteSubscription(subscriptionId: string): Promise {
+ try {
+ await deleteDoc(doc(db, 'subscriptions', subscriptionId));
+ } catch (error) {
+ console.error('Erreur lors de la suppression de l\'abonnement:', error);
+ throw new Error('Impossible de supprimer l\'abonnement');
+ }
+ },
+
+ // Ăcouter les abonnements en temps rĂ©el
+ subscribeToSubscriptions(
+ userId: string,
+ callback: (subscriptions: Subscription[]) => void
+ ): () => void {
+ const q = query(
+ collection(db, 'subscriptions'),
+ where('userId', '==', userId),
+ orderBy('nextPaymentDate', 'asc')
+ );
+
+ const unsubscribe = onSnapshot(q, (snapshot) => {
+ const subscriptions: Subscription[] = [];
+ snapshot.forEach((doc) => {
+ const data = doc.data();
+ subscriptions.push({
+ id: doc.id,
+ userId: data.userId,
+ name: data.name,
+ amount: data.amount,
+ category: data.category,
+ frequency: data.frequency,
+ nextPaymentDate: data.nextPaymentDate.toDate(),
+ reminderDaysBefore: data.reminderDaysBefore,
+ isActive: data.isActive,
+ createdAt: data.createdAt.toDate(),
+ updatedAt: data.updatedAt.toDate()
+ });
+ });
+
+ callback(subscriptions);
+ });
+
+ return unsubscribe;
+ },
+
+ // Calculer la prochaine date de paiement
+ calculateNextPaymentDate(currentDate: Date, frequency: SubscriptionFrequency): Date {
+ const nextDate = new Date(currentDate);
+
+ switch (frequency) {
+ case 'daily':
+ nextDate.setDate(nextDate.getDate() + 1);
+ break;
+ case 'weekly':
+ nextDate.setDate(nextDate.getDate() + 7);
+ break;
+ case 'monthly':
+ nextDate.setMonth(nextDate.getMonth() + 1);
+ break;
+ case 'yearly':
+ nextDate.setFullYear(nextDate.getFullYear() + 1);
+ break;
+ }
+
+ return nextDate;
+ },
+
+ // VĂ©rifier si un rappel doit ĂȘtre envoyĂ©
+ shouldSendReminder(subscription: Subscription): boolean {
+ const today = new Date();
+ const daysUntilPayment = Math.ceil(
+ (subscription.nextPaymentDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
+ );
+
+ return daysUntilPayment <= subscription.reminderDaysBefore && daysUntilPayment >= 0;
+ }
+};
diff --git a/src/services/transactionService.ts b/src/services/transactionService.ts
new file mode 100644
index 0000000..e3dbac2
--- /dev/null
+++ b/src/services/transactionService.ts
@@ -0,0 +1,188 @@
+import {
+ collection,
+ addDoc,
+ updateDoc,
+ deleteDoc,
+ doc,
+ query,
+ where,
+ orderBy,
+ onSnapshot,
+ Timestamp,
+ getDocs,
+ QueryConstraint
+} from 'firebase/firestore';
+import { db } from '../config/firebase';
+import { Transaction, TransactionType } from '../types';
+
+export const transactionService = {
+ // Ajouter une transaction
+ async addTransaction(
+ userId: string,
+ type: TransactionType,
+ amount: number,
+ category: string,
+ date: Date,
+ note?: string,
+ imageUrl?: string
+ ): Promise {
+ try {
+ const transactionData = {
+ userId,
+ type,
+ amount,
+ category,
+ date: Timestamp.fromDate(date),
+ note: note || '',
+ imageUrl: imageUrl || '',
+ createdAt: Timestamp.fromDate(new Date()),
+ updatedAt: Timestamp.fromDate(new Date())
+ };
+
+ const docRef = await addDoc(collection(db, 'transactions'), transactionData);
+ return docRef.id;
+ } catch (error) {
+ console.error('Erreur lors de l\'ajout de la transaction:', error);
+ throw new Error('Impossible d\'ajouter la transaction');
+ }
+ },
+
+ // Mettre Ă jour une transaction
+ async updateTransaction(
+ transactionId: string,
+ updates: Partial>
+ ): Promise {
+ try {
+ const transactionRef = doc(db, 'transactions', transactionId);
+ const updateData: any = {
+ ...updates,
+ updatedAt: Timestamp.fromDate(new Date())
+ };
+
+ if (updates.date) {
+ updateData.date = Timestamp.fromDate(updates.date);
+ }
+
+ await updateDoc(transactionRef, updateData);
+ } catch (error) {
+ console.error('Erreur lors de la mise Ă jour de la transaction:', error);
+ throw new Error('Impossible de mettre Ă jour la transaction');
+ }
+ },
+
+ // Supprimer une transaction
+ async deleteTransaction(transactionId: string): Promise {
+ try {
+ await deleteDoc(doc(db, 'transactions', transactionId));
+ } catch (error) {
+ console.error('Erreur lors de la suppression de la transaction:', error);
+ throw new Error('Impossible de supprimer la transaction');
+ }
+ },
+
+ // Ăcouter les transactions en temps rĂ©el
+ subscribeToTransactions(
+ userId: string,
+ callback: (transactions: Transaction[]) => void,
+ filters?: {
+ type?: TransactionType;
+ category?: string;
+ startDate?: Date;
+ endDate?: Date;
+ }
+ ): () => void {
+ const constraints: QueryConstraint[] = [
+ where('userId', '==', userId),
+ orderBy('date', 'desc')
+ ];
+
+ if (filters?.type) {
+ constraints.push(where('type', '==', filters.type));
+ }
+
+ if (filters?.category) {
+ constraints.push(where('category', '==', filters.category));
+ }
+
+ const q = query(collection(db, 'transactions'), ...constraints);
+
+ const unsubscribe = onSnapshot(q, (snapshot) => {
+ const transactions: Transaction[] = [];
+ snapshot.forEach((doc) => {
+ const data = doc.data();
+ transactions.push({
+ id: doc.id,
+ userId: data.userId,
+ type: data.type,
+ amount: data.amount,
+ category: data.category,
+ date: data.date.toDate(),
+ note: data.note,
+ imageUrl: data.imageUrl,
+ createdAt: data.createdAt.toDate(),
+ updatedAt: data.updatedAt.toDate()
+ });
+ });
+
+ // Filtrer par dates si nécessaire (cÎté client car Firestore a des limitations)
+ let filteredTransactions = transactions;
+ if (filters?.startDate || filters?.endDate) {
+ filteredTransactions = transactions.filter((t) => {
+ if (filters.startDate && t.date < filters.startDate) return false;
+ if (filters.endDate && t.date > filters.endDate) return false;
+ return true;
+ });
+ }
+
+ callback(filteredTransactions);
+ });
+
+ return unsubscribe;
+ },
+
+ // Récupérer les transactions d'un mois spécifique
+ async getMonthlyTransactions(
+ userId: string,
+ year: number,
+ month: number
+ ): Promise {
+ try {
+ const startDate = new Date(year, month, 1);
+ const endDate = new Date(year, month + 1, 0, 23, 59, 59);
+
+ const q = query(
+ collection(db, 'transactions'),
+ where('userId', '==', userId),
+ orderBy('date', 'desc')
+ );
+
+ const snapshot = await getDocs(q);
+ const transactions: Transaction[] = [];
+
+ snapshot.forEach((doc) => {
+ const data = doc.data();
+ const transactionDate = data.date.toDate();
+
+ if (transactionDate >= startDate && transactionDate <= endDate) {
+ transactions.push({
+ id: doc.id,
+ userId: data.userId,
+ type: data.type,
+ amount: data.amount,
+ category: data.category,
+ date: transactionDate,
+ note: data.note,
+ imageUrl: data.imageUrl,
+ createdAt: data.createdAt.toDate(),
+ updatedAt: data.updatedAt.toDate()
+ });
+ }
+ });
+
+ return transactions;
+ } catch (error) {
+ console.error('Erreur lors de la récupération des transactions mensuelles:', error);
+ throw new Error('Impossible de récupérer les transactions');
+ }
+ }
+};
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..fe2c1d7
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,81 @@
+// Types pour l'utilisateur
+export interface User {
+ uid: string;
+ email: string;
+ displayName?: string;
+ createdAt: Date;
+ sharedWith?: string[]; // UIDs des utilisateurs avec qui le compte est partagé
+}
+
+// Types pour les transactions
+export type TransactionType = 'expense' | 'income';
+
+export interface Transaction {
+ id: string;
+ userId: string;
+ type: TransactionType;
+ amount: number;
+ category: string;
+ date: Date;
+ note?: string;
+ imageUrl?: string;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+// Types pour les catégories
+export interface Category {
+ id: string;
+ name: string;
+ icon: string;
+ color: string;
+ type: TransactionType;
+ userId: string;
+}
+
+// Types pour les abonnements
+export type SubscriptionFrequency = 'daily' | 'weekly' | 'monthly' | 'yearly';
+
+export interface Subscription {
+ id: string;
+ userId: string;
+ name: string;
+ amount: number;
+ category: string;
+ frequency: SubscriptionFrequency;
+ nextPaymentDate: Date;
+ reminderDaysBefore: number;
+ isActive: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+// Types pour les statistiques
+export interface CategoryStats {
+ category: string;
+ total: number;
+ count: number;
+ percentage: number;
+}
+
+export interface MonthlyStats {
+ month: string;
+ totalIncome: number;
+ totalExpenses: number;
+ balance: number;
+ categoryBreakdown: CategoryStats[];
+}
+
+// Types pour la navigation
+export type RootStackParamList = {
+ Login: undefined;
+ Signup: undefined;
+ Main: undefined;
+};
+
+export type MainTabParamList = {
+ Dashboard: undefined;
+ Transactions: undefined;
+ Subscriptions: undefined;
+ Analysis: undefined;
+};