# PLANNING STRIPE - DÉVELOPPEUR FLUTTER
## App Flutter - Intégration Stripe Terminal Payments
### V1 ✅ Stripe Connect (Réalisée - 01/09/2024)
### V2 🔄 Tap to Pay (En cours de développement)
---
## 🎯 V2 - TAP TO PAY (NFC intégré uniquement)
### Période estimée : 1.5 semaine de développement
### Dernière mise à jour : 29/09/2025
### 📱 CONFIGURATIONS STRIPE TAP TO PAY CONFIRMÉES
- **iOS** : iPhone XS ou plus récent + iOS 16.4 minimum (source : Stripe docs officielles)
- **Android** : Appareils certifiés par Stripe (liste mise à jour hebdomadairement via API)
- **SDK Terminal** : Version 4.6.0 utilisée (minimum requis 2.23.0 ✅)
- **Batterie minimum** : 10% pour les paiements
- **NFC** : Obligatoire et activé
- **Web** : Non supporté (même sur mobile avec NFC)
---
## 📋 RÉSUMÉ EXÉCUTIF V2
### 🎯 Objectif Principal
Permettre aux membres des amicales de pompiers d'encaisser des paiements par carte bancaire sans contact directement depuis leur téléphone (iPhone XS+ avec iOS 16.4+ dans un premier temps).
### 💡 Fonctionnalités Clés
- **Tap to Pay** sur iPhone/Android (utilisation du NFC intégré du téléphone uniquement)
- **Montants flexibles** : Prédéfinis (10€, 20€, 30€, 50€) ou personnalisés
- **Mode offline** : File d'attente avec synchronisation automatique
- **Dashboard vendeur** : Suivi des ventes en temps réel
- **Reçus numériques** : Envoi par email/SMS
- **Multi-rôles** : Intégration avec le système de permissions existant
### ⚠️ Contraintes Techniques
- **iOS uniquement en V2.1** : iPhone XS minimum, iOS 16.4+
- **Android en V2.2** : Liste d'appareils certifiés via API
- **Connexion internet** : Requise pour initialisation, mode offline disponible ensuite
- **Compte Stripe** : L'amicale doit avoir complété l'onboarding V1
---
## 🗓️ PLANNING DÉTAILLÉ V2
### 📦 PHASE 1 : SETUP TECHNIQUE ET ARCHITECTURE
**Durée estimée : 1 jour**
**Objectif : Préparer l'environnement et l'architecture pour Stripe Tap to Pay**
#### 📚 1.1 Installation des packages (4h)
- [x] Ajouter `mek_stripe_terminal: ^4.6.0` dans pubspec.yaml ✅ FAIT
- [x] Ajouter `flutter_stripe: ^12.0.0` pour le SDK Stripe ✅ FAIT
- [x] Ajouter `device_info_plus: ^10.1.0` pour détecter le modèle d'iPhone ✅ FAIT
- [x] Ajouter `battery_plus: ^6.1.0` pour le niveau de batterie ✅ FAIT
- [x] Ajouter `network_info_plus: ^5.0.3` pour l'IP et WiFi ✅ FAIT
- [x] Ajouter `nfc_manager: ^3.5.0` pour la détection NFC ✅ FAIT
- [x] Connectivity déjà présent : `connectivity_plus: ^6.1.3` ✅ FAIT
- [x] Exécuter `flutter pub get` ✅ FAIT
- [ ] Exécuter `cd ios && pod install`
- [ ] Vérifier la compilation iOS sans erreurs
- [ ] Documenter les versions exactes installées
#### 🔧 1.2a Configuration iOS native (2h)
- [ ] Modifier `ios/Runner/Info.plist` avec les permissions NFC
- [ ] Ajouter `NSLocationWhenInUseUsageDescription` (requis par Stripe)
- [ ] Configurer les entitlements Tap to Pay Apple Developer
- [ ] Tester sur simulateur iOS
- [ ] Vérifier les permissions sur appareil physique
- [ ] Documenter les changements dans Info.plist
#### 🤖 1.2b Configuration Android native (2h)
- [ ] Modifier `android/app/src/main/AndroidManifest.xml` avec permissions NFC
- [ ] Ajouter ``
- [ ] Ajouter ``
- [ ] Ajouter ``
- [ ] Ajouter ``
- [ ] Vérifier/modifier `minSdkVersion 28` dans `android/app/build.gradle`
- [ ] Vérifier `targetSdkVersion 33` ou plus récent
- [ ] Tester sur appareil Android certifié Stripe
- [ ] Documenter les changements
#### 🏗️ 1.3 Architecture des services (4h)
- [ ] Créer `lib/core/services/stripe_tap_to_pay_service.dart`
- [ ] Implémenter le singleton StripeTapToPayService
- [ ] Créer la méthode `initialize()` avec gestion du token
- [ ] Créer la méthode `_fetchConnectionToken()` via API
- [ ] Implémenter la connexion au "lecteur" local (le téléphone)
- [ ] Créer `lib/core/repositories/payment_repository.dart`
- [ ] Implémenter les méthodes CRUD pour les paiements
- [ ] Intégrer avec le pattern Repository existant
- [ ] Ajouter les injections dans `app.dart`
---
### 🔍 PHASE 2 : VÉRIFICATION COMPATIBILITÉ
**Durée estimée : 1.5 jours**
**Objectif : Détecter et informer sur la compatibilité Tap to Pay**
#### 📱 2.1 Service de détection d'appareil (4h) ✅ COMPLÉTÉ
- [x] Créer `lib/core/services/device_info_service.dart` ✅ FAIT
- [x] Lister les modèles iPhone compatibles (XS, XR, 11, 12, 13, 14, 15, 16) ✅ FAIT
- [x] Vérifier la version iOS (≥ 16.4 pour Tap to Pay) ✅ FAIT - iOS 16.4 minimum confirmé par Stripe
- [x] Créer méthode `collectDeviceInfo()` et `canUseTapToPay()` ✅ FAIT avec batterie minimum 10%
- [x] Retourner les infos : model, osVersion, isCompatible, batteryLevel, IP ✅ FAIT
- [x] Gérer le cas Android (SDK ≥ 28 pour Tap to Pay) ✅ FAIT
- [x] Ajouter logs de debug pour diagnostic ✅ FAIT
- [x] Envoi automatique à l'API après login : POST `/users/device-info` ✅ FAIT dans ApiService
- [x] Sauvegarde dans Hive box settings ✅ FAIT avec préfixe `device_`
- [x] **NOUVEAU** : Vérification certification Stripe via API `/stripe/devices/check-tap-to-pay` ✅ FAIT
- [x] **NOUVEAU** : Méthode `checkStripeCertification()` pour Android ✅ FAIT
- [x] **NOUVEAU** : Stockage `device_stripe_certified` dans Hive ✅ FAIT
- [x] **NOUVEAU** : Messages d'erreur détaillés selon le problème (NFC, certification, batterie) ✅ FAIT
#### 🎨 2.2 Écran de vérification (4h)
- [ ] Créer `lib/presentation/payment/compatibility_check_page.dart`
- [ ] Design responsive avec icônes et messages clairs
- [ ] Afficher le modèle d'appareil détecté
- [ ] Afficher la version iOS
- [ ] Message explicatif si non compatible
- [ ] Bouton "Continuer" si compatible
- [ ] Bouton "Retour" si non compatible
- [ ] Intégrer avec la navigation existante
#### 🔄 2.3 Intégration dans le flux utilisateur (4h)
- [ ] Ajouter vérification au démarrage de l'app
- [ ] Sauvegarder le résultat dans SharedPreferences
- [ ] Afficher/masquer les fonctionnalités selon compatibilité
- [ ] Ajouter indicateur dans le dashboard utilisateur
- [ ] Gérer le cas de mise à jour iOS pendant utilisation
---
### 💳 PHASE 3 : INTERFACE DE PAIEMENT
**Durée estimée : 2 jours**
**Objectif : Créer les écrans de sélection et confirmation de paiement**
#### 🎯 3.1 Écran de sélection du montant (6h)
- [ ] Créer `lib/presentation/payment/payment_amount_page.dart`
- [ ] Design avec chips pour montants prédéfinis (10€, 20€, 30€, 50€)
- [ ] Champ de saisie pour montant personnalisé
- [ ] Validation min 1€, max 999€
- [ ] Afficher info amicale en header
- [ ] Calculer et afficher les frais Stripe (si applicable)
- [ ] Bouton "Continuer" avec montant sélectionné
- [ ] Animation de sélection des chips
- [ ] Responsive pour toutes tailles d'écran
#### 📝 3.2 Écran de détails du paiement (4h)
- [ ] Créer `lib/presentation/payment/payment_details_page.dart`
- [ ] Formulaire optionnel : nom, email, téléphone du donateur
- [ ] Checkbox pour reçu (email ou SMS)
- [ ] Résumé : montant, amicale, date
- [ ] Bouton "Payer avec carte sans contact"
- [ ] Possibilité d'ajouter une note/commentaire
- [ ] Sauvegarde locale des infos saisies
#### 🎨 3.3 Composants UI réutilisables (4h)
- [ ] Créer `lib/presentation/widgets/payment/amount_selector_widget.dart`
- [ ] Créer `lib/presentation/widgets/payment/payment_summary_card.dart`
- [ ] Créer `lib/presentation/widgets/payment/donor_info_form.dart`
- [ ] Styles cohérents avec le design existant
- [ ] Animations et feedback visuel
---
### 📲 PHASE 4 : FLUX TAP TO PAY
**Durée estimée : 3 jours**
**Objectif : Implémenter le processus de paiement sans contact**
#### 🎯 4.1 Écran Tap to Pay principal (8h)
- [ ] Créer `lib/presentation/payment/tap_to_pay_page.dart`
- [ ] Afficher montant en grand format
- [ ] Animation NFC (ondes pulsantes)
- [ ] Instructions "Approchez la carte du dos de l'iPhone"
- [ ] Gestion des états : attente, lecture, traitement, succès, échec
- [ ] Bouton annuler pendant l'attente
- [ ] Timeout après 60 secondes
- [ ] Son/vibration au succès
#### 🔄 4.2 Intégration Stripe Tap to Pay (6h)
- [ ] Initialiser le service Tap to Pay local (pas de découverte de lecteurs)
- [ ] Créer PaymentIntent via API backend
- [ ] Implémenter `collectPaymentMethod()` avec NFC du téléphone
- [ ] Implémenter `confirmPaymentIntent()`
- [ ] Gérer les erreurs Stripe spécifiques
- [ ] Logs détaillés pour debug
- [ ] Gestion des timeouts et retry
#### ✅ 4.3 Écran de confirmation (4h)
- [ ] Créer `lib/presentation/payment/payment_success_page.dart`
- [ ] Animation de succès (check vert)
- [ ] Afficher montant et référence de transaction
- [ ] Options : Envoyer reçu, Nouveau paiement, Retour
- [ ] Partage du reçu (share sheet iOS)
- [ ] Sauvegarde locale de la transaction
#### ❌ 4.4 Gestion des erreurs (4h)
- [ ] Créer `lib/presentation/payment/payment_error_page.dart`
- [ ] Messages d'erreur traduits en français
- [ ] Différencier : carte refusée, solde insuffisant, erreur réseau, etc.
- [ ] Bouton "Réessayer" avec même montant
- [ ] Bouton "Changer de montant"
- [ ] Logs pour support technique
---
### 📶 PHASE 5 : MODE OFFLINE ET SYNCHRONISATION
**Durée estimée : 2 jours**
**Objectif : Permettre les paiements sans connexion internet**
#### 💾 5.1 Service de queue offline (6h)
- [ ] Créer `lib/core/services/offline_payment_queue_service.dart`
- [ ] Stocker les paiements dans SharedPreferences
- [ ] Structure : amount, timestamp, amicale_id, user_id, status
- [ ] Méthode `addToQueue()` pour nouveaux paiements
- [ ] Méthode `getQueueSize()` pour badge notification
- [ ] Méthode `clearQueue()` après sync réussie
- [ ] Limite de 100 paiements en queue
- [ ] Expiration après 7 jours
#### 🔄 5.2 Service de synchronisation (6h)
- [ ] Créer `lib/core/services/payment_sync_service.dart`
- [ ] Détecter le retour de connexion avec ConnectivityPlus
- [ ] Envoyer les paiements par batch à l'API
- [ ] Gérer les échecs partiels
- [ ] Retry avec backoff exponentiel
- [ ] Notification de sync réussie
- [ ] Logs de synchronisation
#### 📊 5.3 UI du mode offline (4h)
- [ ] Indicateur "Mode hors ligne" dans l'app bar
- [ ] Badge avec nombre de paiements en attente
- [ ] Écran de détail de la queue
- [ ] Bouton "Forcer la synchronisation"
- [ ] Messages informatifs sur l'état
---
### 📈 PHASE 6 : DASHBOARD ET STATISTIQUES
**Durée estimée : 2 jours**
**Objectif : Tableau de bord pour suivre les ventes**
#### 📊 6.1 Dashboard vendeur (8h)
- [ ] Créer `lib/presentation/dashboard/vendor_dashboard_page.dart`
- [ ] Widget statistiques du jour (nombre, montant total)
- [ ] Widget statistiques de la semaine
- [ ] Widget statistiques du mois
- [ ] Graphique d'évolution (fl_chart)
- [ ] Liste des 10 dernières transactions
- [ ] Filtres par période
- [ ] Export CSV des données
#### 📱 6.2 Détail d'une transaction (4h)
- [ ] Créer `lib/presentation/payment/transaction_detail_page.dart`
- [ ] Afficher toutes les infos de la transaction
- [ ] Status : succès, en attente, échoué
- [ ] Option renvoyer le reçu
- [ ] Option annuler (si possible)
- [ ] Historique des actions
#### 🔔 6.3 Notifications et rappels (4h)
- [ ] Widget de rappel de synchronisation
- [ ] Notification de paiements en attente
- [ ] Alerte si compte Stripe a un problème
- [ ] Rappel de fin de journée pour sync
---
### 🧪 PHASE 7 : TESTS ET VALIDATION
**Durée estimée : 2 jours**
**Objectif : Assurer la qualité et la fiabilité**
#### ✅ 7.1 Tests unitaires (6h)
- [ ] Tests StripeTerminalService
- [ ] Tests DeviceCompatibilityService
- [ ] Tests OfflineQueueService
- [ ] Tests PaymentRepository
- [ ] Tests de validation des montants
- [ ] Tests de sérialisation/désérialisation
- [ ] Coverage > 80%
#### 📱 7.2 Tests d'intégration (6h)
- [ ] Test flux complet de paiement
- [ ] Test mode offline vers online
- [ ] Test gestion des erreurs
- [ ] Test sur différents iPhones
- [ ] Test avec cartes de test Stripe
- [ ] Test limites et edge cases
#### 🎭 7.3 Tests utilisateurs (4h)
- [ ] Créer scénarios de test
- [ ] Test avec 5 utilisateurs pilotes
- [ ] Collecter les retours
- [ ] Corriger les bugs identifiés
- [ ] Valider l'ergonomie
---
### 🚀 PHASE 8 : DÉPLOIEMENT ET DOCUMENTATION
**Durée estimée : 1 jour**
**Objectif : Mise en production et formation**
#### 📦 8.1 Build et déploiement (4h)
- [ ] Build iOS release
- [ ] Upload sur TestFlight
- [ ] Tests de non-régression
- [ ] Déploiement sur App Store
- [ ] Monitoring des premières 24h
#### 📚 8.2 Documentation (4h)
- [ ] Guide utilisateur pompier (PDF)
- [ ] Vidéo tutoriel Tap to Pay
- [ ] FAQ problèmes courants
- [ ] Documentation technique
- [ ] Formation équipe support
---
## 🔄 FLOW COMPLET DE PAIEMENT TAP TO PAY
### 📋 Vue d'ensemble du processus
Le flow de paiement se déroule en plusieurs étapes distinctes entre l'application Flutter, l'API PHP et Stripe :
```
App Flutter → API PHP → Stripe Terminal API → Retour App → NFC Payment → Confirmation
```
### 🎯 Étapes détaillées du flow
#### 1️⃣ **PRÉPARATION DU PAIEMENT (App Flutter)**
- L'utilisateur sélectionne ou crée un passage
- Choix du montant et sélection "Carte Bancaire"
- Récupération du `passage_id` existant ou 0 pour nouveau
#### 2️⃣ **CRÉATION DU PAYMENT INTENT (App → API → Stripe)**
**Requête App → API:**
```json
POST /api/stripe/payments/create-intent
{
"amount": 2000, // en centimes
"currency": "eur",
"payment_method_types": ["card_present"],
"passage_id": 123, // ou 0 si nouveau
"amicale_id": 45,
"member_id": 67,
"stripe_account": "acct_xxx",
"metadata": {
"passage_id": "123",
"type": "tap_to_pay"
}
}
```
**L'API fait alors :**
1. Validation des données reçues
2. Appel Stripe API pour créer le PaymentIntent
3. Stockage en base de données locale
4. Retour à l'app avec `payment_intent_id` et `client_secret`
**Réponse API → App:**
```json
{
"payment_intent_id": "pi_xxx",
"client_secret": "pi_xxx_secret_xxx",
"amount": 2000,
"status": "requires_payment_method"
}
```
#### 3️⃣ **COLLECTE DE LA CARTE (App avec SDK Stripe Terminal)**
L'application utilise le SDK natif pour :
1. Activer le NFC du téléphone
2. Afficher l'écran "Approchez la carte"
3. Lire les données de la carte sans contact
4. Traiter le paiement localement via le SDK
#### 4️⃣ **TRAITEMENT DU PAIEMENT (SDK → Stripe)**
Le SDK Stripe Terminal :
- Envoie les données cryptées de la carte à Stripe
- Traite l'autorisation bancaire
- Retourne le statut du paiement à l'app
#### 5️⃣ **CONFIRMATION ET SAUVEGARDE (App → API)**
**Si paiement réussi :**
```json
POST /api/stripe/payments/confirm
{
"payment_intent_id": "pi_xxx",
"status": "succeeded",
"amount": 2000
}
```
**Puis sauvegarde du passage :**
```json
POST /api/passages
{
"id": 123,
"fk_type": 1, // Effectué
"montant": "20.00",
"fk_type_reglement": 3, // CB
"stripe_payment_id": "pi_xxx",
...
}
```
### 📊 Différences Web vs Tap to Pay
| Aspect | Paiement Web | Tap to Pay |
|--------|-------------|------------|
| **payment_method_types** | ["card"] | ["card_present"] |
| **SDK utilisé** | Stripe.js | Stripe Terminal SDK |
| **Collecte carte** | Formulaire web | NFC téléphone |
| **Metadata** | type: "web" | type: "tap_to_pay" |
| **Environnement** | Navigateur | App native |
| **Prérequis** | Aucun | iPhone XS+ iOS 16.4+ |
### ⚡ Points clés du flow
1. **Passage ID** : Toujours inclus (existant ou 0)
2. **Double confirmation** : PaymentIntent ET Passage sauvegardé
3. **Metadata Stripe** : Permet la traçabilité bidirectionnelle
4. **Endpoint unifié** : `/api/stripe/payments/` pour tous types
5. **Gestion erreurs** : À chaque étape du processus
## 🔄 PHASE 9 : ÉVOLUTIONS FUTURES (V2.2+)
### 📱 Support Android (V2.2)
- [ ] Vérification appareils Android certifiés via API
- [ ] Intégration SDK Android Tap to Pay
- [ ] Tests sur appareils Android certifiés
### 🌍 Fonctionnalités avancées (V2.3)
- [ ] Multi-devises
- [ ] Paiements récurrents (abonnements)
- [ ] Programme de fidélité
- [ ] Intégration comptable
- [ ] Rapports fiscaux automatiques
---
## 📊 MÉTRIQUES DE SUCCÈS
### KPIs Techniques
- [ ] Taux de succès des paiements > 95%
- [ ] Temps moyen de transaction < 15 secondes
- [ ] Synchronisation offline réussie > 99%
- [ ] Crash rate < 0.1%
### KPIs Business
- [ ] Adoption par > 50% des membres en 3 mois
- [ ] Augmentation des dons de 30%
- [ ] Satisfaction utilisateur > 4.5/5
- [ ] Réduction des paiements espèces de 60%
---
## ⚠️ RISQUES ET MITIGATION
### Risques Techniques
| Risque | Impact | Probabilité | Mitigation |
|--------|--------|-------------|------------|
| Incompatibilité iOS | Élevé | Moyen | Détection précoce, messages clairs |
| Problèmes réseau | Moyen | Élevé | Mode offline robuste |
| Erreurs Stripe | Élevé | Faible | Retry logic, logs détaillés |
| Performance | Moyen | Moyen | Optimisation, cache |
### Risques Business
| Risque | Impact | Probabilité | Mitigation |
|--------|--------|-------------|------------|
| Résistance au changement | Élevé | Moyen | Formation, support, incentives |
| Conformité RGPD | Élevé | Faible | Audit, documentation |
| Coûts Stripe | Moyen | Certain | Communication transparente |
---
## 📅 HISTORIQUE V1 - STRIPE CONNECT (COMPLÉTÉE)
### ✅ Fonctionnalités V1 Réalisées (01/09/2024)
#### Configuration Stripe Connect
- ✅ Widget `amicale_form.dart` avec intégration Stripe
- ✅ Service `stripe_connect_service.dart` complet
- ✅ Création de comptes Stripe Express
- ✅ Génération de liens d'onboarding
- ✅ Vérification du statut en temps réel
- ✅ Messages utilisateur en français
- ✅ Interface responsive mobile/desktop
#### API Endpoints Intégrés
- ✅ `/amicales/{id}/stripe/create-account` - Création compte
- ✅ `/amicales/{id}/stripe/account-status` - Vérification statut
- ✅ `/amicales/{id}/stripe/onboarding-link` - Lien configuration
- ✅ `/amicales/{id}/stripe/create-location` - Location Terminal
#### Statuts et Messages
- ✅ "💳 Activez les paiements par carte bancaire"
- ✅ "⏳ Configuration Stripe en cours"
- ✅ "✅ Compte Stripe configuré - 100% des paiements"
---
## 📝 NOTES DE DÉVELOPPEMENT
### Points d'attention pour la V2
1. **Dépendance V1** : L'amicale doit avoir complété l'onboarding Stripe (V1) avant de pouvoir utiliser Tap to Pay (V2)
2. **Architecture existante** : Utiliser le pattern Repository et les services singleton déjà en place
3. **Gestion d'erreurs** : Utiliser `ApiException` pour tous les messages d'erreur
4. **Réactivité** : Utiliser `ValueListenableBuilder` avec les Box Hive
5. **Multi-environnement** : L'ApiService détecte automatiquement DEV/REC/PROD
### Conventions de code
- Noms de fichiers en snake_case
- Classes en PascalCase
- Variables et méthodes en camelCase
- Pas de Provider/Bloc, utiliser l'injection directe
- Tests unitaires obligatoires pour chaque service
### 🎯 Scope Stripe - Exclusivement logiciel
- **TAP TO PAY UNIQUEMENT** : Utilisation du NFC intégré du téléphone
- **PAS de terminaux physiques** : Pas de Bluetooth, USB ou Lightning
- **PAS de lecteurs externes** : Pas de WisePad, Reader M2, etc.
- **Futur** : Paiements Web via Stripe.js
### Ressources utiles
- [Documentation Stripe Terminal Flutter](https://stripe.com/docs/terminal/payments/setup-flutter)
- [Apple Tap to Pay Requirements](https://developer.apple.com/tap-to-pay/)
- [Flutter Hive Documentation](https://docs.hivedb.dev/)
---
## 🔄 DERNIÈRES MISES À JOUR
- **29/09/2025** : Clarification du scope et mise à jour complète
- ✅ Scope : TAP TO PAY UNIQUEMENT (pas de terminaux physiques)
- ✅ Suppression références Bluetooth et lecteurs externes
- ✅ Réduction estimation : 1.5 semaine au lieu de 2-3 semaines
- ✅ DeviceInfoService avec vérification API pour Android
- ✅ Intégration endpoints `/stripe/devices/check-tap-to-pay`
- ✅ Gestion batterie minimum 10%
- ✅ Messages d'erreur détaillés selon le problème
- ✅ Correction bug Tap to Pay sur web mobile
- ✅ SDK Stripe Terminal 4.6.0 (compatible avec requirements)
- **28/09/2025** : Création du planning détaillé V2 avec 9 phases et 200+ TODO
- **01/09/2024** : V1 Stripe Connect complétée et opérationnelle
- **25/08/2024** : Début du développement V1
---
## 📞 CONTACTS PROJET
- **Product Owner** : À définir
- **Tech Lead Flutter** : À définir
- **Support Stripe** : support@stripe.com
- **Équipe Backend PHP** : À coordonner pour les endpoints API
---
*Document de planification V2 - Terminal Payments*
*Dernière révision : 28/09/2025*
connectivity_plus: ^5.0.2 # Connectivité réseau
```
**STATUS**: Configuration Stripe Connect intégrée dans l'interface existante
```bash
cd app
flutter pub get
cd ios
pod install
```
#### ✅ Configuration iOS
```xml
NSLocationWhenInUseUsageDescription
Localisation nécessaire pour les paiements Stripe
```
#### ✅ Configuration Android
```xml
```
```gradle
// android/app/build.gradle
android {
defaultConfig {
minSdkVersion 28 // Minimum requis pour Tap to Pay Android
targetSdkVersion 33 // Ou plus récent
compileSdkVersion 33
}
}
```
### 🌆 Après-midi (4h)
#### ✅ Service de base StripeTerminalService
```dart
// lib/services/stripe_terminal_service.dart
import 'package:stripe_terminal/stripe_terminal.dart';
class StripeTerminalService {
static final StripeTerminalService _instance = StripeTerminalService._internal();
factory StripeTerminalService() => _instance;
StripeTerminalService._internal();
Terminal? _terminal;
bool _isInitialized = false;
Future initialize() async {
if (_isInitialized) return;
_terminal = await Terminal.getInstance(
fetchConnectionToken: _fetchConnectionToken,
);
_isInitialized = true;
}
Future _fetchConnectionToken() async {
final response = await ApiService().post('/terminal/connection-token');
return response['secret'];
}
Future checkTapToPayCapability() async {
if (!Platform.isIOS) return false;
final deviceInfo = DeviceInfoPlugin();
final iosInfo = await deviceInfo.iosInfo;
// iPhone XS et ultérieurs
final supportedModels = [
'iPhone11,', // XS, XS Max
'iPhone12,', // 11, 11 Pro
'iPhone13,', // 12 series
'iPhone14,', // 13 series
'iPhone15,', // 14 series
'iPhone16,', // 15 series
];
final modelIdentifier = iosInfo.utsname.machine;
final isSupported = supportedModels.any((model) =>
modelIdentifier.startsWith(model)
);
// iOS 16.4 minimum
final osVersion = iosInfo.systemVersion.split('.').map(int.parse).toList();
final isOSSupported = osVersion[0] > 16 ||
(osVersion[0] == 16 && osVersion.length > 1 && osVersion[1] >= 4);
return isSupported && isOSSupported;
}
}
```
---
## 📅 MARDI 26/08 - UI Paiement principal (8h)
### 🌅 Matin (4h)
#### ✅ Écran de vérification compatibilité
```dart
// lib/screens/payment/compatibility_check_screen.dart
class CompatibilityCheckScreen extends StatefulWidget {
@override
_CompatibilityCheckScreenState createState() => _CompatibilityCheckScreenState();
}
class _CompatibilityCheckScreenState extends State {
bool? _isCompatible;
String _deviceInfo = '';
@override
void initState() {
super.initState();
_checkCompatibility();
}
Future _checkCompatibility() async {
final service = StripeTerminalService();
final compatible = await service.checkTapToPayCapability();
final deviceInfo = DeviceInfoPlugin();
final iosInfo = await deviceInfo.iosInfo;
setState(() {
_isCompatible = compatible;
_deviceInfo = '${iosInfo.name} - iOS ${iosInfo.systemVersion}';
});
if (compatible) {
await service.initialize();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => PaymentScreen())
);
}
}
@override
Widget build(BuildContext context) {
if (_isCompatible == null) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('Vérification de compatibilité...'),
],
),
),
);
}
if (!_isCompatible!) {
return Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 80, color: Colors.orange),
SizedBox(height: 20),
Text(
'Tap to Pay non disponible',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'Votre appareil: $_deviceInfo',
style: TextStyle(color: Colors.grey),
),
SizedBox(height: 20),
Text(
'Tap to Pay nécessite un iPhone XS ou plus récent avec iOS 16.4+',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
SizedBox(height: 30),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('Retour'),
),
],
),
),
),
);
}
return Container(); // Ne devrait jamais arriver ici
}
}
```
### 🌆 Après-midi (4h)
#### ✅ Écran de paiement principal
```dart
// lib/screens/payment/payment_screen.dart
class PaymentScreen extends StatefulWidget {
final Amicale amicale;
PaymentScreen({required this.amicale});
@override
_PaymentScreenState createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State {
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 _startPayment() async {
setState(() => _isProcessing = true);
try {
// Naviguer vers l'écran Tap to Pay
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => TapToPayScreen(
amount: _selectedAmount,
amicale: widget.amicale,
),
),
);
if (result != null && result['success']) {
_showSuccessDialog(result['paymentIntentId']);
}
} catch (e) {
_showErrorDialog(e.toString());
} finally {
setState(() => _isProcessing = false);
}
}
void _showSuccessDialog(String paymentIntentId) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.check_circle, color: Colors.green, size: 30),
SizedBox(width: 10),
Text('Paiement réussi !'),
],
),
content: Text('Le paiement a été effectué avec succès.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop(); // Retour écran principal
},
child: Text('OK'),
),
],
),
);
}
void _showErrorDialog(String error) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Erreur de paiement'),
content: Text(error),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Réessayer'),
),
],
),
);
}
}
```
---
## 📅 MERCREDI 27/08 - Flux Tap to Pay (8h)
### 🌅 Matin (4h)
#### ✅ Écran Tap to Pay
```dart
// lib/screens/payment/tap_to_pay_screen.dart
class TapToPayScreen extends StatefulWidget {
final double amount;
final Amicale amicale;
TapToPayScreen({required this.amount, required this.amicale});
@override
_TapToPayScreenState createState() => _TapToPayScreenState();
}
class _TapToPayScreenState extends State {
final _terminalService = StripeTerminalService();
PaymentIntent? _paymentIntent;
String _status = 'Initialisation...';
bool _isProcessing = true;
@override
void initState() {
super.initState();
_initializePayment();
}
Future _initializePayment() async {
try {
setState(() => _status = 'Création du paiement...');
// 1. Créer PaymentIntent via API
final response = await ApiService().post('/payments/create-intent', {
'amount': (widget.amount * 100).round(), // en centimes
'amicale_id': widget.amicale.id,
});
setState(() => _status = 'Connexion au lecteur...');
// 2. Initialiser le lecteur Tap to Pay local (le téléphone)
await _terminalService.initializeLocalReader();
// Pas de découverte de lecteurs externes - le téléphone EST le lecteur
setState(() => _status = 'Prêt pour le paiement');
// 3. Collecter le paiement
_paymentIntent = await _terminalService.collectPaymentMethod(
response['client_secret'],
);
setState(() => _status = 'Traitement du paiement...');
// 4. Confirmer le paiement
final confirmedIntent = await _terminalService.confirmPaymentIntent(
_paymentIntent!,
);
if (confirmedIntent.status == PaymentIntentStatus.succeeded) {
_onPaymentSuccess(confirmedIntent.id);
} else {
throw Exception('Paiement non confirmé');
}
} catch (e) {
_onPaymentError(e.toString());
}
}
void _onPaymentSuccess(String paymentIntentId) {
Navigator.pop(context, {
'success': true,
'paymentIntentId': paymentIntentId,
});
}
void _onPaymentError(String error) {
setState(() {
_isProcessing = false;
_status = 'Erreur: $error';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade100,
body: SafeArea(
child: Column(
children: [
// Header
Container(
padding: EdgeInsets.all(20),
color: Colors.white,
child: Row(
children: [
IconButton(
icon: Icon(Icons.close),
onPressed: _isProcessing ? null : () => Navigator.pop(context),
),
Expanded(
child: Text(
'Paiement sans contact',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
SizedBox(width: 48), // Pour équilibrer avec IconButton
],
),
),
// Montant
Container(
margin: EdgeInsets.all(20),
padding: EdgeInsets.all(30),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Column(
children: [
Text(
'${widget.amount.toStringAsFixed(2)} €',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
SizedBox(height: 10),
Text(
widget.amicale.name,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
// Animation Tap to Pay
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_status.contains('Prêt'))
TapToPayAnimation()
else if (_isProcessing)
CircularProgressIndicator(
size: 80,
strokeWidth: 8,
valueColor: AlwaysStoppedAnimation(Colors.red.shade700),
),
SizedBox(height: 30),
Text(
_status,
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
if (_status.contains('Prêt'))
Padding(
padding: EdgeInsets.only(top: 20),
child: Text(
'Approchez la carte du dos de l\'iPhone',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
],
),
),
),
// Footer
if (!_isProcessing && _status.contains('Erreur'))
Padding(
padding: EdgeInsets.all(20),
child: ElevatedButton(
onPressed: _initializePayment,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
),
child: Text('Réessayer', style: TextStyle(fontSize: 16)),
),
),
],
),
),
);
}
}
```
### 🌆 Après-midi (4h)
#### ✅ Animation Tap to Pay
```dart
// lib/widgets/tap_to_pay_animation.dart
class TapToPayAnimation extends StatefulWidget {
@override
_TapToPayAnimationState createState() => _TapToPayAnimationState();
}
class _TapToPayAnimationState extends State
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _scaleAnimation;
late Animation _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
_scaleAnimation = Tween(
begin: 0.8,
end: 1.5,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
_opacityAnimation = Tween(
begin: 1.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
// Ondes animées
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.red.shade700,
width: 3,
),
),
),
),
);
},
),
// Icône centrale
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.red.shade700,
shape: BoxShape.circle,
),
child: Icon(
Icons.contactless,
size: 60,
color: Colors.white,
),
),
],
),
);
}
}
```
---
## 📅 JEUDI 28/08 - Gestion offline et sync (8h)
### 🌅 Matin (4h)
#### ✅ Service de queue offline
```dart
// lib/services/offline_queue_service.dart
class OfflineQueueService {
static const String _queueKey = 'offline_payment_queue';
Future addToQueue(PaymentData payment) async {
final prefs = await SharedPreferences.getInstance();
final queueJson = prefs.getString(_queueKey) ?? '[]';
final queue = List