Fix: Corriger le type PDO dans StripeService et retirer getConnection()

This commit is contained in:
2025-09-01 15:23:48 +02:00
parent ece2f0006c
commit 0b541fbe4a
541 changed files with 123935 additions and 65116 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}

File diff suppressed because one or more lines are too long

View File

@@ -1944,6 +1944,7 @@ file:///home/pierre/dev/geosector/app/lib/core/services/hive_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
file:///home/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/logger_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/stripe_connect_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/theme_service.dart
file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1943,6 +1943,7 @@ file:///home/pierre/dev/geosector/app/lib/core/services/hive_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
file:///home/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/logger_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/stripe_connect_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/theme_service.dart
file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"packages": [
{
"name": "geosector_app",
"version": "3.2.0+320",
"version": "3.2.1+321",
"dependencies": [
"connectivity_plus",
"cupertino_icons",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,213 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
/// Service pour gérer Stripe Connect dans l'application
class StripeConnectService {
final ApiService apiService;
StripeConnectService({required this.apiService});
/// Créer un compte Stripe Connect pour une amicale
/// Retourne l'URL d'onboarding ou null si erreur
Future<String?> createStripeAccount(AmicaleModel amicale) async {
try {
debugPrint('🏢 Création compte Stripe pour ${amicale.name}...');
debugPrint(' Amicale ID: ${amicale.id}');
// 1. Créer le compte Stripe Connect via notre API
final createResponse = await apiService.post(
'/stripe/accounts',
data: {'fk_entite': amicale.id},
);
debugPrint(' Response status: ${createResponse.statusCode}');
debugPrint(' Response data: ${createResponse.data}');
if (createResponse.statusCode != 200 && createResponse.statusCode != 201) {
final error = createResponse.data?['message'] ?? 'Erreur création compte';
// Si le compte existe déjà, récupérer l'account_id
if (error.toString().contains('existe déjà')) {
final accountId = createResponse.data?['account_id'];
debugPrint(' Compte existant détecté, account_id: $accountId');
if (accountId != null) {
return await getOnboardingLink(accountId);
}
}
debugPrint('❌ Erreur création compte: $error');
throw Exception(error);
}
final accountId = createResponse.data?['account_id'];
debugPrint('✅ Compte créé/récupéré: $accountId');
if (accountId == null) {
throw Exception('account_id non retourné par l\'API');
}
// 2. Créer la Location pour Terminal/Tap to Pay
try {
await apiService.post(
'/stripe/locations',
data: {'fk_entite': amicale.id},
);
debugPrint('✅ Location Terminal créée');
} catch (e) {
debugPrint('⚠️ Erreur création Location (non bloquant): $e');
}
// 3. Obtenir le lien d'onboarding
return await getOnboardingLink(accountId);
} catch (e) {
debugPrint('❌ Erreur StripeConnect: $e');
return null;
}
}
/// Obtenir le lien d'onboarding pour finaliser la configuration
Future<String?> getOnboardingLink(String accountId) async {
try {
debugPrint('📋 Génération du lien d\'onboarding pour account: $accountId');
// URLs de retour après onboarding
const baseUrl = 'https://dapp.geosector.fr'; // À adapter selon l'environnement
final returnUrl = Uri.encodeFull('$baseUrl/stripe/success');
final refreshUrl = Uri.encodeFull('$baseUrl/stripe/refresh');
debugPrint(' Return URL: $returnUrl');
debugPrint(' Refresh URL: $refreshUrl');
final response = await apiService.post(
'/stripe/accounts/$accountId/onboarding-link',
data: {
'return_url': returnUrl,
'refresh_url': refreshUrl,
},
);
debugPrint(' Response status: ${response.statusCode}');
debugPrint(' Response data: ${response.data}');
if (response.statusCode == 200) {
final url = response.data['url'];
debugPrint('✅ Lien onboarding généré: $url');
return url;
}
final errorMessage = response.data?['message'] ?? 'Erreur inconnue';
throw Exception('Impossible de générer le lien: $errorMessage');
} catch (e) {
debugPrint('❌ Erreur onboarding link: $e');
return null;
}
}
/// Vérifier le statut du compte Stripe d'une amicale
Future<StripeAccountStatus> checkAccountStatus(int amicaleId) async {
try {
final response = await apiService.get('/stripe/accounts/$amicaleId/status');
if (response.statusCode == 200) {
final data = response.data;
return StripeAccountStatus(
hasAccount: data['has_account'] ?? false,
accountId: data['account_id'],
chargesEnabled: data['charges_enabled'] ?? false,
payoutsEnabled: data['payouts_enabled'] ?? false,
onboardingCompleted: data['onboarding_completed'] ?? false,
);
}
return StripeAccountStatus(hasAccount: false);
} catch (e) {
debugPrint('❌ Erreur vérification statut: $e');
return StripeAccountStatus(hasAccount: false);
}
}
/// Ouvrir le Dashboard Stripe pour l'amicale
Future<void> openStripeDashboard(String? accountId) async {
if (accountId == null || accountId.isEmpty) return;
// URL du dashboard Stripe Express
final url = Uri.parse('https://dashboard.stripe.com/express/$accountId');
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}
/// Lancer le processus d'onboarding dans un navigateur externe
Future<bool> launchOnboarding(String url) async {
try {
debugPrint('🚀 Lancement onboarding URL: $url');
debugPrint(' Plateforme: ${kIsWeb ? "Web" : "Mobile"}');
final uri = Uri.parse(url);
// Sur web, on peut directement lancer sans vérification
if (kIsWeb) {
final launched = await launchUrl(
uri,
webOnlyWindowName: '_blank', // Ouvre dans un nouvel onglet
);
debugPrint(launched ? '✅ URL lancée avec succès' : '❌ Échec du lancement');
return launched;
} else {
// Sur mobile, vérifier d'abord si on peut lancer l'URL
if (await canLaunchUrl(uri)) {
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
debugPrint('✅ URL lancée avec succès');
return true;
} else {
debugPrint('❌ Impossible de lancer l\'URL sur mobile');
}
}
} catch (e) {
debugPrint('❌ Erreur lancement onboarding: $e');
debugPrint(' Détails: $e');
}
return false;
}
}
/// Statut d'un compte Stripe Connect
class StripeAccountStatus {
final bool hasAccount;
final String? accountId;
final bool chargesEnabled;
final bool payoutsEnabled;
final bool onboardingCompleted;
StripeAccountStatus({
required this.hasAccount,
this.accountId,
this.chargesEnabled = false,
this.payoutsEnabled = false,
this.onboardingCompleted = false,
});
bool get canAcceptPayments => chargesEnabled && payoutsEnabled;
String get statusMessage {
if (!hasAccount) return 'Pas de compte Stripe';
if (!onboardingCompleted) return 'Configuration en cours';
if (!chargesEnabled) return 'Paiements non activés';
if (!payoutsEnabled) return 'Virements non activés';
return 'Compte actif';
}
Color get statusColor {
if (canAcceptPayments) return Colors.green;
if (hasAccount) return Colors.orange;
return Colors.red;
}
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/stripe_connect_service.dart';
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:image_picker/image_picker.dart';
@@ -66,6 +67,11 @@ class _AmicaleFormState extends State<AmicaleForm> {
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
String? _logoUrl;
// Pour Stripe Connect
StripeConnectService? _stripeService;
bool _isCheckingStripeStatus = false;
StripeAccountStatus? _stripeStatus;
@override
void initState() {
@@ -97,6 +103,159 @@ class _AmicaleFormState extends State<AmicaleForm> {
_chkUserDeletePass = amicale?.chkUserDeletePass ?? false;
// Note : Le logo sera chargé dynamiquement depuis l'API
// Initialiser le service Stripe si API disponible
if (widget.apiService != null) {
_stripeService = StripeConnectService(apiService: widget.apiService!);
// Vérifier le statut Stripe si l'amicale a déjà un compte
if (_chkStripe && widget.amicale != null) {
_checkStripeStatus();
}
}
}
// Vérifier le statut du compte Stripe
Future<void> _checkStripeStatus() async {
if (_stripeService == null || widget.amicale == null) return;
setState(() => _isCheckingStripeStatus = true);
try {
final status = await _stripeService!.checkAccountStatus(widget.amicale!.id);
setState(() {
_stripeStatus = status;
if (status.accountId != null) {
_stripeIdController.text = status.accountId!;
}
});
} catch (e) {
debugPrint('Erreur vérification statut Stripe: $e');
} finally {
setState(() => _isCheckingStripeStatus = false);
}
}
// Configurer Stripe Connect
Future<void> _configureStripe() async {
if (_stripeService == null || widget.amicale == null) return;
// Vérifier que nous sommes sur Web
if (!kIsWeb) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Configuration Web requise'),
content: const Text(
'La configuration du compte Stripe doit être effectuée depuis un navigateur web.\n\n'
'Veuillez vous connecter depuis un ordinateur pour configurer les paiements par carte bancaire.',
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
return;
}
// Afficher un dialog de confirmation
final bool? confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Configuration Stripe'),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vous allez être redirigé vers Stripe pour :'),
SizedBox(height: 8),
Text('• Créer votre compte marchand'),
Text('• Configurer vos informations bancaires'),
Text('• Activer les paiements par carte'),
SizedBox(height: 16),
Text('Ce processus prend environ 5-10 minutes.'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Continuer'),
),
],
),
);
if (confirm != true) return;
// Afficher le loading
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Préparation de votre compte Stripe...'),
],
),
),
);
try {
// Créer ou récupérer le lien d'onboarding
final url = await _stripeService!.createStripeAccount(widget.amicale!);
// Fermer le loading
if (mounted) Navigator.of(context).pop();
if (url != null) {
// Lancer l'onboarding
final success = await _stripeService!.launchOnboarding(url);
if (success) {
// Activer la checkbox
setState(() => _chkStripe = true);
// Afficher un message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Configuration Stripe lancée. Revenez ici après avoir terminé.'),
duration: Duration(seconds: 5),
),
);
}
// Vérifier le statut après un délai
Future.delayed(const Duration(seconds: 5), _checkStripeStatus);
}
} else {
throw Exception('Impossible de créer le lien de configuration');
}
} catch (e) {
// Fermer le loading si encore ouvert
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
// Afficher l'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
@@ -744,7 +903,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
}
// Construire le formulaire principal
Widget _buildMainForm(ThemeData theme, bool restrictedFieldsReadOnly) {
Widget _buildMainForm(ThemeData theme, bool restrictedFieldsReadOnly, bool stripeReadOnly) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -976,78 +1135,133 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
const SizedBox(height: 16),
// Stripe Checkbox et Stripe ID sur la même ligne
Row(
crossAxisAlignment: CrossAxisAlignment.center,
// Stripe Checkbox et configuration
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Checkbox Stripe
Checkbox(
value: _chkStripe,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
if (value == true) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmation'),
content: const Text(
'En acceptant les règlements par carte bancaire, des commissions de 1.4% seront prélevées sur les montants encaissés. Souhaitez-vous continuer ?'),
actions: [
TextButton(
onPressed: () {
// L'utilisateur a répondu "non"
setState(() {
_chkStripe = false;
});
Navigator.of(context).pop();
},
child: const Text('Non'),
),
ElevatedButton(
onPressed: () {
// L'utilisateur a répondu "oui"
setState(() {
_chkStripe = true;
});
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF20335E),
foregroundColor: Colors.white,
),
child: const Text('Oui'),
),
],
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Checkbox Stripe
Checkbox(
value: _chkStripe,
onChanged: stripeReadOnly
? null
: (value) {
setState(() {
_chkStripe = value ?? false;
});
// Si on active la checkbox et qu'on a une amicale, vérifier le statut
if (value == true && widget.amicale != null) {
_checkStripeStatus();
}
},
activeColor: const Color(0xFF20335E),
),
Text(
"Accepte les règlements en CB",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 16),
// Bouton Configuration Stripe si checkbox cochée
if (_chkStripe && !stripeReadOnly && widget.amicale != null) ...[
ElevatedButton.icon(
onPressed: _isCheckingStripeStatus ? null : _configureStripe,
icon: _isCheckingStripeStatus
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Icon(kIsWeb ? Icons.settings : Icons.computer, size: 18),
label: Text(
!kIsWeb
? 'Config. Web requise'
: (_stripeStatus?.canAcceptPayments == true
? 'Compte actif'
: 'Configurer Stripe')
),
style: ElevatedButton.styleFrom(
backgroundColor: !kIsWeb
? Colors.grey
: (_stripeStatus?.statusColor ?? Colors.orange),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
const SizedBox(width: 8),
// Indicateur de statut
if (_stripeStatus != null)
Tooltip(
message: _stripeStatus!.statusMessage,
child: Icon(
_stripeStatus!.canAcceptPayments
? Icons.check_circle
: Icons.warning_amber_rounded,
color: _stripeStatus!.statusColor,
size: 20,
),
),
],
],
),
// Stripe ID (sur une ligne séparée)
const SizedBox(height: 8),
CustomTextField(
controller: _stripeIdController,
label: "ID Compte Stripe",
readOnly: true,
helperText: _chkStripe
? (_stripeStatus?.accountId != null
? "Compte Stripe Connect: ${_stripeStatus!.accountId}"
: "L'ID sera généré automatiquement lors de la configuration")
: "Activez les paiements CB pour configurer Stripe",
),
// Message d'information sur les commissions
if (_chkStripe)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _stripeStatus?.statusColor.withOpacity(0.1) ?? Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _stripeStatus?.statusColor.withOpacity(0.3) ?? Colors.orange.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
_stripeStatus?.canAcceptPayments == true
? Icons.check_circle_outline
: Icons.info_outline,
color: _stripeStatus?.statusColor ?? Colors.orange,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_stripeStatus?.canAcceptPayments == true
? "✅ Compte Stripe configuré - Commission plateforme: 2.5% (min 0.50€)"
: _stripeStatus?.onboardingCompleted == false
? "⏳ Configuration Stripe en cours. Veuillez compléter le processus d'onboarding."
: "💳 Les paiements CB seront soumis à une commission de 2.5% (min 0.50€)",
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
),
);
} else {
// Si l'utilisateur décoche la case, pas besoin de confirmation
setState(() {
_chkStripe = false;
});
}
},
activeColor: const Color(0xFF20335E),
),
Text(
"Accepte les règlements en CB",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
const SizedBox(width: 16),
// Stripe ID
Expanded(
child: CustomTextField(
controller: _stripeIdController,
label: "ID Stripe Paiements CB",
readOnly: restrictedFieldsReadOnly,
helperText: "Les règlements par CB sont taxés d'une commission de 1.4%",
),
),
],
),
const SizedBox(height: 16),
@@ -1251,9 +1465,15 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Déterminer si l'utilisateur peut modifier les champs restreints
final bool canEditRestrictedFields = userRole > 2;
// Pour Stripe, les admins d'amicale (rôle 2) peuvent aussi configurer
final bool canEditStripe = userRole >= 2;
// Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits
final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields;
// Lecture seule spécifique pour Stripe
final bool stripeReadOnly = widget.readOnly || !canEditStripe;
// Calculer la largeur maximale du formulaire pour les écrans larges
final screenWidth = MediaQuery.of(context).size.width;
@@ -1281,7 +1501,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
const SizedBox(height: 24),
// Formulaire principal
_buildMainForm(theme, restrictedFieldsReadOnly),
_buildMainForm(theme, restrictedFieldsReadOnly, stripeReadOnly),
],
),
),

View File

@@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/pierre/dev/flutter
FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=3.2.0
FLUTTER_BUILD_NUMBER=320
FLUTTER_BUILD_NAME=3.2.1
FLUTTER_BUILD_NUMBER=321
FLUTTER_CLI_BUILD_MODE=debug
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true

View File

@@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/pierre/dev/flutter"
export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=3.2.0"
export "FLUTTER_BUILD_NUMBER=320"
export "FLUTTER_BUILD_NAME=3.2.1"
export "FLUTTER_BUILD_NUMBER=321"
export "FLUTTER_CLI_BUILD_MODE=debug"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"

View File

@@ -1,7 +1,7 @@
name: geosector_app
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
publish_to: 'none'
version: 3.2.0+320
version: 3.2.1+321
environment:
sdk: '>=3.0.0 <4.0.0'