- 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>
70 KiB
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.0dans pubspec.yaml ✅ FAIT - Ajouter
flutter_stripe: ^12.0.0pour le SDK Stripe ✅ FAIT - Ajouter
device_info_plus: ^10.1.0pour détecter le modèle d'iPhone ✅ FAIT - Ajouter
battery_plus: ^6.1.0pour le niveau de batterie ✅ FAIT - Ajouter
network_info_plus: ^5.0.3pour l'IP et WiFi ✅ FAIT - Ajouter
nfc_manager: ^3.5.0pour 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.plistavec 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.xmlavec 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 28dansandroid/app/build.gradle - Vérifier
targetSdkVersion 33ou 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()etcanUseTapToPay()✅ 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_certifieddans 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_idexistant 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 :
- Validation des données reçues
- Appel Stripe API pour créer le PaymentIntent
- Stockage en base de données locale
- Retour à l'app avec
payment_intent_idetclient_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 :
- Activer le NFC du téléphone
- Afficher l'écran "Approchez la carte"
- Lire les données de la carte sans contact
- 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
- Passage ID : Toujours inclus (existant ou 0)
- Double confirmation : PaymentIntent ET Passage sauvegardé
- Metadata Stripe : Permet la traçabilité bidirectionnelle
- Endpoint unifié :
/api/stripe/payments/pour tous types - 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.dartavec intégration Stripe - ✅ Service
stripe_connect_service.dartcomplet - ✅ 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
- Dépendance V1 : L'amicale doit avoir complété l'onboarding Stripe (V1) avant de pouvoir utiliser Tap to Pay (V2)
- Architecture existante : Utiliser le pattern Repository et les services singleton déjà en place
- Gestion d'erreurs : Utiliser
ApiExceptionpour tous les messages d'erreur - Réactivité : Utiliser
ValueListenableBuilderavec les Box Hive - 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
- App Flutter iOS & Android avec Tap to Pay
- Détection automatique compatibilité appareil (iOS et Android)
- Vérification API pour appareils Android certifiés
- Mode offline avec synchronisation
- Dashboard des ventes
- Reçus par email/SMS
- 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.dartmis à 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 StripeApiService: Communication HTTP avec backendHiveService: Stockage local (pas utilisé pour Stripe - données sensibles)
Gestion d'État
ValueListenableBuilder: Réactivité automatique UIChangeNotifier: États de chargement Stripe- Pas de stockage local des données Stripe (sécurité)
Flow Utilisateur Implémenté
- Amicale non configurée → Bouton "Configurer Stripe" visible
- Clic configuration → Appel API création compte
- Compte créé → Redirection vers lien onboarding Stripe
- Onboarding complété → Statut mise à jour automatiquement
- 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)
-
Packages Stripe Terminal
stripe_terminal: ^3.2.0stripe_ios: ^10.0.0(iOS uniquement initialement)
-
Écrans de Paiement
- Sélection montant
- Interface Tap to Pay
- Confirmation et reçu
-
Compatibilité Appareils
- Vérification iPhone XS+ / iOS 15.4+
- Liste appareils Android certifiés (via API)
-
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