- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2263 lines
70 KiB
Markdown
2263 lines
70 KiB
Markdown
# PLANNING STRIPE - DÉVELOPPEUR FLUTTER
|
||
## App Flutter - Intégration Stripe Terminal Payments
|
||
### V1 ✅ Stripe Connect (Réalisée - 01/09/2024)
|
||
### V2 🔄 Tap to Pay (En cours de développement)
|
||
|
||
---
|
||
|
||
## 🎯 V2 - TAP TO PAY (NFC intégré uniquement)
|
||
### Période estimée : 1.5 semaine de développement
|
||
### Dernière mise à jour : 29/09/2025
|
||
|
||
### 📱 CONFIGURATIONS STRIPE TAP TO PAY CONFIRMÉES
|
||
- **iOS** : iPhone XS ou plus récent + iOS 16.4 minimum (source : Stripe docs officielles)
|
||
- **Android** : Appareils certifiés par Stripe (liste mise à jour hebdomadairement via API)
|
||
- **SDK Terminal** : Version 4.6.0 utilisée (minimum requis 2.23.0 ✅)
|
||
- **Batterie minimum** : 10% pour les paiements
|
||
- **NFC** : Obligatoire et activé
|
||
- **Web** : Non supporté (même sur mobile avec NFC)
|
||
|
||
---
|
||
|
||
## 📋 RÉSUMÉ EXÉCUTIF V2
|
||
|
||
### 🎯 Objectif Principal
|
||
Permettre aux membres des amicales de pompiers d'encaisser des paiements par carte bancaire sans contact directement depuis leur téléphone (iPhone XS+ avec iOS 16.4+ dans un premier temps).
|
||
|
||
### 💡 Fonctionnalités Clés
|
||
- **Tap to Pay** sur iPhone/Android (utilisation du NFC intégré du téléphone uniquement)
|
||
- **Montants flexibles** : Prédéfinis (10€, 20€, 30€, 50€) ou personnalisés
|
||
- **Mode offline** : File d'attente avec synchronisation automatique
|
||
- **Dashboard vendeur** : Suivi des ventes en temps réel
|
||
- **Reçus numériques** : Envoi par email/SMS
|
||
- **Multi-rôles** : Intégration avec le système de permissions existant
|
||
|
||
### ⚠️ Contraintes Techniques
|
||
- **iOS uniquement en V2.1** : iPhone XS minimum, iOS 16.4+
|
||
- **Android en V2.2** : Liste d'appareils certifiés via API
|
||
- **Connexion internet** : Requise pour initialisation, mode offline disponible ensuite
|
||
- **Compte Stripe** : L'amicale doit avoir complété l'onboarding V1
|
||
|
||
---
|
||
|
||
## 🗓️ PLANNING DÉTAILLÉ V2
|
||
|
||
### 📦 PHASE 1 : SETUP TECHNIQUE ET ARCHITECTURE
|
||
**Durée estimée : 1 jour**
|
||
**Objectif : Préparer l'environnement et l'architecture pour Stripe Tap to Pay**
|
||
|
||
#### 📚 1.1 Installation des packages (4h)
|
||
- [x] Ajouter `mek_stripe_terminal: ^4.6.0` dans pubspec.yaml ✅ FAIT
|
||
- [x] Ajouter `flutter_stripe: ^12.0.0` pour le SDK Stripe ✅ FAIT
|
||
- [x] Ajouter `device_info_plus: ^10.1.0` pour détecter le modèle d'iPhone ✅ FAIT
|
||
- [x] Ajouter `battery_plus: ^6.1.0` pour le niveau de batterie ✅ FAIT
|
||
- [x] Ajouter `network_info_plus: ^5.0.3` pour l'IP et WiFi ✅ FAIT
|
||
- [x] Ajouter `nfc_manager: ^3.5.0` pour la détection NFC ✅ FAIT
|
||
- [x] Connectivity déjà présent : `connectivity_plus: ^6.1.3` ✅ FAIT
|
||
- [x] Exécuter `flutter pub get` ✅ FAIT
|
||
- [ ] Exécuter `cd ios && pod install`
|
||
- [ ] Vérifier la compilation iOS sans erreurs
|
||
- [ ] Documenter les versions exactes installées
|
||
|
||
#### 🔧 1.2a Configuration iOS native (2h)
|
||
- [ ] Modifier `ios/Runner/Info.plist` avec les permissions NFC
|
||
- [ ] Ajouter `NSLocationWhenInUseUsageDescription` (requis par Stripe)
|
||
- [ ] Configurer les entitlements Tap to Pay Apple Developer
|
||
- [ ] Tester sur simulateur iOS
|
||
- [ ] Vérifier les permissions sur appareil physique
|
||
- [ ] Documenter les changements dans Info.plist
|
||
|
||
#### 🤖 1.2b Configuration Android native (2h)
|
||
- [ ] Modifier `android/app/src/main/AndroidManifest.xml` avec permissions NFC
|
||
- [ ] Ajouter `<uses-permission android:name="android.permission.NFC" />`
|
||
- [ ] Ajouter `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
|
||
- [ ] Ajouter `<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />`
|
||
- [ ] Ajouter `<uses-feature android:name="android.hardware.nfc" android:required="false" />`
|
||
- [ ] Vérifier/modifier `minSdkVersion 28` dans `android/app/build.gradle`
|
||
- [ ] Vérifier `targetSdkVersion 33` ou plus récent
|
||
- [ ] Tester sur appareil Android certifié Stripe
|
||
- [ ] Documenter les changements
|
||
|
||
#### 🏗️ 1.3 Architecture des services (4h)
|
||
- [ ] Créer `lib/core/services/stripe_tap_to_pay_service.dart`
|
||
- [ ] Implémenter le singleton StripeTapToPayService
|
||
- [ ] Créer la méthode `initialize()` avec gestion du token
|
||
- [ ] Créer la méthode `_fetchConnectionToken()` via API
|
||
- [ ] Implémenter la connexion au "lecteur" local (le téléphone)
|
||
- [ ] Créer `lib/core/repositories/payment_repository.dart`
|
||
- [ ] Implémenter les méthodes CRUD pour les paiements
|
||
- [ ] Intégrer avec le pattern Repository existant
|
||
- [ ] Ajouter les injections dans `app.dart`
|
||
|
||
---
|
||
|
||
### 🔍 PHASE 2 : VÉRIFICATION COMPATIBILITÉ
|
||
**Durée estimée : 1.5 jours**
|
||
**Objectif : Détecter et informer sur la compatibilité Tap to Pay**
|
||
|
||
#### 📱 2.1 Service de détection d'appareil (4h) ✅ COMPLÉTÉ
|
||
- [x] Créer `lib/core/services/device_info_service.dart` ✅ FAIT
|
||
- [x] Lister les modèles iPhone compatibles (XS, XR, 11, 12, 13, 14, 15, 16) ✅ FAIT
|
||
- [x] Vérifier la version iOS (≥ 16.4 pour Tap to Pay) ✅ FAIT - iOS 16.4 minimum confirmé par Stripe
|
||
- [x] Créer méthode `collectDeviceInfo()` et `canUseTapToPay()` ✅ FAIT avec batterie minimum 10%
|
||
- [x] Retourner les infos : model, osVersion, isCompatible, batteryLevel, IP ✅ FAIT
|
||
- [x] Gérer le cas Android (SDK ≥ 28 pour Tap to Pay) ✅ FAIT
|
||
- [x] Ajouter logs de debug pour diagnostic ✅ FAIT
|
||
- [x] Envoi automatique à l'API après login : POST `/users/device-info` ✅ FAIT dans ApiService
|
||
- [x] Sauvegarde dans Hive box settings ✅ FAIT avec préfixe `device_`
|
||
- [x] **NOUVEAU** : Vérification certification Stripe via API `/stripe/devices/check-tap-to-pay` ✅ FAIT
|
||
- [x] **NOUVEAU** : Méthode `checkStripeCertification()` pour Android ✅ FAIT
|
||
- [x] **NOUVEAU** : Stockage `device_stripe_certified` dans Hive ✅ FAIT
|
||
- [x] **NOUVEAU** : Messages d'erreur détaillés selon le problème (NFC, certification, batterie) ✅ FAIT
|
||
|
||
#### 🎨 2.2 Écran de vérification (4h)
|
||
- [ ] Créer `lib/presentation/payment/compatibility_check_page.dart`
|
||
- [ ] Design responsive avec icônes et messages clairs
|
||
- [ ] Afficher le modèle d'appareil détecté
|
||
- [ ] Afficher la version iOS
|
||
- [ ] Message explicatif si non compatible
|
||
- [ ] Bouton "Continuer" si compatible
|
||
- [ ] Bouton "Retour" si non compatible
|
||
- [ ] Intégrer avec la navigation existante
|
||
|
||
#### 🔄 2.3 Intégration dans le flux utilisateur (4h)
|
||
- [ ] Ajouter vérification au démarrage de l'app
|
||
- [ ] Sauvegarder le résultat dans SharedPreferences
|
||
- [ ] Afficher/masquer les fonctionnalités selon compatibilité
|
||
- [ ] Ajouter indicateur dans le dashboard utilisateur
|
||
- [ ] Gérer le cas de mise à jour iOS pendant utilisation
|
||
|
||
---
|
||
|
||
### 💳 PHASE 3 : INTERFACE DE PAIEMENT
|
||
**Durée estimée : 2 jours**
|
||
**Objectif : Créer les écrans de sélection et confirmation de paiement**
|
||
|
||
#### 🎯 3.1 Écran de sélection du montant (6h)
|
||
- [ ] Créer `lib/presentation/payment/payment_amount_page.dart`
|
||
- [ ] Design avec chips pour montants prédéfinis (10€, 20€, 30€, 50€)
|
||
- [ ] Champ de saisie pour montant personnalisé
|
||
- [ ] Validation min 1€, max 999€
|
||
- [ ] Afficher info amicale en header
|
||
- [ ] Calculer et afficher les frais Stripe (si applicable)
|
||
- [ ] Bouton "Continuer" avec montant sélectionné
|
||
- [ ] Animation de sélection des chips
|
||
- [ ] Responsive pour toutes tailles d'écran
|
||
|
||
#### 📝 3.2 Écran de détails du paiement (4h)
|
||
- [ ] Créer `lib/presentation/payment/payment_details_page.dart`
|
||
- [ ] Formulaire optionnel : nom, email, téléphone du donateur
|
||
- [ ] Checkbox pour reçu (email ou SMS)
|
||
- [ ] Résumé : montant, amicale, date
|
||
- [ ] Bouton "Payer avec carte sans contact"
|
||
- [ ] Possibilité d'ajouter une note/commentaire
|
||
- [ ] Sauvegarde locale des infos saisies
|
||
|
||
#### 🎨 3.3 Composants UI réutilisables (4h)
|
||
- [ ] Créer `lib/presentation/widgets/payment/amount_selector_widget.dart`
|
||
- [ ] Créer `lib/presentation/widgets/payment/payment_summary_card.dart`
|
||
- [ ] Créer `lib/presentation/widgets/payment/donor_info_form.dart`
|
||
- [ ] Styles cohérents avec le design existant
|
||
- [ ] Animations et feedback visuel
|
||
|
||
---
|
||
|
||
### 📲 PHASE 4 : FLUX TAP TO PAY
|
||
**Durée estimée : 3 jours**
|
||
**Objectif : Implémenter le processus de paiement sans contact**
|
||
|
||
#### 🎯 4.1 Écran Tap to Pay principal (8h)
|
||
- [ ] Créer `lib/presentation/payment/tap_to_pay_page.dart`
|
||
- [ ] Afficher montant en grand format
|
||
- [ ] Animation NFC (ondes pulsantes)
|
||
- [ ] Instructions "Approchez la carte du dos de l'iPhone"
|
||
- [ ] Gestion des états : attente, lecture, traitement, succès, échec
|
||
- [ ] Bouton annuler pendant l'attente
|
||
- [ ] Timeout après 60 secondes
|
||
- [ ] Son/vibration au succès
|
||
|
||
#### 🔄 4.2 Intégration Stripe Tap to Pay (6h)
|
||
- [ ] Initialiser le service Tap to Pay local (pas de découverte de lecteurs)
|
||
- [ ] Créer PaymentIntent via API backend
|
||
- [ ] Implémenter `collectPaymentMethod()` avec NFC du téléphone
|
||
- [ ] Implémenter `confirmPaymentIntent()`
|
||
- [ ] Gérer les erreurs Stripe spécifiques
|
||
- [ ] Logs détaillés pour debug
|
||
- [ ] Gestion des timeouts et retry
|
||
|
||
#### ✅ 4.3 Écran de confirmation (4h)
|
||
- [ ] Créer `lib/presentation/payment/payment_success_page.dart`
|
||
- [ ] Animation de succès (check vert)
|
||
- [ ] Afficher montant et référence de transaction
|
||
- [ ] Options : Envoyer reçu, Nouveau paiement, Retour
|
||
- [ ] Partage du reçu (share sheet iOS)
|
||
- [ ] Sauvegarde locale de la transaction
|
||
|
||
#### ❌ 4.4 Gestion des erreurs (4h)
|
||
- [ ] Créer `lib/presentation/payment/payment_error_page.dart`
|
||
- [ ] Messages d'erreur traduits en français
|
||
- [ ] Différencier : carte refusée, solde insuffisant, erreur réseau, etc.
|
||
- [ ] Bouton "Réessayer" avec même montant
|
||
- [ ] Bouton "Changer de montant"
|
||
- [ ] Logs pour support technique
|
||
|
||
---
|
||
|
||
### 📶 PHASE 5 : MODE OFFLINE ET SYNCHRONISATION
|
||
**Durée estimée : 2 jours**
|
||
**Objectif : Permettre les paiements sans connexion internet**
|
||
|
||
#### 💾 5.1 Service de queue offline (6h)
|
||
- [ ] Créer `lib/core/services/offline_payment_queue_service.dart`
|
||
- [ ] Stocker les paiements dans SharedPreferences
|
||
- [ ] Structure : amount, timestamp, amicale_id, user_id, status
|
||
- [ ] Méthode `addToQueue()` pour nouveaux paiements
|
||
- [ ] Méthode `getQueueSize()` pour badge notification
|
||
- [ ] Méthode `clearQueue()` après sync réussie
|
||
- [ ] Limite de 100 paiements en queue
|
||
- [ ] Expiration après 7 jours
|
||
|
||
#### 🔄 5.2 Service de synchronisation (6h)
|
||
- [ ] Créer `lib/core/services/payment_sync_service.dart`
|
||
- [ ] Détecter le retour de connexion avec ConnectivityPlus
|
||
- [ ] Envoyer les paiements par batch à l'API
|
||
- [ ] Gérer les échecs partiels
|
||
- [ ] Retry avec backoff exponentiel
|
||
- [ ] Notification de sync réussie
|
||
- [ ] Logs de synchronisation
|
||
|
||
#### 📊 5.3 UI du mode offline (4h)
|
||
- [ ] Indicateur "Mode hors ligne" dans l'app bar
|
||
- [ ] Badge avec nombre de paiements en attente
|
||
- [ ] Écran de détail de la queue
|
||
- [ ] Bouton "Forcer la synchronisation"
|
||
- [ ] Messages informatifs sur l'état
|
||
|
||
---
|
||
|
||
### 📈 PHASE 6 : DASHBOARD ET STATISTIQUES
|
||
**Durée estimée : 2 jours**
|
||
**Objectif : Tableau de bord pour suivre les ventes**
|
||
|
||
#### 📊 6.1 Dashboard vendeur (8h)
|
||
- [ ] Créer `lib/presentation/dashboard/vendor_dashboard_page.dart`
|
||
- [ ] Widget statistiques du jour (nombre, montant total)
|
||
- [ ] Widget statistiques de la semaine
|
||
- [ ] Widget statistiques du mois
|
||
- [ ] Graphique d'évolution (fl_chart)
|
||
- [ ] Liste des 10 dernières transactions
|
||
- [ ] Filtres par période
|
||
- [ ] Export CSV des données
|
||
|
||
#### 📱 6.2 Détail d'une transaction (4h)
|
||
- [ ] Créer `lib/presentation/payment/transaction_detail_page.dart`
|
||
- [ ] Afficher toutes les infos de la transaction
|
||
- [ ] Status : succès, en attente, échoué
|
||
- [ ] Option renvoyer le reçu
|
||
- [ ] Option annuler (si possible)
|
||
- [ ] Historique des actions
|
||
|
||
#### 🔔 6.3 Notifications et rappels (4h)
|
||
- [ ] Widget de rappel de synchronisation
|
||
- [ ] Notification de paiements en attente
|
||
- [ ] Alerte si compte Stripe a un problème
|
||
- [ ] Rappel de fin de journée pour sync
|
||
|
||
---
|
||
|
||
### 🧪 PHASE 7 : TESTS ET VALIDATION
|
||
**Durée estimée : 2 jours**
|
||
**Objectif : Assurer la qualité et la fiabilité**
|
||
|
||
#### ✅ 7.1 Tests unitaires (6h)
|
||
- [ ] Tests StripeTerminalService
|
||
- [ ] Tests DeviceCompatibilityService
|
||
- [ ] Tests OfflineQueueService
|
||
- [ ] Tests PaymentRepository
|
||
- [ ] Tests de validation des montants
|
||
- [ ] Tests de sérialisation/désérialisation
|
||
- [ ] Coverage > 80%
|
||
|
||
#### 📱 7.2 Tests d'intégration (6h)
|
||
- [ ] Test flux complet de paiement
|
||
- [ ] Test mode offline vers online
|
||
- [ ] Test gestion des erreurs
|
||
- [ ] Test sur différents iPhones
|
||
- [ ] Test avec cartes de test Stripe
|
||
- [ ] Test limites et edge cases
|
||
|
||
#### 🎭 7.3 Tests utilisateurs (4h)
|
||
- [ ] Créer scénarios de test
|
||
- [ ] Test avec 5 utilisateurs pilotes
|
||
- [ ] Collecter les retours
|
||
- [ ] Corriger les bugs identifiés
|
||
- [ ] Valider l'ergonomie
|
||
|
||
---
|
||
|
||
### 🚀 PHASE 8 : DÉPLOIEMENT ET DOCUMENTATION
|
||
**Durée estimée : 1 jour**
|
||
**Objectif : Mise en production et formation**
|
||
|
||
#### 📦 8.1 Build et déploiement (4h)
|
||
- [ ] Build iOS release
|
||
- [ ] Upload sur TestFlight
|
||
- [ ] Tests de non-régression
|
||
- [ ] Déploiement sur App Store
|
||
- [ ] Monitoring des premières 24h
|
||
|
||
#### 📚 8.2 Documentation (4h)
|
||
- [ ] Guide utilisateur pompier (PDF)
|
||
- [ ] Vidéo tutoriel Tap to Pay
|
||
- [ ] FAQ problèmes courants
|
||
- [ ] Documentation technique
|
||
- [ ] Formation équipe support
|
||
|
||
---
|
||
|
||
## 🔄 FLOW COMPLET DE PAIEMENT TAP TO PAY
|
||
|
||
### 📋 Vue d'ensemble du processus
|
||
|
||
Le flow de paiement se déroule en plusieurs étapes distinctes entre l'application Flutter, l'API PHP et Stripe :
|
||
|
||
```
|
||
App Flutter → API PHP → Stripe Terminal API → Retour App → NFC Payment → Confirmation
|
||
```
|
||
|
||
### 🎯 Étapes détaillées du flow
|
||
|
||
#### 1️⃣ **PRÉPARATION DU PAIEMENT (App Flutter)**
|
||
- L'utilisateur sélectionne ou crée un passage
|
||
- Choix du montant et sélection "Carte Bancaire"
|
||
- Récupération du `passage_id` existant ou 0 pour nouveau
|
||
|
||
#### 2️⃣ **CRÉATION DU PAYMENT INTENT (App → API → Stripe)**
|
||
|
||
**Requête App → API:**
|
||
```json
|
||
POST /api/stripe/payments/create-intent
|
||
{
|
||
"amount": 2000, // en centimes
|
||
"currency": "eur",
|
||
"payment_method_types": ["card_present"],
|
||
"passage_id": 123, // ou 0 si nouveau
|
||
"amicale_id": 45,
|
||
"member_id": 67,
|
||
"stripe_account": "acct_xxx",
|
||
"metadata": {
|
||
"passage_id": "123",
|
||
"type": "tap_to_pay"
|
||
}
|
||
}
|
||
```
|
||
|
||
**L'API fait alors :**
|
||
1. Validation des données reçues
|
||
2. Appel Stripe API pour créer le PaymentIntent
|
||
3. Stockage en base de données locale
|
||
4. Retour à l'app avec `payment_intent_id` et `client_secret`
|
||
|
||
**Réponse API → App:**
|
||
```json
|
||
{
|
||
"payment_intent_id": "pi_xxx",
|
||
"client_secret": "pi_xxx_secret_xxx",
|
||
"amount": 2000,
|
||
"status": "requires_payment_method"
|
||
}
|
||
```
|
||
|
||
#### 3️⃣ **COLLECTE DE LA CARTE (App avec SDK Stripe Terminal)**
|
||
|
||
L'application utilise le SDK natif pour :
|
||
1. Activer le NFC du téléphone
|
||
2. Afficher l'écran "Approchez la carte"
|
||
3. Lire les données de la carte sans contact
|
||
4. Traiter le paiement localement via le SDK
|
||
|
||
#### 4️⃣ **TRAITEMENT DU PAIEMENT (SDK → Stripe)**
|
||
|
||
Le SDK Stripe Terminal :
|
||
- Envoie les données cryptées de la carte à Stripe
|
||
- Traite l'autorisation bancaire
|
||
- Retourne le statut du paiement à l'app
|
||
|
||
#### 5️⃣ **CONFIRMATION ET SAUVEGARDE (App → API)**
|
||
|
||
**Si paiement réussi :**
|
||
```json
|
||
POST /api/stripe/payments/confirm
|
||
{
|
||
"payment_intent_id": "pi_xxx",
|
||
"status": "succeeded",
|
||
"amount": 2000
|
||
}
|
||
```
|
||
|
||
**Puis sauvegarde du passage :**
|
||
```json
|
||
POST /api/passages
|
||
{
|
||
"id": 123,
|
||
"fk_type": 1, // Effectué
|
||
"montant": "20.00",
|
||
"fk_type_reglement": 3, // CB
|
||
"stripe_payment_id": "pi_xxx",
|
||
...
|
||
}
|
||
```
|
||
|
||
### 📊 Différences Web vs Tap to Pay
|
||
|
||
| Aspect | Paiement Web | Tap to Pay |
|
||
|--------|-------------|------------|
|
||
| **payment_method_types** | ["card"] | ["card_present"] |
|
||
| **SDK utilisé** | Stripe.js | Stripe Terminal SDK |
|
||
| **Collecte carte** | Formulaire web | NFC téléphone |
|
||
| **Metadata** | type: "web" | type: "tap_to_pay" |
|
||
| **Environnement** | Navigateur | App native |
|
||
| **Prérequis** | Aucun | iPhone XS+ iOS 16.4+ |
|
||
|
||
### ⚡ Points clés du flow
|
||
|
||
1. **Passage ID** : Toujours inclus (existant ou 0)
|
||
2. **Double confirmation** : PaymentIntent ET Passage sauvegardé
|
||
3. **Metadata Stripe** : Permet la traçabilité bidirectionnelle
|
||
4. **Endpoint unifié** : `/api/stripe/payments/` pour tous types
|
||
5. **Gestion erreurs** : À chaque étape du processus
|
||
|
||
## 🔄 PHASE 9 : ÉVOLUTIONS FUTURES (V2.2+)
|
||
|
||
### 📱 Support Android (V2.2)
|
||
- [ ] Vérification appareils Android certifiés via API
|
||
- [ ] Intégration SDK Android Tap to Pay
|
||
- [ ] Tests sur appareils Android certifiés
|
||
|
||
### 🌍 Fonctionnalités avancées (V2.3)
|
||
- [ ] Multi-devises
|
||
- [ ] Paiements récurrents (abonnements)
|
||
- [ ] Programme de fidélité
|
||
- [ ] Intégration comptable
|
||
- [ ] Rapports fiscaux automatiques
|
||
|
||
---
|
||
|
||
## 📊 MÉTRIQUES DE SUCCÈS
|
||
|
||
### KPIs Techniques
|
||
- [ ] Taux de succès des paiements > 95%
|
||
- [ ] Temps moyen de transaction < 15 secondes
|
||
- [ ] Synchronisation offline réussie > 99%
|
||
- [ ] Crash rate < 0.1%
|
||
|
||
### KPIs Business
|
||
- [ ] Adoption par > 50% des membres en 3 mois
|
||
- [ ] Augmentation des dons de 30%
|
||
- [ ] Satisfaction utilisateur > 4.5/5
|
||
- [ ] Réduction des paiements espèces de 60%
|
||
|
||
---
|
||
|
||
## ⚠️ RISQUES ET MITIGATION
|
||
|
||
### Risques Techniques
|
||
| Risque | Impact | Probabilité | Mitigation |
|
||
|--------|--------|-------------|------------|
|
||
| Incompatibilité iOS | Élevé | Moyen | Détection précoce, messages clairs |
|
||
| Problèmes réseau | Moyen | Élevé | Mode offline robuste |
|
||
| Erreurs Stripe | Élevé | Faible | Retry logic, logs détaillés |
|
||
| Performance | Moyen | Moyen | Optimisation, cache |
|
||
|
||
### Risques Business
|
||
| Risque | Impact | Probabilité | Mitigation |
|
||
|--------|--------|-------------|------------|
|
||
| Résistance au changement | Élevé | Moyen | Formation, support, incentives |
|
||
| Conformité RGPD | Élevé | Faible | Audit, documentation |
|
||
| Coûts Stripe | Moyen | Certain | Communication transparente |
|
||
|
||
---
|
||
|
||
## 📅 HISTORIQUE V1 - STRIPE CONNECT (COMPLÉTÉE)
|
||
|
||
### ✅ Fonctionnalités V1 Réalisées (01/09/2024)
|
||
|
||
#### Configuration Stripe Connect
|
||
- ✅ Widget `amicale_form.dart` avec intégration Stripe
|
||
- ✅ Service `stripe_connect_service.dart` complet
|
||
- ✅ Création de comptes Stripe Express
|
||
- ✅ Génération de liens d'onboarding
|
||
- ✅ Vérification du statut en temps réel
|
||
- ✅ Messages utilisateur en français
|
||
- ✅ Interface responsive mobile/desktop
|
||
|
||
#### API Endpoints Intégrés
|
||
- ✅ `/amicales/{id}/stripe/create-account` - Création compte
|
||
- ✅ `/amicales/{id}/stripe/account-status` - Vérification statut
|
||
- ✅ `/amicales/{id}/stripe/onboarding-link` - Lien configuration
|
||
- ✅ `/amicales/{id}/stripe/create-location` - Location Terminal
|
||
|
||
#### Statuts et Messages
|
||
- ✅ "💳 Activez les paiements par carte bancaire"
|
||
- ✅ "⏳ Configuration Stripe en cours"
|
||
- ✅ "✅ Compte Stripe configuré - 100% des paiements"
|
||
|
||
---
|
||
|
||
## 📝 NOTES DE DÉVELOPPEMENT
|
||
|
||
### Points d'attention pour la V2
|
||
1. **Dépendance V1** : L'amicale doit avoir complété l'onboarding Stripe (V1) avant de pouvoir utiliser Tap to Pay (V2)
|
||
2. **Architecture existante** : Utiliser le pattern Repository et les services singleton déjà en place
|
||
3. **Gestion d'erreurs** : Utiliser `ApiException` pour tous les messages d'erreur
|
||
4. **Réactivité** : Utiliser `ValueListenableBuilder` avec les Box Hive
|
||
5. **Multi-environnement** : L'ApiService détecte automatiquement DEV/REC/PROD
|
||
|
||
### Conventions de code
|
||
- Noms de fichiers en snake_case
|
||
- Classes en PascalCase
|
||
- Variables et méthodes en camelCase
|
||
- Pas de Provider/Bloc, utiliser l'injection directe
|
||
- Tests unitaires obligatoires pour chaque service
|
||
|
||
### 🎯 Scope Stripe - Exclusivement logiciel
|
||
- **TAP TO PAY UNIQUEMENT** : Utilisation du NFC intégré du téléphone
|
||
- **PAS de terminaux physiques** : Pas de Bluetooth, USB ou Lightning
|
||
- **PAS de lecteurs externes** : Pas de WisePad, Reader M2, etc.
|
||
- **Futur** : Paiements Web via Stripe.js
|
||
|
||
### Ressources utiles
|
||
- [Documentation Stripe Terminal Flutter](https://stripe.com/docs/terminal/payments/setup-flutter)
|
||
- [Apple Tap to Pay Requirements](https://developer.apple.com/tap-to-pay/)
|
||
- [Flutter Hive Documentation](https://docs.hivedb.dev/)
|
||
|
||
---
|
||
|
||
## 🔄 DERNIÈRES MISES À JOUR
|
||
|
||
- **29/09/2025** : Clarification du scope et mise à jour complète
|
||
- ✅ Scope : TAP TO PAY UNIQUEMENT (pas de terminaux physiques)
|
||
- ✅ Suppression références Bluetooth et lecteurs externes
|
||
- ✅ Réduction estimation : 1.5 semaine au lieu de 2-3 semaines
|
||
- ✅ DeviceInfoService avec vérification API pour Android
|
||
- ✅ Intégration endpoints `/stripe/devices/check-tap-to-pay`
|
||
- ✅ Gestion batterie minimum 10%
|
||
- ✅ Messages d'erreur détaillés selon le problème
|
||
- ✅ Correction bug Tap to Pay sur web mobile
|
||
- ✅ SDK Stripe Terminal 4.6.0 (compatible avec requirements)
|
||
- **28/09/2025** : Création du planning détaillé V2 avec 9 phases et 200+ TODO
|
||
- **01/09/2024** : V1 Stripe Connect complétée et opérationnelle
|
||
- **25/08/2024** : Début du développement V1
|
||
|
||
---
|
||
|
||
## 📞 CONTACTS PROJET
|
||
|
||
- **Product Owner** : À définir
|
||
- **Tech Lead Flutter** : À définir
|
||
- **Support Stripe** : support@stripe.com
|
||
- **Équipe Backend PHP** : À coordonner pour les endpoints API
|
||
|
||
---
|
||
|
||
*Document de planification V2 - Terminal Payments*
|
||
*Dernière révision : 28/09/2025*
|
||
connectivity_plus: ^5.0.2 # Connectivité réseau
|
||
```
|
||
|
||
**STATUS**: Configuration Stripe Connect intégrée dans l'interface existante
|
||
|
||
```bash
|
||
cd app
|
||
flutter pub get
|
||
cd ios
|
||
pod install
|
||
```
|
||
|
||
#### ✅ Configuration iOS
|
||
```xml
|
||
<!-- ios/Runner/Info.plist -->
|
||
<key>NSLocationWhenInUseUsageDescription</key>
|
||
<string>Localisation nécessaire pour les paiements Stripe</string>
|
||
<!-- Pas de permissions Bluetooth requises pour Tap to Pay -->
|
||
<!-- Le NFC est géré nativement par le SDK Stripe -->
|
||
```
|
||
|
||
#### ✅ Configuration Android
|
||
```xml
|
||
<!-- android/app/src/main/AndroidManifest.xml -->
|
||
<uses-permission android:name="android.permission.NFC" />
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||
|
||
<!-- Déclaration de la fonctionnalité NFC (optionnelle pour ne pas exclure les appareils sans NFC) -->
|
||
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
||
```
|
||
|
||
```gradle
|
||
// android/app/build.gradle
|
||
android {
|
||
defaultConfig {
|
||
minSdkVersion 28 // Minimum requis pour Tap to Pay Android
|
||
targetSdkVersion 33 // Ou plus récent
|
||
compileSdkVersion 33
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Service de base StripeTerminalService
|
||
```dart
|
||
// lib/services/stripe_terminal_service.dart
|
||
import 'package:stripe_terminal/stripe_terminal.dart';
|
||
|
||
class StripeTerminalService {
|
||
static final StripeTerminalService _instance = StripeTerminalService._internal();
|
||
factory StripeTerminalService() => _instance;
|
||
StripeTerminalService._internal();
|
||
|
||
Terminal? _terminal;
|
||
bool _isInitialized = false;
|
||
|
||
Future<void> initialize() async {
|
||
if (_isInitialized) return;
|
||
|
||
_terminal = await Terminal.getInstance(
|
||
fetchConnectionToken: _fetchConnectionToken,
|
||
);
|
||
|
||
_isInitialized = true;
|
||
}
|
||
|
||
Future<String> _fetchConnectionToken() async {
|
||
final response = await ApiService().post('/terminal/connection-token');
|
||
return response['secret'];
|
||
}
|
||
|
||
Future<bool> checkTapToPayCapability() async {
|
||
if (!Platform.isIOS) return false;
|
||
|
||
final deviceInfo = DeviceInfoPlugin();
|
||
final iosInfo = await deviceInfo.iosInfo;
|
||
|
||
// iPhone XS et ultérieurs
|
||
final supportedModels = [
|
||
'iPhone11,', // XS, XS Max
|
||
'iPhone12,', // 11, 11 Pro
|
||
'iPhone13,', // 12 series
|
||
'iPhone14,', // 13 series
|
||
'iPhone15,', // 14 series
|
||
'iPhone16,', // 15 series
|
||
];
|
||
|
||
final modelIdentifier = iosInfo.utsname.machine;
|
||
final isSupported = supportedModels.any((model) =>
|
||
modelIdentifier.startsWith(model)
|
||
);
|
||
|
||
// iOS 16.4 minimum
|
||
final osVersion = iosInfo.systemVersion.split('.').map(int.parse).toList();
|
||
final isOSSupported = osVersion[0] > 16 ||
|
||
(osVersion[0] == 16 && osVersion.length > 1 && osVersion[1] >= 4);
|
||
|
||
return isSupported && isOSSupported;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 MARDI 26/08 - UI Paiement principal (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Écran de vérification compatibilité
|
||
```dart
|
||
// lib/screens/payment/compatibility_check_screen.dart
|
||
class CompatibilityCheckScreen extends StatefulWidget {
|
||
@override
|
||
_CompatibilityCheckScreenState createState() => _CompatibilityCheckScreenState();
|
||
}
|
||
|
||
class _CompatibilityCheckScreenState extends State<CompatibilityCheckScreen> {
|
||
bool? _isCompatible;
|
||
String _deviceInfo = '';
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_checkCompatibility();
|
||
}
|
||
|
||
Future<void> _checkCompatibility() async {
|
||
final service = StripeTerminalService();
|
||
final compatible = await service.checkTapToPayCapability();
|
||
|
||
final deviceInfo = DeviceInfoPlugin();
|
||
final iosInfo = await deviceInfo.iosInfo;
|
||
|
||
setState(() {
|
||
_isCompatible = compatible;
|
||
_deviceInfo = '${iosInfo.name} - iOS ${iosInfo.systemVersion}';
|
||
});
|
||
|
||
if (compatible) {
|
||
await service.initialize();
|
||
Navigator.pushReplacement(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => PaymentScreen())
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_isCompatible == null) {
|
||
return Scaffold(
|
||
body: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
CircularProgressIndicator(),
|
||
SizedBox(height: 20),
|
||
Text('Vérification de compatibilité...'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
if (!_isCompatible!) {
|
||
return Scaffold(
|
||
body: Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.error_outline, size: 80, color: Colors.orange),
|
||
SizedBox(height: 20),
|
||
Text(
|
||
'Tap to Pay non disponible',
|
||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||
),
|
||
SizedBox(height: 10),
|
||
Text(
|
||
'Votre appareil: $_deviceInfo',
|
||
style: TextStyle(color: Colors.grey),
|
||
),
|
||
SizedBox(height: 20),
|
||
Text(
|
||
'Tap to Pay nécessite un iPhone XS ou plus récent avec iOS 16.4+',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 16),
|
||
),
|
||
SizedBox(height: 30),
|
||
ElevatedButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('Retour'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(); // Ne devrait jamais arriver ici
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Écran de paiement principal
|
||
```dart
|
||
// lib/screens/payment/payment_screen.dart
|
||
class PaymentScreen extends StatefulWidget {
|
||
final Amicale amicale;
|
||
|
||
PaymentScreen({required this.amicale});
|
||
|
||
@override
|
||
_PaymentScreenState createState() => _PaymentScreenState();
|
||
}
|
||
|
||
class _PaymentScreenState extends State<PaymentScreen> {
|
||
final _amounts = [10.0, 20.0, 30.0, 50.0]; // en euros
|
||
double _selectedAmount = 20.0;
|
||
bool _isProcessing = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text('Paiement Calendrier'),
|
||
backgroundColor: Colors.red.shade700,
|
||
),
|
||
body: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// Info amicale
|
||
Card(
|
||
child: ListTile(
|
||
leading: Icon(Icons.group, color: Colors.red),
|
||
title: Text(widget.amicale.name),
|
||
subtitle: Text('Calendrier ${DateTime.now().year}'),
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
// Sélection montant
|
||
Text(
|
||
'Sélectionnez le montant',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
SizedBox(height: 15),
|
||
|
||
Wrap(
|
||
spacing: 10,
|
||
runSpacing: 10,
|
||
children: _amounts.map((amount) {
|
||
final isSelected = _selectedAmount == amount;
|
||
return ChoiceChip(
|
||
label: Text(
|
||
'${amount.toStringAsFixed(0)}€',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: isSelected ? Colors.white : Colors.black,
|
||
),
|
||
),
|
||
selected: isSelected,
|
||
selectedColor: Colors.red.shade700,
|
||
onSelected: (selected) {
|
||
if (selected && !_isProcessing) {
|
||
setState(() => _selectedAmount = amount);
|
||
}
|
||
},
|
||
);
|
||
}).toList(),
|
||
),
|
||
|
||
// Montant personnalisé
|
||
SizedBox(height: 20),
|
||
TextField(
|
||
keyboardType: TextInputType.number,
|
||
decoration: InputDecoration(
|
||
labelText: 'Ou entrez un montant personnalisé',
|
||
prefixText: '€ ',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
onChanged: (value) {
|
||
final amount = double.tryParse(value);
|
||
if (amount != null && amount >= 1) {
|
||
setState(() => _selectedAmount = amount);
|
||
}
|
||
},
|
||
),
|
||
|
||
Spacer(),
|
||
|
||
// Bouton paiement
|
||
ElevatedButton(
|
||
onPressed: _isProcessing ? null : _startPayment,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red.shade700,
|
||
padding: EdgeInsets.symmetric(vertical: 20),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
),
|
||
child: _isProcessing
|
||
? Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
SizedBox(
|
||
width: 20,
|
||
height: 20,
|
||
child: CircularProgressIndicator(
|
||
color: Colors.white,
|
||
strokeWidth: 2,
|
||
),
|
||
),
|
||
SizedBox(width: 10),
|
||
Text('Traitement...', style: TextStyle(fontSize: 18)),
|
||
],
|
||
)
|
||
: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.contactless, size: 30),
|
||
SizedBox(width: 10),
|
||
Text(
|
||
'Payer ${_selectedAmount.toStringAsFixed(2)}€',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 10),
|
||
|
||
Text(
|
||
'Paiement sécurisé par Stripe',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _startPayment() async {
|
||
setState(() => _isProcessing = true);
|
||
|
||
try {
|
||
// Naviguer vers l'écran Tap to Pay
|
||
final result = await Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (_) => TapToPayScreen(
|
||
amount: _selectedAmount,
|
||
amicale: widget.amicale,
|
||
),
|
||
),
|
||
);
|
||
|
||
if (result != null && result['success']) {
|
||
_showSuccessDialog(result['paymentIntentId']);
|
||
}
|
||
} catch (e) {
|
||
_showErrorDialog(e.toString());
|
||
} finally {
|
||
setState(() => _isProcessing = false);
|
||
}
|
||
}
|
||
|
||
void _showSuccessDialog(String paymentIntentId) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) => AlertDialog(
|
||
title: Row(
|
||
children: [
|
||
Icon(Icons.check_circle, color: Colors.green, size: 30),
|
||
SizedBox(width: 10),
|
||
Text('Paiement réussi !'),
|
||
],
|
||
),
|
||
content: Text('Le paiement a été effectué avec succès.'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
Navigator.of(context).pop(); // Retour écran principal
|
||
},
|
||
child: Text('OK'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showErrorDialog(String error) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: Text('Erreur de paiement'),
|
||
content: Text(error),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: Text('Réessayer'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 MERCREDI 27/08 - Flux Tap to Pay (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Écran Tap to Pay
|
||
```dart
|
||
// lib/screens/payment/tap_to_pay_screen.dart
|
||
class TapToPayScreen extends StatefulWidget {
|
||
final double amount;
|
||
final Amicale amicale;
|
||
|
||
TapToPayScreen({required this.amount, required this.amicale});
|
||
|
||
@override
|
||
_TapToPayScreenState createState() => _TapToPayScreenState();
|
||
}
|
||
|
||
class _TapToPayScreenState extends State<TapToPayScreen> {
|
||
final _terminalService = StripeTerminalService();
|
||
PaymentIntent? _paymentIntent;
|
||
String _status = 'Initialisation...';
|
||
bool _isProcessing = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initializePayment();
|
||
}
|
||
|
||
Future<void> _initializePayment() async {
|
||
try {
|
||
setState(() => _status = 'Création du paiement...');
|
||
|
||
// 1. Créer PaymentIntent via API
|
||
final response = await ApiService().post('/payments/create-intent', {
|
||
'amount': (widget.amount * 100).round(), // en centimes
|
||
'amicale_id': widget.amicale.id,
|
||
});
|
||
|
||
setState(() => _status = 'Connexion au lecteur...');
|
||
|
||
// 2. Initialiser le lecteur Tap to Pay local (le téléphone)
|
||
await _terminalService.initializeLocalReader();
|
||
// Pas de découverte de lecteurs externes - le téléphone EST le lecteur
|
||
|
||
setState(() => _status = 'Prêt pour le paiement');
|
||
|
||
// 3. Collecter le paiement
|
||
_paymentIntent = await _terminalService.collectPaymentMethod(
|
||
response['client_secret'],
|
||
);
|
||
|
||
setState(() => _status = 'Traitement du paiement...');
|
||
|
||
// 4. Confirmer le paiement
|
||
final confirmedIntent = await _terminalService.confirmPaymentIntent(
|
||
_paymentIntent!,
|
||
);
|
||
|
||
if (confirmedIntent.status == PaymentIntentStatus.succeeded) {
|
||
_onPaymentSuccess(confirmedIntent.id);
|
||
} else {
|
||
throw Exception('Paiement non confirmé');
|
||
}
|
||
|
||
} catch (e) {
|
||
_onPaymentError(e.toString());
|
||
}
|
||
}
|
||
|
||
void _onPaymentSuccess(String paymentIntentId) {
|
||
Navigator.pop(context, {
|
||
'success': true,
|
||
'paymentIntentId': paymentIntentId,
|
||
});
|
||
}
|
||
|
||
void _onPaymentError(String error) {
|
||
setState(() {
|
||
_isProcessing = false;
|
||
_status = 'Erreur: $error';
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.grey.shade100,
|
||
body: SafeArea(
|
||
child: Column(
|
||
children: [
|
||
// Header
|
||
Container(
|
||
padding: EdgeInsets.all(20),
|
||
color: Colors.white,
|
||
child: Row(
|
||
children: [
|
||
IconButton(
|
||
icon: Icon(Icons.close),
|
||
onPressed: _isProcessing ? null : () => Navigator.pop(context),
|
||
),
|
||
Expanded(
|
||
child: Text(
|
||
'Paiement sans contact',
|
||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
SizedBox(width: 48), // Pour équilibrer avec IconButton
|
||
],
|
||
),
|
||
),
|
||
|
||
// Montant
|
||
Container(
|
||
margin: EdgeInsets.all(20),
|
||
padding: EdgeInsets.all(30),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(15),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.grey.shade300,
|
||
blurRadius: 10,
|
||
offset: Offset(0, 5),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
'${widget.amount.toStringAsFixed(2)} €',
|
||
style: TextStyle(
|
||
fontSize: 48,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.red.shade700,
|
||
),
|
||
),
|
||
SizedBox(height: 10),
|
||
Text(
|
||
widget.amicale.name,
|
||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Animation Tap to Pay
|
||
Expanded(
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (_status.contains('Prêt'))
|
||
TapToPayAnimation()
|
||
else if (_isProcessing)
|
||
CircularProgressIndicator(
|
||
size: 80,
|
||
strokeWidth: 8,
|
||
valueColor: AlwaysStoppedAnimation(Colors.red.shade700),
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
Text(
|
||
_status,
|
||
style: TextStyle(fontSize: 18),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
|
||
if (_status.contains('Prêt'))
|
||
Padding(
|
||
padding: EdgeInsets.only(top: 20),
|
||
child: Text(
|
||
'Approchez la carte du dos de l\'iPhone',
|
||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// Footer
|
||
if (!_isProcessing && _status.contains('Erreur'))
|
||
Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: ElevatedButton(
|
||
onPressed: _initializePayment,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red.shade700,
|
||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
|
||
),
|
||
child: Text('Réessayer', style: TextStyle(fontSize: 16)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Animation Tap to Pay
|
||
```dart
|
||
// lib/widgets/tap_to_pay_animation.dart
|
||
class TapToPayAnimation extends StatefulWidget {
|
||
@override
|
||
_TapToPayAnimationState createState() => _TapToPayAnimationState();
|
||
}
|
||
|
||
class _TapToPayAnimationState extends State<TapToPayAnimation>
|
||
with SingleTickerProviderStateMixin {
|
||
late AnimationController _controller;
|
||
late Animation<double> _scaleAnimation;
|
||
late Animation<double> _opacityAnimation;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = AnimationController(
|
||
duration: Duration(seconds: 2),
|
||
vsync: this,
|
||
)..repeat();
|
||
|
||
_scaleAnimation = Tween<double>(
|
||
begin: 0.8,
|
||
end: 1.5,
|
||
).animate(CurvedAnimation(
|
||
parent: _controller,
|
||
curve: Curves.easeOut,
|
||
));
|
||
|
||
_opacityAnimation = Tween<double>(
|
||
begin: 1.0,
|
||
end: 0.0,
|
||
).animate(CurvedAnimation(
|
||
parent: _controller,
|
||
curve: Curves.easeOut,
|
||
));
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: 200,
|
||
height: 200,
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
// Ondes animées
|
||
AnimatedBuilder(
|
||
animation: _controller,
|
||
builder: (context, child) {
|
||
return Transform.scale(
|
||
scale: _scaleAnimation.value,
|
||
child: Opacity(
|
||
opacity: _opacityAnimation.value,
|
||
child: Container(
|
||
width: 150,
|
||
height: 150,
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
border: Border.all(
|
||
color: Colors.red.shade700,
|
||
width: 3,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
|
||
// Icône centrale
|
||
Container(
|
||
width: 100,
|
||
height: 100,
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.shade700,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
Icons.contactless,
|
||
size: 60,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 JEUDI 28/08 - Gestion offline et sync (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Service de queue offline
|
||
```dart
|
||
// lib/services/offline_queue_service.dart
|
||
class OfflineQueueService {
|
||
static const String _queueKey = 'offline_payment_queue';
|
||
|
||
Future<void> addToQueue(PaymentData payment) async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final queueJson = prefs.getString(_queueKey) ?? '[]';
|
||
final queue = List<Map<String, dynamic>>.from(json.decode(queueJson));
|
||
|
||
queue.add({
|
||
'local_id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||
'amount': payment.amount,
|
||
'amicale_id': payment.amicaleId,
|
||
'pompier_id': payment.pompierId,
|
||
'created_at': DateTime.now().toIso8601String(),
|
||
'payment_method': 'card',
|
||
'status': 'pending_sync',
|
||
});
|
||
|
||
await prefs.setString(_queueKey, json.encode(queue));
|
||
}
|
||
|
||
Future<List<Map<String, dynamic>>> getQueue() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final queueJson = prefs.getString(_queueKey) ?? '[]';
|
||
return List<Map<String, dynamic>>.from(json.decode(queueJson));
|
||
}
|
||
|
||
Future<void> syncQueue() async {
|
||
final queue = await getQueue();
|
||
if (queue.isEmpty) return;
|
||
|
||
final connectivity = await Connectivity().checkConnectivity();
|
||
if (connectivity == ConnectivityResult.none) return;
|
||
|
||
try {
|
||
final response = await ApiService().post('/payments/batch-sync', {
|
||
'transactions': queue,
|
||
});
|
||
|
||
// Clear queue after successful sync
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString(_queueKey, '[]');
|
||
|
||
} catch (e) {
|
||
print('Sync failed: $e');
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Moniteur de connectivité
|
||
```dart
|
||
// lib/services/connectivity_monitor.dart
|
||
class ConnectivityMonitor {
|
||
final _connectivity = Connectivity();
|
||
StreamSubscription<ConnectivityResult>? _subscription;
|
||
final _offlineQueue = OfflineQueueService();
|
||
|
||
void startMonitoring() {
|
||
_subscription = _connectivity.onConnectivityChanged.listen((result) {
|
||
if (result != ConnectivityResult.none) {
|
||
// Back online, try to sync
|
||
_offlineQueue.syncQueue();
|
||
}
|
||
});
|
||
}
|
||
|
||
void stopMonitoring() {
|
||
_subscription?.cancel();
|
||
}
|
||
|
||
Future<bool> isOnline() async {
|
||
final result = await _connectivity.checkConnectivity();
|
||
return result != ConnectivityResult.none;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 VENDREDI 29/08 - Écran récap et reçu (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Écran de récapitulatif post-paiement
|
||
```dart
|
||
// lib/screens/payment/payment_receipt_screen.dart
|
||
class PaymentReceiptScreen extends StatelessWidget {
|
||
final String paymentIntentId;
|
||
final double amount;
|
||
final Amicale amicale;
|
||
|
||
PaymentReceiptScreen({
|
||
required this.paymentIntentId,
|
||
required this.amount,
|
||
required this.amicale,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.green.shade50,
|
||
appBar: AppBar(
|
||
title: Text('Paiement confirmé'),
|
||
backgroundColor: Colors.green.shade700,
|
||
automaticallyImplyLeading: false,
|
||
),
|
||
body: SafeArea(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: Column(
|
||
children: [
|
||
// Success icon
|
||
Container(
|
||
padding: EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
Icons.check_circle,
|
||
size: 80,
|
||
color: Colors.green.shade700,
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
Text(
|
||
'Merci pour votre soutien !',
|
||
style: TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 20),
|
||
|
||
// Détails de la transaction
|
||
Container(
|
||
padding: EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(15),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.grey.shade200,
|
||
blurRadius: 10,
|
||
offset: Offset(0, 5),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildReceiptRow('Montant', '${amount.toStringAsFixed(2)} €'),
|
||
Divider(),
|
||
_buildReceiptRow('Amicale', amicale.name),
|
||
Divider(),
|
||
_buildReceiptRow('Date', DateFormat('dd/MM/yyyy HH:mm').format(DateTime.now())),
|
||
Divider(),
|
||
_buildReceiptRow('Référence', paymentIntentId.substring(0, 10).toUpperCase()),
|
||
],
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
// Options de reçu
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
children: [
|
||
OutlinedButton.icon(
|
||
onPressed: () => _sendReceiptByEmail(context),
|
||
icon: Icon(Icons.email),
|
||
label: Text('Email'),
|
||
style: OutlinedButton.styleFrom(
|
||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||
),
|
||
),
|
||
OutlinedButton.icon(
|
||
onPressed: () => _sendReceiptBySMS(context),
|
||
icon: Icon(Icons.sms),
|
||
label: Text('SMS'),
|
||
style: OutlinedButton.styleFrom(
|
||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
Spacer(),
|
||
|
||
// Bouton terminer
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.green.shade700,
|
||
padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
),
|
||
child: Text(
|
||
'Terminer',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildReceiptRow(String label, String value) {
|
||
return Padding(
|
||
padding: EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(label, style: TextStyle(color: Colors.grey)),
|
||
Text(value, style: TextStyle(fontWeight: FontWeight.bold)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _sendReceiptByEmail(BuildContext context) async {
|
||
// Implémenter l'envoi par email via API
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Reçu envoyé par email')),
|
||
);
|
||
}
|
||
|
||
Future<void> _sendReceiptBySMS(BuildContext context) async {
|
||
// Implémenter l'envoi par SMS via API
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Reçu envoyé par SMS')),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Dashboard pompier
|
||
```dart
|
||
// lib/screens/dashboard/pompier_dashboard_screen.dart
|
||
class PompierDashboardScreen extends StatefulWidget {
|
||
@override
|
||
_PompierDashboardScreenState createState() => _PompierDashboardScreenState();
|
||
}
|
||
|
||
class _PompierDashboardScreenState extends State<PompierDashboardScreen> {
|
||
Map<String, dynamic>? _stats;
|
||
bool _isLoading = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadStats();
|
||
}
|
||
|
||
Future<void> _loadStats() async {
|
||
try {
|
||
final response = await ApiService().get('/pompiers/me/stats');
|
||
setState(() {
|
||
_stats = response;
|
||
_isLoading = false;
|
||
});
|
||
} catch (e) {
|
||
setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text('Mes ventes'),
|
||
backgroundColor: Colors.red.shade700,
|
||
),
|
||
body: _isLoading
|
||
? Center(child: CircularProgressIndicator())
|
||
: RefreshIndicator(
|
||
onRefresh: _loadStats,
|
||
child: ListView(
|
||
padding: EdgeInsets.all(20),
|
||
children: [
|
||
// Stats du jour
|
||
_buildStatCard(
|
||
'Aujourd\'hui',
|
||
_stats?['today_count'] ?? 0,
|
||
_stats?['today_amount'] ?? 0,
|
||
Colors.blue,
|
||
),
|
||
|
||
SizedBox(height: 15),
|
||
|
||
// Stats de la semaine
|
||
_buildStatCard(
|
||
'Cette semaine',
|
||
_stats?['week_count'] ?? 0,
|
||
_stats?['week_amount'] ?? 0,
|
||
Colors.orange,
|
||
),
|
||
|
||
SizedBox(height: 15),
|
||
|
||
// Stats totales
|
||
_buildStatCard(
|
||
'Total',
|
||
_stats?['total_count'] ?? 0,
|
||
_stats?['total_amount'] ?? 0,
|
||
Colors.green,
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
// Dernières transactions
|
||
Text(
|
||
'Dernières ventes',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
|
||
SizedBox(height: 10),
|
||
|
||
...(_stats?['recent_transactions'] ?? []).map((transaction) {
|
||
return Card(
|
||
child: ListTile(
|
||
leading: Icon(Icons.check_circle, color: Colors.green),
|
||
title: Text('${(transaction['amount'] / 100).toStringAsFixed(2)} €'),
|
||
subtitle: Text(DateFormat('dd/MM HH:mm').format(
|
||
DateTime.parse(transaction['created_at'])
|
||
)),
|
||
trailing: Icon(Icons.arrow_forward_ios, size: 16),
|
||
),
|
||
);
|
||
}).toList(),
|
||
],
|
||
),
|
||
),
|
||
floatingActionButton: FloatingActionButton.extended(
|
||
onPressed: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => CompatibilityCheckScreen()),
|
||
);
|
||
},
|
||
backgroundColor: Colors.red.shade700,
|
||
icon: Icon(Icons.contactless),
|
||
label: Text('Nouveau paiement'),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStatCard(String title, int count, int amount, Color color) {
|
||
return Container(
|
||
padding: EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [color.withOpacity(0.8), color],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(15),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: color.withOpacity(0.3),
|
||
blurRadius: 10,
|
||
offset: Offset(0, 5),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||
),
|
||
SizedBox(height: 5),
|
||
Text(
|
||
'$count ventes',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
Text(
|
||
'${(amount / 100).toStringAsFixed(2)} €',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 28,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 LUNDI 01/09 - Intégration avec login (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Vérification statut Stripe au login
|
||
```dart
|
||
// lib/screens/auth/login_screen.dart
|
||
class LoginScreen extends StatefulWidget {
|
||
@override
|
||
_LoginScreenState createState() => _LoginScreenState();
|
||
}
|
||
|
||
class _LoginScreenState extends State<LoginScreen> {
|
||
final _emailController = TextEditingController();
|
||
final _passwordController = TextEditingController();
|
||
bool _isLoading = false;
|
||
|
||
Future<void> _login() async {
|
||
setState(() => _isLoading = true);
|
||
|
||
try {
|
||
// 1. Authentification
|
||
final authResponse = await ApiService().post('/auth/login', {
|
||
'email': _emailController.text,
|
||
'password': _passwordController.text,
|
||
});
|
||
|
||
// 2. Stocker token
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString('auth_token', authResponse['token']);
|
||
await prefs.setString('user_id', authResponse['user']['id'].toString());
|
||
|
||
// 3. Vérifier statut Stripe de l'amicale
|
||
final amicaleResponse = await ApiService().get(
|
||
'/amicales/${authResponse['user']['amicale_id']}/stripe-status'
|
||
);
|
||
|
||
if (!amicaleResponse['charges_enabled']) {
|
||
// Amicale pas encore configurée pour Stripe
|
||
Navigator.pushReplacement(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => StripeSetupPendingScreen()),
|
||
);
|
||
} else {
|
||
// Tout est OK, aller au dashboard
|
||
Navigator.pushReplacement(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => PompierDashboardScreen()),
|
||
);
|
||
}
|
||
|
||
} catch (e) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Erreur de connexion: ${e.toString()}')),
|
||
);
|
||
} finally {
|
||
setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: SafeArea(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(20),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
// Logo
|
||
Image.asset('assets/logo_pompiers.png', height: 100),
|
||
|
||
SizedBox(height: 40),
|
||
|
||
Text(
|
||
'Calendriers des Pompiers',
|
||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||
),
|
||
|
||
SizedBox(height: 40),
|
||
|
||
// Email field
|
||
TextField(
|
||
controller: _emailController,
|
||
decoration: InputDecoration(
|
||
labelText: 'Email',
|
||
prefixIcon: Icon(Icons.email),
|
||
border: OutlineInputBorder(),
|
||
),
|
||
keyboardType: TextInputType.emailAddress,
|
||
),
|
||
|
||
SizedBox(height: 15),
|
||
|
||
// Password field
|
||
TextField(
|
||
controller: _passwordController,
|
||
decoration: InputDecoration(
|
||
labelText: 'Mot de passe',
|
||
prefixIcon: Icon(Icons.lock),
|
||
border: OutlineInputBorder(),
|
||
),
|
||
obscureText: true,
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
// Login button
|
||
ElevatedButton(
|
||
onPressed: _isLoading ? null : _login,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red.shade700,
|
||
padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
),
|
||
child: _isLoading
|
||
? CircularProgressIndicator(color: Colors.white)
|
||
: Text(
|
||
'Se connecter',
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Écran attente configuration Stripe
|
||
```dart
|
||
// lib/screens/stripe/stripe_setup_pending_screen.dart
|
||
class StripeSetupPendingScreen extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(30),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.hourglass_empty,
|
||
size: 80,
|
||
color: Colors.orange,
|
||
),
|
||
|
||
SizedBox(height: 30),
|
||
|
||
Text(
|
||
'Configuration en attente',
|
||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||
),
|
||
|
||
SizedBox(height: 20),
|
||
|
||
Text(
|
||
'Votre amicale n\'a pas encore configuré son compte de paiement Stripe.',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||
),
|
||
|
||
SizedBox(height: 10),
|
||
|
||
Text(
|
||
'Contactez votre responsable pour finaliser la configuration.',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||
),
|
||
|
||
SizedBox(height: 40),
|
||
|
||
ElevatedButton.icon(
|
||
onPressed: () async {
|
||
// Vérifier à nouveau le statut
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final amicaleId = prefs.getString('amicale_id');
|
||
|
||
final response = await ApiService().get(
|
||
'/amicales/$amicaleId/stripe-status'
|
||
);
|
||
|
||
if (response['charges_enabled']) {
|
||
Navigator.pushReplacement(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => PompierDashboardScreen()),
|
||
);
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('Configuration toujours en attente')),
|
||
);
|
||
}
|
||
},
|
||
icon: Icon(Icons.refresh),
|
||
label: Text('Vérifier à nouveau'),
|
||
style: ElevatedButton.styleFrom(
|
||
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 12),
|
||
),
|
||
),
|
||
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.pushReplacement(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => LoginScreen()),
|
||
);
|
||
},
|
||
child: Text('Se déconnecter'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 MARDI 02/09 - Tests unitaires (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Tests du service Terminal
|
||
```dart
|
||
// test/services/stripe_terminal_service_test.dart
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:mockito/mockito.dart';
|
||
|
||
void main() {
|
||
group('StripeTerminalService', () {
|
||
test('should detect Tap to Pay capability on compatible iPhone', () async {
|
||
// Test avec iPhone 14
|
||
final service = StripeTerminalService();
|
||
// Mock device info
|
||
final isCompatible = await service.checkTapToPayCapability();
|
||
expect(isCompatible, true);
|
||
});
|
||
|
||
test('should reject old iPhone models', () async {
|
||
// Test avec iPhone X
|
||
final service = StripeTerminalService();
|
||
// Mock device info pour iPhone X
|
||
final isCompatible = await service.checkTapToPayCapability();
|
||
expect(isCompatible, false);
|
||
});
|
||
});
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Tests d'intégration
|
||
```dart
|
||
// test/integration/payment_flow_test.dart
|
||
void main() {
|
||
testWidgets('Complete payment flow', (WidgetTester tester) async {
|
||
// Test du flux complet de paiement
|
||
await tester.pumpWidget(MyApp());
|
||
|
||
// Login
|
||
await tester.enterText(find.byType(TextField).first, 'test@pompiers.fr');
|
||
await tester.enterText(find.byType(TextField).last, 'password');
|
||
await tester.tap(find.text('Se connecter'));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Navigate to payment
|
||
await tester.tap(find.text('Nouveau paiement'));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Select amount
|
||
await tester.tap(find.text('20€'));
|
||
await tester.tap(find.text('Payer 20.00€'));
|
||
await tester.pumpAndSettle();
|
||
|
||
// Verify Tap to Pay screen
|
||
expect(find.text('Paiement sans contact'), findsOneWidget);
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 MERCREDI 03/09 - Optimisations et polish (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Gestion des erreurs améliorée
|
||
```dart
|
||
// lib/utils/error_handler.dart
|
||
class ErrorHandler {
|
||
static String getReadableError(dynamic error) {
|
||
if (error is StripeException) {
|
||
switch (error.code) {
|
||
case 'card_declined':
|
||
return 'Carte refusée';
|
||
case 'insufficient_funds':
|
||
return 'Solde insuffisant';
|
||
case 'lost_card':
|
||
return 'Carte perdue';
|
||
case 'stolen_card':
|
||
return 'Carte volée';
|
||
default:
|
||
return 'Erreur de paiement: ${error.message}';
|
||
}
|
||
}
|
||
|
||
if (error.toString().contains('SocketException')) {
|
||
return 'Pas de connexion internet';
|
||
}
|
||
|
||
return 'Une erreur est survenue. Veuillez réessayer.';
|
||
}
|
||
}
|
||
```
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Performance et cache
|
||
```dart
|
||
// lib/services/cache_service.dart
|
||
class CacheService {
|
||
static const Duration _cacheValidity = Duration(minutes: 5);
|
||
final Map<String, CacheEntry> _cache = {};
|
||
|
||
T? get<T>(String key) {
|
||
final entry = _cache[key];
|
||
if (entry == null) return null;
|
||
|
||
if (DateTime.now().difference(entry.timestamp) > _cacheValidity) {
|
||
_cache.remove(key);
|
||
return null;
|
||
}
|
||
|
||
return entry.data as T;
|
||
}
|
||
|
||
void set(String key, dynamic data) {
|
||
_cache[key] = CacheEntry(data: data, timestamp: DateTime.now());
|
||
}
|
||
}
|
||
|
||
class CacheEntry {
|
||
final dynamic data;
|
||
final DateTime timestamp;
|
||
|
||
CacheEntry({required this.data, required this.timestamp});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 JEUDI 04/09 - Finalisation et déploiement (8h)
|
||
|
||
---
|
||
|
||
## 📅 VENDREDI 05/09 - Livraison et support (8h)
|
||
|
||
### 🌅 Matin (4h)
|
||
|
||
#### ✅ Build et livraison finale
|
||
```bash
|
||
# iOS - Build final
|
||
flutter build ios --release
|
||
# Upload vers TestFlight pour distribution beta
|
||
```
|
||
|
||
#### ✅ Tests finaux sur appareils réels
|
||
- [ ] Test complet sur iPhone XS
|
||
- [ ] Test complet sur iPhone 14/15
|
||
- [ ] Validation avec vraies cartes test Stripe
|
||
- [ ] Vérification mode offline/online
|
||
|
||
### 🌆 Après-midi (4h)
|
||
|
||
#### ✅ Support et formation
|
||
- [ ] Formation équipe support client
|
||
- [ ] Création guide utilisateur pompier
|
||
- [ ] Vidéo démo Tap to Pay
|
||
- [ ] Support hotline pour premiers utilisateurs
|
||
- [ ] Monitoring des premiers paiements en production
|
||
|
||
---
|
||
|
||
## 📊 RÉCAPITULATIF
|
||
|
||
- **Total heures** : 72h sur 10 jours
|
||
- **Écrans créés** : 8
|
||
- **Services** : 5
|
||
- **Tests** : 15+
|
||
- **Compatibilité** : iOS uniquement (iPhone XS+, iOS 15.4+)
|
||
|
||
## 🎯 LIVRABLES
|
||
|
||
1. **App Flutter iOS & Android** avec Tap to Pay
|
||
2. **Détection automatique** compatibilité appareil (iOS et Android)
|
||
3. **Vérification API** pour appareils Android certifiés
|
||
4. **Mode offline** avec synchronisation
|
||
5. **Dashboard** des ventes
|
||
6. **Reçus** par email/SMS
|
||
7. **Liste dynamique** des appareils Android compatibles
|
||
|
||
## ⚠️ POINTS D'ATTENTION
|
||
|
||
- [ ] Tester sur vrais iPhone XS, 11, 12, 13, 14, 15
|
||
- [ ] Valider avec cartes test Stripe
|
||
- [ ] Former utilisateurs pilotes
|
||
- [ ] Prévoir support jour J
|
||
|
||
---
|
||
|
||
## 🎯 BILAN DÉVELOPPEMENT FLUTTER (01/09/2024)
|
||
|
||
### ✅ INTÉGRATION STRIPE CONNECT RÉALISÉE
|
||
|
||
#### **Interface de Configuration Stripe**
|
||
- **Widget**: `amicale_form.dart` mis à jour
|
||
- **Fonctionnalités implémentées**:
|
||
- Vérification statut compte Stripe automatique
|
||
- Bouton "Configurer Stripe" pour amicales non configurées
|
||
- Affichage statut : "✅ Compte Stripe configuré - 100% des paiements pour votre amicale"
|
||
- Gestion des erreurs et états de chargement
|
||
|
||
#### **Service Stripe Connect**
|
||
- **Fichier**: `stripe_connect_service.dart` ✅
|
||
- **Méthodes implémentées**:
|
||
```dart
|
||
Future<Map<String, dynamic>> createStripeAccount() // Créer compte
|
||
Future<Map<String, dynamic>> getAccountStatus(int id) // Statut compte
|
||
Future<String> createOnboardingLink(String accountId) // Lien onboarding
|
||
Future<Map<String, dynamic>> createLocation() // Location Terminal
|
||
```
|
||
- **Intégration API**: Communication avec backend PHP via `ApiService`
|
||
|
||
#### **États et Interface Utilisateur**
|
||
- **Statut Stripe dynamique** avec codes couleur :
|
||
- 🟠 Orange : Configuration en cours / Non configuré
|
||
- 🟢 Vert : Compte configuré et opérationnel
|
||
- **Messages utilisateur** :
|
||
- "💳 Activez les paiements par carte bancaire pour vos membres"
|
||
- "⏳ Configuration Stripe en cours. Veuillez compléter le processus d'onboarding."
|
||
- "✅ Compte Stripe configuré - 100% des paiements pour votre amicale"
|
||
|
||
### 🔧 ARCHITECTURE TECHNIQUE FLUTTER
|
||
|
||
#### **Pattern Repository**
|
||
- `AmicaleRepository`: Gestion des données amicale + intégration Stripe
|
||
- `ApiService`: Communication HTTP avec backend
|
||
- `HiveService`: Stockage local (pas utilisé pour Stripe - données sensibles)
|
||
|
||
#### **Gestion d'État**
|
||
- `ValueListenableBuilder`: Réactivité automatique UI
|
||
- `ChangeNotifier`: États de chargement Stripe
|
||
- Pas de stockage local des données Stripe (sécurité)
|
||
|
||
#### **Flow Utilisateur Implémenté**
|
||
1. **Amicale non configurée** → Bouton "Configurer Stripe" visible
|
||
2. **Clic configuration** → Appel API création compte
|
||
3. **Compte créé** → Redirection vers lien onboarding Stripe
|
||
4. **Onboarding complété** → Statut mise à jour automatiquement
|
||
5. **Compte opérationnel** → Message "100% des paiements"
|
||
|
||
### 📱 INTERFACE UTILISATEUR
|
||
|
||
#### **Responsive Design**
|
||
- Adaptation mobile/desktop
|
||
- Cards Material Design
|
||
- Indicateurs visuels clairs (icônes, couleurs)
|
||
- Gestion des états d'erreur
|
||
|
||
#### **Messages Utilisateur**
|
||
- **Français uniquement** (conforme CLAUDE.md)
|
||
- Termes métier appropriés ("amicale", "membres")
|
||
- Messages d'erreur explicites
|
||
- Feedback temps réel
|
||
|
||
### 🚀 FONCTIONNALITÉS OPÉRATIONNELLES
|
||
|
||
#### **Stripe Connect V1** ✅
|
||
- Création comptes Stripe Express ✅
|
||
- Génération liens onboarding ✅
|
||
- Vérification statut en temps réel ✅
|
||
- Affichage information "0% commission plateforme" ✅
|
||
|
||
#### **Prêt pour V2 - Terminal Payments** 🔄
|
||
- Architecture préparée pour Tap to Pay
|
||
- Services Stripe prêts pour extension
|
||
- Interface utilisateur extensible
|
||
|
||
### ⚠️ LIMITATIONS ACTUELLES
|
||
|
||
#### **V1 - Connect Onboarding Uniquement**
|
||
- Pas encore de paiements Terminal implémentés
|
||
- Interface de configuration uniquement
|
||
- Tap to Pay prévu en V2
|
||
|
||
#### **Sécurité**
|
||
- Aucune donnée sensible stockée localement
|
||
- Clés Stripe uniquement côté backend
|
||
- Communication HTTPS obligatoire
|
||
|
||
### 🎯 PROCHAINES ÉTAPES FLUTTER
|
||
|
||
#### **V2 - Terminal Payments (À venir)**
|
||
1. **Packages Stripe Terminal**
|
||
- `stripe_terminal: ^3.2.0`
|
||
- `stripe_ios: ^10.0.0` (iOS uniquement initialement)
|
||
|
||
2. **Écrans de Paiement**
|
||
- Sélection montant
|
||
- Interface Tap to Pay
|
||
- Confirmation et reçu
|
||
|
||
3. **Compatibilité Appareils**
|
||
- Vérification iPhone XS+ / iOS 15.4+
|
||
- Liste appareils Android certifiés (via API)
|
||
|
||
4. **Mode Offline**
|
||
- Queue de synchronisation
|
||
- Gestion connectivité réseau
|
||
|
||
#### **Tests et Validation**
|
||
- Tests widgets Stripe
|
||
- Tests d'intégration API
|
||
- Validation UX/UI sur vrais appareils
|
||
|
||
### 📊 MÉTRIQUES DÉVELOPPEMENT
|
||
|
||
- **Fichiers modifiés** : 2 (`amicale_form.dart`, `stripe_connect_service.dart`)
|
||
- **Lignes de code** : ~200 lignes ajoutées
|
||
- **APIs intégrées** : 4 endpoints Stripe
|
||
- **Tests** : Interface testée manuellement ✅
|
||
- **Statut** : V1 Connect opérationnelle ✅
|
||
|
||
---
|
||
|
||
*Document créé le 24/08/2024 - Dernière mise à jour : 01/09/2024* |