feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -278,55 +278,102 @@ Log::info('Stripe account activated', [
|
||||
|
||||
## 📱 FLOW TAP TO PAY (Application Flutter)
|
||||
|
||||
### 🎯 Architecture technique
|
||||
|
||||
Le flow Tap to Pay repose sur trois composants principaux :
|
||||
|
||||
1. **DeviceInfoService** - Vérification de compatibilité hardware (Android SDK ≥ 28, NFC disponible)
|
||||
2. **StripeTapToPayService** - Gestion du SDK Stripe Terminal et des paiements
|
||||
3. **Backend API** - Endpoints PHP pour les tokens de connexion et PaymentIntents
|
||||
|
||||
### 🔄 Diagramme de séquence complet
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐
|
||||
│ App Flutter │ │ API PHP │ │ Stripe │ │ Carte │
|
||||
└──────┬──────┘ └──────┬──────┘ └────┬─────┘ └────┬────┘
|
||||
│ │ │ │
|
||||
[1] │ Validation form │ │ │
|
||||
│ + montant CB │ │ │
|
||||
│ │ │ │
|
||||
[2] │ POST/PUT passage │ │ │
|
||||
│──────────────────>│ │ │
|
||||
│ │ │ │
|
||||
[3] │<──────────────────│ │ │
|
||||
│ Passage ID: 456 │ │ │
|
||||
│ │ │ │
|
||||
[4] │ POST create-intent│ │ │
|
||||
│──────────────────>│ (avec passage_id: 456) │
|
||||
│ │ │ │
|
||||
[5] │ │ Create PaymentIntent │
|
||||
│ │─────────────────>│ │
|
||||
│ │ │ │
|
||||
[6] │ │<─────────────────│ │
|
||||
│ │ pi_xxx + secret │ │
|
||||
│ │ │ │
|
||||
[7] │<──────────────────│ │ │
|
||||
│ PaymentIntent ID │ │ │
|
||||
│ │ │ │
|
||||
[8] │ SDK Terminal Init │ │ │
|
||||
│ "Approchez carte" │ │ │
|
||||
│ │ │ │
|
||||
[9] │<──────────────────────────────────────────────────────│
|
||||
│ NFC : Lecture carte sans contact │
|
||||
│ │ │ │
|
||||
[10] │ Process Payment │ │ │
|
||||
│───────────────────────────────────>│ │
|
||||
│ │ │ │
|
||||
[11] │<───────────────────────────────────│ │
|
||||
│ Payment Success │ │
|
||||
│ │ │ │
|
||||
[12] │ POST confirm │ │ │
|
||||
│──────────────────>│ │ │
|
||||
│ │ │ │
|
||||
[13] │ PUT passage/456 │ │ │
|
||||
│──────────────────>│ (ajout stripe_payment_id) │
|
||||
│ │ │ │
|
||||
[14] │<──────────────────│ │ │
|
||||
│ Passage updated │ │ │
|
||||
│ │ │ │
|
||||
┌─────────────┐ ┌─────────────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐
|
||||
│ App Flutter │ │ DeviceInfo │ │ API │ │ Stripe │ │ SDK Terminal │
|
||||
│ │ │ Service │ │ PHP │ │ │ │ │
|
||||
└──────┬──────┘ └────────┬────────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘
|
||||
│ │ │ │ │
|
||||
[1] │ Login utilisateur │ │ │ │
|
||||
│────────────────────>│ │ │ │
|
||||
│ │ │ │ │
|
||||
[2] │ │ checkStripeCertification() │ │
|
||||
│ │ • Android SDK ≥ 28 │ │
|
||||
│ │ • NFC disponible │ │
|
||||
│ │ │ │ │
|
||||
[3] │<────────────────────│ │ │ │
|
||||
│ ✅ Compatible │ │ │ │
|
||||
│ │ │ │ │
|
||||
[4] │ Validation form │ │ │ │
|
||||
│ + montant CB │ │ │ │
|
||||
│ │ │ │ │
|
||||
[5] │ POST/PUT passage │ │ │ │
|
||||
│────────────────────────────────────────>│ │ │
|
||||
│ │ │ │ │
|
||||
[6] │<────────────────────────────────────────│ │ │
|
||||
│ Passage ID: 456 │ │ │ │
|
||||
│ │ │ │ │
|
||||
[7] │ initialize() │ │ │ │
|
||||
│────────────────────────────────────────────────────────────────────────────>│
|
||||
│ │ │ │ │
|
||||
[8] │ │ │ │ Terminal.initTerminal()
|
||||
│ │ │ │ │ (fetchToken callback)
|
||||
│ │ │ │ │
|
||||
[9] │ │ │ POST /terminal/connection-token │
|
||||
│────────────────────────────────────────>│ │ │
|
||||
│ {amicale_id, stripe_account, location_id} │ │
|
||||
│ │ │ │ │
|
||||
[10] │ │ │ CreateConnectionToken │
|
||||
│ │ │───────────────>│ │
|
||||
│ │ │ │ │
|
||||
[11] │ │ │<───────────────│ │
|
||||
│ │ │ {secret: "..."}│ │
|
||||
│ │ │ │ │
|
||||
[12] │<────────────────────────────────────────│ │ │
|
||||
│ Connection Token │ │ │ │
|
||||
│ │ │ │ │
|
||||
[13] │────────────────────────────────────────────────────────────────────────────>│
|
||||
│ Token delivered to SDK │ │ ✅ SDK Ready │
|
||||
│ │ │ │ │
|
||||
[14] │ createPaymentIntent() │ │ │
|
||||
│────────────────────────────────────────>│ │ │
|
||||
│ {amount, passage_id, amicale_id} │ │ │
|
||||
│ │ │ │ │
|
||||
[15] │ │ │ Create PaymentIntent │
|
||||
│ │ │───────────────>│ │
|
||||
│ │ │ │ │
|
||||
[16] │ │ │<───────────────│ │
|
||||
│ │ │ pi_xxx + secret│ │
|
||||
│ │ │ │ │
|
||||
[17] │<────────────────────────────────────────│ │ │
|
||||
│ PaymentIntent ID │ │ │ │
|
||||
│ │ │ │ │
|
||||
[18] │ collectPayment() │ │ │ │
|
||||
│────────────────────────────────────────────────────────────────────────────>│
|
||||
│ │ │ │ │
|
||||
[19] │ │ │ │ discoverReaders()
|
||||
│ │ │ │ + connectReader()
|
||||
│ │ │ │ │
|
||||
[20] │ │ │ │ collectPaymentMethod()
|
||||
│ 💳 "Présentez la carte" │ │ ⏳ Attente NFC │
|
||||
│ │ │ │ │
|
||||
[21] │<──────────────────────────────────────────────────────────── 📱 TAP CARTE │
|
||||
│ │ │ │ │
|
||||
[22] │ confirmPayment() │ │ │ │
|
||||
│────────────────────────────────────────────────────────────────────────────>│
|
||||
│ │ │ │ │
|
||||
[23] │ │ │ │ confirmPaymentIntent()
|
||||
│ │ │ │ │
|
||||
[24] │ │ │ │ ✅ Succeeded │
|
||||
│<────────────────────────────────────────────────────────────────────────────│
|
||||
│ Payment Success │ │ │ │
|
||||
│ │ │ │ │
|
||||
[25] │ PUT passage/456 │ │ │ │
|
||||
│────────────────────────────────────────>│ │ │
|
||||
│ {stripe_payment_id: "pi_xxx"} │ │ │
|
||||
│ │ │ │ │
|
||||
[26] │<────────────────────────────────────────│ │ │
|
||||
│ ✅ Passage updated │ │ │ │
|
||||
```
|
||||
|
||||
### 🎮 Gestion du Terminal de Paiement
|
||||
@@ -378,6 +425,59 @@ Le terminal de paiement reste affiché jusqu'à la réponse définitive de Strip
|
||||
- **Persistence** : Le terminal reste ouvert jusqu'à réponse définitive de Stripe
|
||||
- **Gestion d'erreur** : Possibilité de réessayer sans perdre le contexte
|
||||
|
||||
### 🔑 Connection Token - Flow détaillé
|
||||
|
||||
Le Connection Token est crucial pour le SDK Stripe Terminal. Il est récupéré via un callback au moment de l'initialisation.
|
||||
|
||||
**Code côté App (stripe_tap_to_pay_service.dart:87-89) :**
|
||||
```dart
|
||||
await Terminal.initTerminal(
|
||||
fetchToken: _fetchConnectionToken, // Callback appelé automatiquement
|
||||
);
|
||||
```
|
||||
|
||||
**Callback de récupération (lignes 137-161) :**
|
||||
```dart
|
||||
Future<String> _fetchConnectionToken() async {
|
||||
debugPrint('🔑 Récupération du token de connexion Stripe...');
|
||||
|
||||
final response = await ApiService.instance.post(
|
||||
'/stripe/terminal/connection-token',
|
||||
data: {
|
||||
'amicale_id': CurrentAmicaleService.instance.amicaleId,
|
||||
'stripe_account': _stripeAccountId,
|
||||
'location_id': _locationId,
|
||||
}
|
||||
);
|
||||
|
||||
final token = response.data['secret'];
|
||||
if (token == null || token.isEmpty) {
|
||||
throw Exception('Token de connexion invalide');
|
||||
}
|
||||
|
||||
debugPrint('✅ Token de connexion récupéré');
|
||||
return token;
|
||||
}
|
||||
```
|
||||
|
||||
**Backend PHP :**
|
||||
```php
|
||||
// POST /stripe/terminal/connection-token
|
||||
$token = \Stripe\Terminal\ConnectionToken::create([], [
|
||||
'stripe_account' => $amicale->stripe_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'secret' => $token->secret,
|
||||
]);
|
||||
```
|
||||
|
||||
**Points importants :**
|
||||
- ✅ Le token est **temporaire** (valide quelques minutes)
|
||||
- ✅ Un nouveau token est créé à **chaque initialisation** du SDK
|
||||
- ✅ Le token est spécifique au **compte Stripe Connect** de l'amicale
|
||||
- ✅ Utilisé pour **authentifier** le Terminal SDK auprès de Stripe
|
||||
|
||||
### 📋 Détail des étapes
|
||||
|
||||
#### Étape 1 : VALIDATION DU FORMULAIRE
|
||||
@@ -1085,28 +1185,168 @@ POST/PUT /api/passages
|
||||
|
||||
## 🔄 GESTION DES ERREURS
|
||||
|
||||
### 📱 Erreurs Tap to Pay
|
||||
### 📱 Erreurs Tap to Pay - Messages utilisateur clairs
|
||||
|
||||
| Code erreur | Description | Action utilisateur |
|
||||
|-------------|-------------|-------------------|
|
||||
| `device_not_compatible` | iPhone non compatible | Afficher message explicatif |
|
||||
| `nfc_disabled` | NFC désactivé | Demander activation dans réglages |
|
||||
| `card_declined` | Carte refusée | Essayer autre carte |
|
||||
| `insufficient_funds` | Solde insuffisant | Essayer autre carte |
|
||||
| `network_error` | Erreur réseau | Réessayer ou mode offline |
|
||||
| `timeout` | Timeout lecture carte | Rapprocher carte et réessayer |
|
||||
L'application détecte automatiquement le type d'erreur et affiche un message adapté :
|
||||
|
||||
### 🔄 Flow de retry
|
||||
#### Gestion intelligente des erreurs (passage_form_dialog.dart)
|
||||
|
||||
```dart
|
||||
catch (e) {
|
||||
final errorMsg = e.toString().toLowerCase();
|
||||
String userMessage;
|
||||
bool shouldCancelPayment = true;
|
||||
|
||||
if (errorMsg.contains('canceled') || errorMsg.contains('cancelled')) {
|
||||
// Annulation volontaire
|
||||
userMessage = 'Paiement annulé';
|
||||
|
||||
} else if (errorMsg.contains('cardreadtimedout') || errorMsg.contains('timed out')) {
|
||||
// Timeout NFC avec conseils
|
||||
userMessage = 'Lecture de la carte impossible.\n\n'
|
||||
'Conseils :\n'
|
||||
'• Maintenez la carte contre le dos du téléphone\n'
|
||||
'• Ne bougez pas jusqu\'à confirmation\n'
|
||||
'• Retirez la coque si nécessaire\n'
|
||||
'• Essayez différentes positions sur le téléphone';
|
||||
|
||||
} else if (errorMsg.contains('already') && errorMsg.contains('payment')) {
|
||||
// PaymentIntent existe déjà
|
||||
userMessage = 'Un paiement est déjà en cours pour ce passage.\n'
|
||||
'Veuillez réessayer dans quelques instants.';
|
||||
shouldCancelPayment = false;
|
||||
}
|
||||
|
||||
// Annulation automatique du PaymentIntent pour permettre nouvelle tentative
|
||||
if (shouldCancelPayment && _paymentIntentId != null) {
|
||||
await StripeTapToPayService.instance.cancelPayment(_paymentIntentId!);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Table des erreurs et actions
|
||||
|
||||
| Type erreur | Message utilisateur | Action automatique |
|
||||
|-------------|--------------------|--------------------|
|
||||
| `canceled` / `cancelled` | "Paiement annulé" | Annulation PaymentIntent ✅ |
|
||||
| `cardReadTimedOut` | Message avec 4 conseils NFC | Annulation PaymentIntent ✅ |
|
||||
| `already payment` | "Paiement déjà en cours" | Pas d'annulation ⏳ |
|
||||
| `device_not_compatible` | "Appareil non compatible" | Annulation PaymentIntent ✅ |
|
||||
| `nfc_disabled` | "NFC désactivé" | Annulation PaymentIntent ✅ |
|
||||
| Autre erreur | Message technique complet | Annulation PaymentIntent ✅ |
|
||||
|
||||
### ⚠️ Contraintes NFC - Tap to Pay vs Google Pay
|
||||
|
||||
**Différence fondamentale :**
|
||||
- **Google Pay (émission)** : Le téléphone *émet* un signal NFC puissant → fonctionne avec coque
|
||||
- **Tap to Pay (réception)** : Le téléphone *lit* le signal de la carte → très sensible aux interférences
|
||||
|
||||
#### Coques problématiques
|
||||
- ❌ **Kevlar / Carbone** : Fibres conductrices perturbent la réception NFC
|
||||
- ❌ **Métal** : Bloque complètement les ondes NFC
|
||||
- ❌ **Coque épaisse** : Réduit la portée effective
|
||||
- ✅ **TPU / Silicone** : Compatible
|
||||
|
||||
#### Bonnes pratiques pour réussite NFC
|
||||
|
||||
**Position optimale :**
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 📱 Téléphone │
|
||||
│ │
|
||||
│ [Capteur NFC]│ ← Généralement vers le haut du dos
|
||||
│ │
|
||||
│ │
|
||||
│ 💳 Carte ici │ ← Maintenir immobile 3-5 secondes
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
**Checklist utilisateur :**
|
||||
1. ✅ Retirer la coque si échec
|
||||
2. ✅ Carte à plat contre le dos du téléphone
|
||||
3. ✅ Ne pas bouger pendant toute la lecture
|
||||
4. ✅ Essayer différentes positions (haut/milieu du téléphone)
|
||||
5. ✅ Carte sans contact activée (logo sans contact visible)
|
||||
|
||||
### 🔄 Flow de retry automatique
|
||||
|
||||
```
|
||||
1. Erreur détectée
|
||||
2. Message utilisateur explicite
|
||||
3. Option "Réessayer" proposée
|
||||
4. Conservation du montant et contexte
|
||||
5. Nouveau PaymentIntent si nécessaire
|
||||
6. Maximum 3 tentatives
|
||||
1. Erreur détectée → Analyse du type
|
||||
2. Annulation automatique PaymentIntent (si applicable)
|
||||
3. Message clair avec conseils contextuels
|
||||
4. Bouton "Réessayer" disponible
|
||||
5. Nouveau PaymentIntent créé automatiquement
|
||||
6. Conservation du contexte (montant, passage)
|
||||
```
|
||||
|
||||
**Avantages :**
|
||||
- ✅ Pas de blocage "PaymentIntent déjà existant"
|
||||
- ✅ Nombre illimité de tentatives
|
||||
- ✅ Contexte préservé (pas besoin de tout ressaisir)
|
||||
- ✅ Messages orientés solution plutôt qu'erreur technique
|
||||
|
||||
### 🏗️ Environnement et Build Release
|
||||
|
||||
#### Détection automatique de l'environnement
|
||||
|
||||
L'application détecte l'environnement via l'URL de l'API (plus fiable que `kDebugMode`) :
|
||||
|
||||
```dart
|
||||
// stripe_tap_to_pay_service.dart (lignes 236-252)
|
||||
Future<bool> _ensureReaderConnected() async {
|
||||
// Détection via URL API
|
||||
final apiUrl = ApiService.instance.baseUrl;
|
||||
final isProduction = apiUrl.contains('app3.geosector.fr');
|
||||
final isSimulated = !isProduction;
|
||||
|
||||
final config = TapToPayDiscoveryConfiguration(
|
||||
isSimulated: isSimulated,
|
||||
);
|
||||
|
||||
debugPrint('🔧 Environnement: ${isProduction ? "PRODUCTION (réel)" : "DEV/REC (simulé)"}');
|
||||
debugPrint('🔧 isSimulated: $isSimulated');
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping environnement :**
|
||||
| URL API | Environnement | Reader Stripe | Cartes acceptées |
|
||||
|---------|---------------|---------------|------------------|
|
||||
| `dapp.geosector.fr` | DEV | Simulé | Cartes test uniquement |
|
||||
| `rapp.geosector.fr` | REC | Simulé | Cartes test uniquement |
|
||||
| `app3.geosector.fr` | PROD | Réel | Cartes réelles uniquement |
|
||||
|
||||
#### ⚠️ Restriction Stripe - Build Release obligatoire en PROD
|
||||
|
||||
**Erreur si app debuggable en PROD :**
|
||||
```
|
||||
Debuggable applications are not supported when using the production
|
||||
version of the Tap to Pay reader. Please use a simulated version of
|
||||
the reader by setting TapToPayDiscoveryConfiguration.isSimulated to true.
|
||||
```
|
||||
|
||||
**Solution - Build release :**
|
||||
```bash
|
||||
# Build APK optimisé pour production
|
||||
flutter build apk --release
|
||||
|
||||
# Installation sur device
|
||||
adb install build/app/outputs/flutter-apk/app-release.apk
|
||||
```
|
||||
|
||||
**Différences debug vs release :**
|
||||
| Aspect | Debug (`flutter run`) | Release (`flutter build`) |
|
||||
|--------|-----------------------|--------------------|
|
||||
| **Optimisation** | ❌ Code non optimisé | ✅ R8/ProGuard activé |
|
||||
| **Taille APK** | ~200 MB | ~30-50 MB |
|
||||
| **Performance** | Lente (dev mode) | Rapide (optimisée) |
|
||||
| **Tap to Pay PROD** | ❌ Refusé par Stripe | ✅ Accepté |
|
||||
| **Débogage** | ✅ Hot reload, logs | ❌ Pas de hot reload |
|
||||
|
||||
**Pourquoi Stripe refuse les apps debug :**
|
||||
- **Sécurité renforcée** : Les apps debuggables peuvent être inspectées
|
||||
- **Conformité PCI-DSS** : Exigences de sécurité pour paiements réels
|
||||
- **Protection production** : Éviter utilisation accidentelle de readers réels en dev
|
||||
|
||||
---
|
||||
|
||||
## 📊 MONITORING ET LOGS
|
||||
|
||||
Reference in New Issue
Block a user