- 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>
1104 lines
55 KiB
Dart
Executable File
1104 lines
55 KiB
Dart
Executable File
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
|
import 'package:go_router/go_router.dart';
|
|
import 'dart:math' as math;
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:geosector_app/presentation/widgets/custom_button.dart';
|
|
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
|
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
|
|
import 'package:geosector_app/core/services/app_info_service.dart';
|
|
import 'package:geosector_app/core/services/hive_service.dart';
|
|
import 'package:geosector_app/core/constants/app_keys.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
|
|
|
class RegisterPage extends StatefulWidget {
|
|
const RegisterPage({super.key});
|
|
|
|
@override
|
|
State<RegisterPage> createState() => _RegisterPageState();
|
|
}
|
|
|
|
// Class pour dessiner les petits points blancs sur le fond
|
|
class DotsPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = Colors.white.withValues(alpha: 0.5)
|
|
..style = PaintingStyle.fill;
|
|
|
|
final random = math.Random(42); // Seed fixe pour consistance
|
|
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
|
|
for (int i = 0; i < numberOfDots; i++) {
|
|
final x = random.nextDouble() * size.width;
|
|
final y = random.nextDouble() * size.height;
|
|
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
// Modèle pour les villes
|
|
class City {
|
|
final String name;
|
|
final String postalCode;
|
|
|
|
City({required this.name, required this.postalCode});
|
|
|
|
factory City.fromJson(Map<String, dynamic> json) {
|
|
return City(
|
|
name: json['nom'] ?? '',
|
|
postalCode: json['codePostal'] ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RegisterPageState extends State<RegisterPage> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nameController = TextEditingController();
|
|
final _amicaleNameController = TextEditingController();
|
|
final _postalCodeController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _captchaController = TextEditingController();
|
|
String _appVersion = '';
|
|
|
|
// Valeur cachée pour le test anti-robot
|
|
final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
// Valeurs pour le captcha simple
|
|
final int _captchaNum1 =
|
|
2 + (DateTime.now().second % 5); // Nombre entre 2 et 6
|
|
final int _captchaNum2 =
|
|
3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
|
|
|
|
// État de la connexion Internet et de la plateforme
|
|
bool _isConnected = false;
|
|
bool _isMobile = false;
|
|
String _connectionType = '';
|
|
bool _isLoading = false; // État de chargement local
|
|
|
|
// Liste des villes correspondant au code postal
|
|
List<City> _cities = [];
|
|
City? _selectedCity;
|
|
bool _isLoadingCities = false;
|
|
|
|
Future<void> _getAppVersion() async {
|
|
try {
|
|
final packageInfo = await PackageInfo.fromPlatform();
|
|
if (mounted) {
|
|
setState(() {
|
|
_appVersion = packageInfo.version;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération de la version: $e');
|
|
// Fallback sur la version du AppInfoService si elle existe
|
|
if (mounted) {
|
|
setState(() {
|
|
_appVersion = AppInfoService.fullVersion
|
|
.split(' ')
|
|
.last; // Extraire juste le numéro
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// VÉRIFICATION CRITIQUE : S'assurer que Hive est initialisé correctement
|
|
// Vérifier la clé 'hive_initialized' dans la box settings
|
|
try {
|
|
// D'abord vérifier que les boxes sont disponibles
|
|
if (!HiveService.instance.areBoxesInitialized()) {
|
|
debugPrint('⚠️ RegisterPage: Boxes Hive non initialisées, redirection vers SplashPage');
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
context.go('/?action=register');
|
|
}
|
|
});
|
|
return; // IMPORTANT : Arrêter l'exécution du reste de initState
|
|
}
|
|
|
|
// Ensuite vérifier la clé de réinitialisation
|
|
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
final isInitialized = settingsBox.get('hive_initialized', defaultValue: false);
|
|
|
|
if (isInitialized != true) {
|
|
debugPrint('⚠️ RegisterPage: Réinitialisation Hive requise (hive_initialized=$isInitialized)');
|
|
|
|
// Forcer une réinitialisation complète via SplashPage
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
context.go('/?action=register');
|
|
}
|
|
});
|
|
return; // IMPORTANT : Arrêter l'exécution du reste de initState
|
|
}
|
|
|
|
debugPrint('✅ RegisterPage: Hive correctement initialisé');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ RegisterPage: Erreur lors de la vérification de hive_initialized: $e');
|
|
// En cas d'erreur, forcer la réinitialisation
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
context.go('/?action=register');
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Déterminer si l'application s'exécute sur mobile
|
|
_isMobile = !kIsWeb;
|
|
|
|
// Initialiser l'état de la connexion
|
|
_checkConnectivity();
|
|
|
|
// Récupérer la version de l'application
|
|
_getAppVersion();
|
|
|
|
// Écouter les changements du code postal
|
|
_postalCodeController.addListener(_onPostalCodeChanged);
|
|
}
|
|
|
|
// Méthode pour vérifier la connectivité
|
|
Future<void> _checkConnectivity() async {
|
|
await connectivityService.checkConnectivity();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isConnected = connectivityService.isConnected;
|
|
_connectionType = connectivityService.connectionType;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Méthode appelée lorsque le code postal change
|
|
void _onPostalCodeChanged() {
|
|
final postalCode = _postalCodeController.text;
|
|
|
|
// Réinitialiser la ville sélectionnée si le code postal change
|
|
setState(() {
|
|
_selectedCity = null;
|
|
});
|
|
|
|
// Si le code postal a au moins 3 chiffres, rechercher les villes correspondantes
|
|
if (postalCode.length >= 3) {
|
|
_fetchCities(postalCode);
|
|
} else {
|
|
setState(() {
|
|
_cities = [];
|
|
});
|
|
}
|
|
}
|
|
|
|
// Méthode pour récupérer les villes correspondant au code postal
|
|
Future<void> _fetchCities(String postalCode) async {
|
|
if (!_isConnected) return;
|
|
|
|
setState(() {
|
|
_isLoadingCities = true;
|
|
});
|
|
|
|
try {
|
|
// Utiliser l'API interne de geosector pour récupérer les villes par code postal
|
|
final baseUrl = Uri
|
|
.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
|
|
final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode';
|
|
|
|
final response = await http.get(
|
|
Uri.parse(apiUrl),
|
|
headers: {'Content-Type': 'application/json'},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
final Map<String, dynamic> responseData = json.decode(response.body);
|
|
|
|
// Vérifier si la réponse contient des données
|
|
if (responseData['success'] == true && responseData['data'] != null) {
|
|
final List<dynamic> data = responseData['data'];
|
|
|
|
setState(() {
|
|
_cities = data
|
|
.map((city) => City(
|
|
name: city['nom'] ?? '',
|
|
postalCode: city['code_postal'] ?? postalCode,
|
|
))
|
|
.toList();
|
|
|
|
_isLoadingCities = false;
|
|
|
|
// Ne pas présélectionner automatiquement la première ville
|
|
// Laisser l'utilisateur saisir son code postal complet et choisir une ville
|
|
_selectedCity = null;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_cities = [];
|
|
_isLoadingCities = false;
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
_cities = [];
|
|
_isLoadingCities = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la récupération des villes: $e');
|
|
setState(() {
|
|
_cities = [];
|
|
_isLoadingCities = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_amicaleNameController.dispose();
|
|
_postalCodeController.removeListener(_onPostalCodeChanged);
|
|
_postalCodeController.dispose();
|
|
_emailController.dispose();
|
|
_captchaController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Utiliser l'instance globale de userRepository définie dans app.dart
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
// Fond dégradé avec petits points blancs
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 500),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.white, Colors.blue.shade300],
|
|
),
|
|
),
|
|
child: CustomPaint(
|
|
painter: DotsPainter(),
|
|
child: const SizedBox(
|
|
width: double.infinity, height: double.infinity),
|
|
),
|
|
),
|
|
SafeArea(
|
|
child: Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 500),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Logo et titre
|
|
Image.asset(
|
|
'assets/images/logo-geosector-1024.png',
|
|
height: 140,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Inscription Administrateur',
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Enregistrez votre amicale sur GeoSector',
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Indicateur de connectivité
|
|
ConnectivityIndicator(
|
|
onConnectivityChanged: (isConnected) {
|
|
if (mounted && _isConnected != isConnected) {
|
|
setState(() {
|
|
_isConnected = isConnected;
|
|
_connectionType =
|
|
connectivityService.connectionType;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
|
|
// Message d'avertissement pour les utilisateurs mobiles sans connexion
|
|
if (_isMobile && !_isConnected)
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 16),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.error.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: theme.colorScheme.error.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.signal_wifi_off,
|
|
color: theme.colorScheme.error,
|
|
size: 32,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Connexion Internet requise',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous inscrire.',
|
|
style: theme.textTheme.bodyMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
await _checkConnectivity();
|
|
if (_isConnected && context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Connexion Internet $_connectionType détectée.'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Vérifier à nouveau'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: theme.colorScheme.primary,
|
|
foregroundColor: theme.colorScheme.onPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Formulaire d'inscription
|
|
Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
CustomTextField(
|
|
controller: _nameController,
|
|
label: 'Nom complet',
|
|
hintText: 'Entrez votre nom complet',
|
|
prefixIcon: Icons.person_outline,
|
|
isRequired: true,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer votre nom complet';
|
|
}
|
|
if (value.length < 5) {
|
|
return 'Le nom doit contenir au moins 5 caractères';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _emailController,
|
|
label: 'Email',
|
|
hintText: 'Entrez votre email',
|
|
prefixIcon: Icons.email_outlined,
|
|
keyboardType: TextInputType.emailAddress,
|
|
isRequired: true,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer votre email';
|
|
}
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
|
.hasMatch(value)) {
|
|
return 'Veuillez entrer un email valide';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _amicaleNameController,
|
|
label: 'Nom de l\'amicale',
|
|
hintText: 'Entrez le nom de votre amicale',
|
|
prefixIcon: Icons.local_fire_department,
|
|
isRequired: true,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer le nom de votre amicale';
|
|
}
|
|
if (value.length < 5) {
|
|
return 'Le nom de l\'amicale doit contenir au moins 5 caractères';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
CustomTextField(
|
|
controller: _postalCodeController,
|
|
label: 'Code postal de l\'amicale',
|
|
hintText:
|
|
'Entrez le code postal de votre amicale',
|
|
prefixIcon: Icons.location_on_outlined,
|
|
keyboardType: TextInputType.number,
|
|
isRequired: true,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(5),
|
|
],
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez entrer votre code postal';
|
|
}
|
|
if (!RegExp(r'^[0-9]{5}$').hasMatch(value)) {
|
|
return 'Le code postal doit contenir 5 chiffres';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Sélection de la commune
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'Commune de l\'amicale',
|
|
style:
|
|
theme.textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
const Text(
|
|
' •',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: const Color(0xFFECEFF1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: _isLoadingCities
|
|
? const Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 16),
|
|
child: Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
)
|
|
: DropdownButtonFormField<City>(
|
|
initialValue: _selectedCity,
|
|
decoration: InputDecoration(
|
|
prefixIcon: Icon(
|
|
Icons.location_city_outlined,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
hintText: _postalCodeController
|
|
.text.length <
|
|
3
|
|
? 'Entrez d\'abord au moins 3 chiffres du code postal'
|
|
: _cities.isEmpty
|
|
? 'Aucune commune trouvée pour ce code postal'
|
|
: 'Sélectionnez une commune',
|
|
border: OutlineInputBorder(
|
|
borderRadius:
|
|
BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 16,
|
|
),
|
|
),
|
|
items: _cities.map((City city) {
|
|
return DropdownMenuItem<City>(
|
|
value: city,
|
|
child: Text(city.name),
|
|
);
|
|
}).toList(),
|
|
onChanged: (City? newValue) {
|
|
setState(() {
|
|
_selectedCity = newValue;
|
|
|
|
// Mettre à jour le code postal avec celui de la ville sélectionnée
|
|
if (newValue != null) {
|
|
// Désactiver temporairement le listener pour éviter une boucle infinie
|
|
_postalCodeController
|
|
.removeListener(
|
|
_onPostalCodeChanged);
|
|
|
|
// Mettre à jour le code postal
|
|
_postalCodeController.text =
|
|
newValue.postalCode;
|
|
|
|
// Réactiver le listener
|
|
_postalCodeController
|
|
.addListener(
|
|
_onPostalCodeChanged);
|
|
}
|
|
});
|
|
},
|
|
validator: (value) {
|
|
if (value == null) {
|
|
return 'Veuillez sélectionner une commune';
|
|
}
|
|
return null;
|
|
},
|
|
isExpanded: true,
|
|
icon: Icon(
|
|
Icons.arrow_drop_down,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
dropdownColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Test anti-robot (captcha simple)
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Vérification de sécurité',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w500,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
CustomTextField(
|
|
controller: _captchaController,
|
|
label:
|
|
'Combien font $_captchaNum1 + $_captchaNum2 ?',
|
|
hintText: 'Entrez le résultat',
|
|
prefixIcon: Icons.security,
|
|
keyboardType: TextInputType.number,
|
|
isRequired: true,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Veuillez répondre à cette question';
|
|
}
|
|
final int? answer = int.tryParse(value);
|
|
if (answer == null) {
|
|
return 'Veuillez entrer un nombre';
|
|
}
|
|
if (answer != _captchaNum1 + _captchaNum2) {
|
|
return 'La réponse est incorrecte';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
|
|
// Champ caché pour le token anti-robot
|
|
// Ce champ ne sera pas visible mais sera envoyé avec le formulaire
|
|
Opacity(
|
|
opacity: 0,
|
|
child: SizedBox(
|
|
height: 0,
|
|
child: TextFormField(
|
|
initialValue: _hiddenToken,
|
|
enabled: false,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Bouton d'inscription
|
|
CustomButton(
|
|
onPressed: (_isLoading ||
|
|
(_isMobile && !_isConnected))
|
|
? null
|
|
: () async {
|
|
if (_formKey.currentState!.validate()) {
|
|
// Vérifier la connexion Internet avant de soumettre
|
|
// Utiliser l'instance globale de connectivityService définie dans app.dart
|
|
await connectivityService
|
|
.checkConnectivity();
|
|
|
|
if (!connectivityService.isConnected) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(
|
|
SnackBar(
|
|
content: const Text(
|
|
'Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
|
|
backgroundColor:
|
|
theme.colorScheme.error,
|
|
duration:
|
|
const Duration(seconds: 3),
|
|
action: SnackBarAction(
|
|
label: 'Réessayer',
|
|
onPressed: () async {
|
|
await connectivityService
|
|
.checkConnectivity();
|
|
if (connectivityService
|
|
.isConnected &&
|
|
context.mounted) {
|
|
ScaffoldMessenger.of(
|
|
context)
|
|
.showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Connexion Internet ${connectivityService.connectionType} détectée.'),
|
|
backgroundColor:
|
|
Colors.green,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
// Vérifier que le captcha est correct
|
|
final int? captchaAnswer = int.tryParse(
|
|
_captchaController.text);
|
|
if (captchaAnswer !=
|
|
_captchaNum1 + _captchaNum2) {
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'La vérification de sécurité a échoué. Veuillez réessayer.'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Préparer les données du formulaire
|
|
final Map<String, dynamic> formData = {
|
|
'email': _emailController.text.trim(),
|
|
'name': _nameController.text.trim(),
|
|
'amicale_name': _amicaleNameController
|
|
.text
|
|
.trim(),
|
|
'postal_code':
|
|
_postalCodeController.text,
|
|
'city_name':
|
|
_selectedCity?.name ?? '',
|
|
'captcha_answer': captchaAnswer,
|
|
'captcha_expected':
|
|
_captchaNum1 + _captchaNum2,
|
|
'token': _hiddenToken,
|
|
};
|
|
|
|
// Afficher un indicateur de chargement
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
try {
|
|
// Envoyer les données à l'API
|
|
final baseUrl = Uri.base.origin;
|
|
final apiUrl =
|
|
'$baseUrl/api/register';
|
|
|
|
final response = await http.post(
|
|
Uri.parse(apiUrl),
|
|
headers: {
|
|
'Content-Type':
|
|
'application/json',
|
|
},
|
|
body: json.encode(formData),
|
|
);
|
|
|
|
// Masquer l'indicateur de chargement
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
|
|
// Traiter la réponse
|
|
if (response.statusCode == 200 ||
|
|
response.statusCode == 201) {
|
|
final responseData =
|
|
json.decode(response.body);
|
|
|
|
// Vérifier si la réponse indique un succès
|
|
final bool isSuccess =
|
|
responseData['success'] ==
|
|
true ||
|
|
responseData['status'] ==
|
|
'success';
|
|
|
|
// Récupérer le message de la réponse
|
|
final String message = responseData[
|
|
'message'] ??
|
|
(isSuccess
|
|
? 'Inscription réussie !'
|
|
: 'Échec de l\'inscription. Veuillez réessayer.');
|
|
|
|
if (isSuccess) {
|
|
if (context.mounted) {
|
|
// Afficher une boîte de dialogue de succès
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible:
|
|
false, // L'utilisateur doit cliquer sur OK
|
|
builder:
|
|
(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Row(
|
|
children: [
|
|
Icon(
|
|
Icons.check_circle,
|
|
color: Colors.green,
|
|
),
|
|
SizedBox(width: 10),
|
|
Text(
|
|
'Inscription réussie'),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize:
|
|
MainAxisSize.min,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment
|
|
.start,
|
|
children: [
|
|
Text(
|
|
'Votre demande d\'inscription a été enregistrée avec succès.',
|
|
style: theme
|
|
.textTheme
|
|
.bodyLarge,
|
|
),
|
|
const SizedBox(
|
|
height: 16),
|
|
Text(
|
|
'Vous allez recevoir un email contenant :',
|
|
style: theme
|
|
.textTheme
|
|
.bodyMedium,
|
|
),
|
|
const SizedBox(
|
|
height: 8),
|
|
Row(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment
|
|
.start,
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.arrow_right,
|
|
size: 20,
|
|
color: theme
|
|
.colorScheme
|
|
.primary),
|
|
const SizedBox(
|
|
width: 4),
|
|
const Expanded(
|
|
child: Text(
|
|
'Votre identifiant de connexion'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 4),
|
|
Row(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment
|
|
.start,
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.arrow_right,
|
|
size: 20,
|
|
color: theme
|
|
.colorScheme
|
|
.primary),
|
|
const SizedBox(
|
|
width: 4),
|
|
const Expanded(
|
|
child: Text(
|
|
'Un lien pour définir votre mot de passe'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 16),
|
|
Text(
|
|
'Vérifiez votre boîte de réception et vos spams.',
|
|
style: TextStyle(
|
|
fontStyle:
|
|
FontStyle
|
|
.italic,
|
|
color: theme
|
|
.colorScheme
|
|
.onSurface
|
|
.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(
|
|
context)
|
|
.pop();
|
|
// Rediriger vers splash avec redirection automatique vers login admin
|
|
context
|
|
.go('/?action=login&type=admin');
|
|
},
|
|
style: TextButton
|
|
.styleFrom(
|
|
foregroundColor:
|
|
theme
|
|
.colorScheme
|
|
.primary,
|
|
textStyle:
|
|
const TextStyle(
|
|
fontWeight:
|
|
FontWeight
|
|
.bold),
|
|
),
|
|
child:
|
|
const Text('OK'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
// Afficher le message d'erreur retourné par l'API
|
|
if (context.mounted) {
|
|
// Afficher un message d'erreur plus visible
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text(
|
|
'Erreur d\'inscription'),
|
|
content: Text(message),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(
|
|
context)
|
|
.pop();
|
|
},
|
|
child:
|
|
const Text('OK'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
// Afficher également un SnackBar
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Gérer les erreurs HTTP
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l'inscription"}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Masquer l'indicateur de chargement
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
|
|
// Gérer les exceptions
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Erreur: ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
text: (_isMobile && !_isConnected)
|
|
? 'Connexion Internet requise'
|
|
: 'Enregistrer mon amicale',
|
|
isLoading: _isLoading,
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Déjà un compte
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Déjà un compte ?',
|
|
style: theme.textTheme.bodyMedium,
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
context.go('/?action=login&type=admin');
|
|
},
|
|
child: Text(
|
|
'Se connecter',
|
|
style: TextStyle(
|
|
color: theme.colorScheme.primary,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Lien vers le site web
|
|
TextButton(
|
|
onPressed: () {
|
|
// Déterminer l'URL du site web en fonction de l'environnement
|
|
String webUrl = 'https://geosector.fr';
|
|
|
|
if (kIsWeb) {
|
|
final host = Uri.base.host;
|
|
if (host.startsWith('dapp.')) {
|
|
webUrl = 'https://dev.geosector.fr';
|
|
} else if (host.startsWith('rapp.')) {
|
|
webUrl = 'https://rec.geosector.fr';
|
|
} else if (host.startsWith('app.')) {
|
|
webUrl = 'https://geosector.fr';
|
|
}
|
|
}
|
|
|
|
// Ouvrir l'URL dans une nouvelle fenêtre/onglet
|
|
launchUrl(
|
|
Uri.parse(webUrl),
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
},
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.language,
|
|
size: 16,
|
|
color: Colors.blue,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'Revenir sur le site web',
|
|
style: TextStyle(
|
|
color: Colors.blue,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Badge de version en bas à droite
|
|
if (_appVersion.isNotEmpty)
|
|
Positioned(
|
|
bottom: 16,
|
|
right: 16,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.primary.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: theme.colorScheme.primary.withValues(alpha: 0.3),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Text(
|
|
'v$_appVersion',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.primary.withValues(alpha: 0.8),
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|