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

70 KiB
Raw Blame History

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)

  • Ajouter mek_stripe_terminal: ^4.6.0 dans pubspec.yaml FAIT
  • Ajouter flutter_stripe: ^12.0.0 pour le SDK Stripe FAIT
  • Ajouter device_info_plus: ^10.1.0 pour détecter le modèle d'iPhone FAIT
  • Ajouter battery_plus: ^6.1.0 pour le niveau de batterie FAIT
  • Ajouter network_info_plus: ^5.0.3 pour l'IP et WiFi FAIT
  • Ajouter nfc_manager: ^3.5.0 pour la détection NFC FAIT
  • Connectivity déjà présent : connectivity_plus: ^6.1.3 FAIT
  • 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É

  • Créer lib/core/services/device_info_service.dart FAIT
  • Lister les modèles iPhone compatibles (XS, XR, 11, 12, 13, 14, 15, 16) FAIT
  • Vérifier la version iOS (≥ 16.4 pour Tap to Pay) FAIT - iOS 16.4 minimum confirmé par Stripe
  • Créer méthode collectDeviceInfo() et canUseTapToPay() FAIT avec batterie minimum 10%
  • Retourner les infos : model, osVersion, isCompatible, batteryLevel, IP FAIT
  • Gérer le cas Android (SDK ≥ 28 pour Tap to Pay) FAIT
  • Ajouter logs de debug pour diagnostic FAIT
  • Envoi automatique à l'API après login : POST /users/device-info FAIT dans ApiService
  • Sauvegarde dans Hive box settings FAIT avec préfixe device_
  • NOUVEAU : Vérification certification Stripe via API /stripe/devices/check-tap-to-pay FAIT
  • NOUVEAU : Méthode checkStripeCertification() pour Android FAIT
  • NOUVEAU : Stockage device_stripe_certified dans Hive FAIT
  • 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:

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:

{
  "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 :

POST /api/stripe/payments/confirm
{
  "payment_intent_id": "pi_xxx",
  "status": "succeeded",
  "amount": 2000
}

Puis sauvegarde du passage :

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


🔄 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

<!-- 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

<!-- 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" />
// 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

// 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é

// 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

// 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

// 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

// 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

// 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é

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

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