Files
geo/app/docs/PLANNING-STRIPE-FLUTTER.md
pierre b6584c83fa feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API
- 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>
2025-10-05 20:11:15 +02:00

2263 lines
70 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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*