Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,489 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _emailSent = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: _emailSent ? _buildSuccessView() : _buildFormView(),
),
),
);
},
),
),
);
}
Widget _buildFormView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildInstructions(),
const SizedBox(height: 32),
_buildForm(),
const SizedBox(height: 32),
_buildSendButton(),
const SizedBox(height: 24),
_buildBackToLogin(),
],
);
}
Widget _buildSuccessView() {
return Column(
children: [
const SizedBox(height: 60),
// Icône de succès
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
border: Border.all(
color: AppTheme.successColor.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.mark_email_read_rounded,
size: 60,
color: AppTheme.successColor,
),
),
const SizedBox(height: 32),
// Titre de succès
const Text(
'Email envoyé !',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Message de succès
const Text(
'Nous avons envoyé un lien de réinitialisation à :',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_emailController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Instructions
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.infoColor.withOpacity(0.2),
),
),
child: const Column(
children: [
Icon(
Icons.info_outline,
color: AppTheme.infoColor,
size: 24,
),
SizedBox(height: 12),
Text(
'Instructions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 8),
Text(
'1. Vérifiez votre boîte email (et vos spams)\n'
'2. Cliquez sur le lien de réinitialisation\n'
'3. Créez un nouveau mot de passe sécurisé',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
// Boutons d'action
Column(
children: [
LoadingButton(
onPressed: _handleResendEmail,
text: 'Renvoyer l\'email',
width: double.infinity,
height: 48,
backgroundColor: AppTheme.secondaryColor,
),
const SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Retour à la connexion',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.warningColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.lock_reset_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
const Text(
'Pas de problème ! Nous allons vous aider à le récupérer.',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildInstructions() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comment ça marche ?',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'Saisissez votre email et nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe.',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.4,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
validator: _validateEmail,
onFieldSubmitted: (_) => _handleSendResetEmail(),
autofocus: true,
),
);
}
Widget _buildSendButton() {
return LoadingButton(
onPressed: _handleSendResetEmail,
isLoading: _isLoading,
text: 'Envoyer le lien de réinitialisation',
width: double.infinity,
height: 56,
);
}
Widget _buildBackToLogin() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Vous vous souvenez de votre mot de passe ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
Future<void> _handleSendResetEmail() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'envoi d'email
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Transition vers la vue de succès
setState(() {
_emailSent = true;
_isLoading = false;
});
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'envoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _handleResendEmail() async {
try {
// Simulation de renvoi d'email
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email renvoyé avec succès !'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du renvoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
}

View File

@@ -1,296 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/auth/services/keycloak_webview_auth_service.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/di/injection.dart';
import '../../../../shared/theme/app_theme.dart';
/// Page de connexion utilisant Keycloak OIDC
class KeycloakLoginPage extends StatefulWidget {
const KeycloakLoginPage({super.key});
@override
State<KeycloakLoginPage> createState() => _KeycloakLoginPageState();
}
class _KeycloakLoginPageState extends State<KeycloakLoginPage> {
late KeycloakWebViewAuthService _authService;
bool _isLoading = false;
@override
void initState() {
super.initState();
_authService = getIt<KeycloakWebViewAuthService>();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: StreamBuilder<AuthState>(
stream: _authService.authStateStream,
builder: (context, snapshot) {
final authState = snapshot.data ?? const AuthState.unknown();
if (authState.isAuthenticated) {
// Rediriger vers la page principale si déjà connecté
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacementNamed('/main');
});
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
MediaQuery.of(context).padding.top -
MediaQuery.of(context).padding.bottom - 48,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Logo et titre
_buildHeader(),
const SizedBox(height: 48),
// Message d'accueil
_buildWelcomeMessage(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(authState),
const SizedBox(height: 16),
// Message d'erreur si présent
if (authState.errorMessage != null)
_buildErrorMessage(authState.errorMessage!),
const SizedBox(height: 32),
// Informations sur la sécurité
_buildSecurityInfo(),
],
),
),
),
);
},
),
);
}
Widget _buildHeader() {
return Column(
children: [
// Logo UnionFlow
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(60),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: const Icon(
Icons.groups,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 24),
// Titre
Text(
'UnionFlow',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Gestion d\'organisations',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
),
],
);
}
Widget _buildWelcomeMessage() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const Icon(
Icons.security,
size: 48,
color: AppTheme.primaryColor,
),
const SizedBox(height: 16),
Text(
'Connexion sécurisée',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Connectez-vous avec votre compte UnionFlow pour accéder à toutes les fonctionnalités de l\'application.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
);
}
Widget _buildLoginButton(AuthState authState) {
return ElevatedButton(
onPressed: authState.isLoading || _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
),
child: authState.isLoading || _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 24),
const SizedBox(width: 12),
Text(
'Se connecter avec Keycloak',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildErrorMessage(String errorMessage) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.errorColor.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppTheme.errorColor,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
errorMessage,
style: const TextStyle(
color: AppTheme.errorColor,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
Widget _buildSecurityInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Column(
children: [
Row(
children: [
const Icon(
Icons.info_outline,
color: Colors.blue,
size: 20,
),
const SizedBox(width: 8),
Text(
'Authentification sécurisée',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
Text(
'Vos données sont protégées par Keycloak, une solution d\'authentification enterprise. '
'Votre mot de passe n\'est jamais stocké sur cet appareil.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.blue[700],
),
),
],
),
);
}
Future<void> _handleLogin() async {
setState(() {
_isLoading = true;
});
try {
await _authService.loginWithWebView(context);
} catch (e) {
// L'erreur sera gérée par le stream AuthState
print('Erreur de connexion: $e');
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}

View File

@@ -0,0 +1,596 @@
/// Page d'Authentification Keycloak via WebView
///
/// Interface utilisateur professionnelle pour l'authentification Keycloak
/// utilisant WebView avec gestion complète des états et des erreurs.
///
/// Fonctionnalités :
/// - WebView sécurisée avec contrôles de navigation
/// - Indicateurs de progression et de chargement
/// - Gestion des erreurs réseau et timeouts
/// - Interface utilisateur adaptative
/// - Support des thèmes sombre/clair
/// - Logging détaillé pour le debugging
library keycloak_webview_auth_page;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../../core/auth/services/keycloak_webview_auth_service.dart';
import '../../../../core/auth/models/user.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// États de l'authentification WebView
enum KeycloakWebViewAuthState {
/// Initialisation en cours
initializing,
/// Chargement de la page d'authentification
loading,
/// Page d'authentification affichée
ready,
/// Authentification en cours
authenticating,
/// Authentification réussie
success,
/// Erreur d'authentification
error,
/// Timeout
timeout,
}
/// Page d'authentification Keycloak avec WebView
class KeycloakWebViewAuthPage extends StatefulWidget {
/// Callback appelé en cas de succès d'authentification
final Function(User user) onAuthSuccess;
/// Callback appelé en cas d'erreur
final Function(String error) onAuthError;
/// Callback appelé en cas d'annulation
final VoidCallback? onAuthCancel;
/// Timeout pour l'authentification (en secondes)
final int timeoutSeconds;
const KeycloakWebViewAuthPage({
super.key,
required this.onAuthSuccess,
required this.onAuthError,
this.onAuthCancel,
this.timeoutSeconds = 300, // 5 minutes par défaut
});
@override
State<KeycloakWebViewAuthPage> createState() => _KeycloakWebViewAuthPageState();
}
class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage>
with TickerProviderStateMixin {
// Contrôleurs et état
late WebViewController _webViewController;
late AnimationController _progressAnimationController;
late Animation<double> _progressAnimation;
Timer? _timeoutTimer;
// État de l'authentification
KeycloakWebViewAuthState _authState = KeycloakWebViewAuthState.initializing;
String? _errorMessage;
double _loadingProgress = 0.0;
String _currentUrl = '';
// Paramètres d'authentification
String? _authUrl;
String? _expectedState;
String? _codeVerifier;
@override
void initState() {
super.initState();
_initializeAnimations();
_initializeAuthentication();
}
@override
void dispose() {
_progressAnimationController.dispose();
_timeoutTimer?.cancel();
super.dispose();
}
/// Initialise les animations
void _initializeAnimations() {
_progressAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _progressAnimationController,
curve: Curves.easeInOut,
));
}
/// Initialise l'authentification
Future<void> _initializeAuthentication() async {
try {
debugPrint('🚀 Initialisation de l\'authentification WebView...');
setState(() {
_authState = KeycloakWebViewAuthState.initializing;
});
// Préparer l'authentification
final Map<String, String> authParams =
await KeycloakWebViewAuthService.prepareAuthentication();
_authUrl = authParams['url'];
_expectedState = authParams['state'];
_codeVerifier = authParams['code_verifier'];
if (_authUrl == null) {
throw Exception('URL d\'authentification manquante');
}
// Initialiser la WebView
await _initializeWebView();
// Démarrer le timer de timeout
_startTimeoutTimer();
debugPrint('✅ Authentification initialisée avec succès');
} catch (e) {
debugPrint('💥 Erreur initialisation authentification: $e');
_handleError('Erreur d\'initialisation: $e');
}
}
/// Initialise la WebView
Future<void> _initializeWebView() async {
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(ColorTokens.surface)
..setNavigationDelegate(
NavigationDelegate(
onProgress: _onLoadingProgress,
onPageStarted: _onPageStarted,
onPageFinished: _onPageFinished,
onWebResourceError: _onWebResourceError,
onNavigationRequest: _onNavigationRequest,
),
);
// Charger l'URL d'authentification
if (_authUrl != null) {
await _webViewController.loadRequest(Uri.parse(_authUrl!));
setState(() {
_authState = KeycloakWebViewAuthState.loading;
});
}
}
/// Démarre le timer de timeout
void _startTimeoutTimer() {
_timeoutTimer = Timer(Duration(seconds: widget.timeoutSeconds), () {
if (_authState != KeycloakWebViewAuthState.success) {
debugPrint('⏰ Timeout d\'authentification atteint');
_handleTimeout();
}
});
}
/// Gère la progression du chargement
void _onLoadingProgress(int progress) {
setState(() {
_loadingProgress = progress / 100.0;
});
if (progress == 100) {
_progressAnimationController.forward();
}
}
/// Gère le début du chargement d'une page
void _onPageStarted(String url) {
debugPrint('📄 Chargement de la page: $url');
setState(() {
_currentUrl = url;
_loadingProgress = 0.0;
});
_progressAnimationController.reset();
}
/// Gère la fin du chargement d'une page
void _onPageFinished(String url) {
debugPrint('✅ Page chargée: $url');
setState(() {
_currentUrl = url;
if (_authState == KeycloakWebViewAuthState.loading) {
_authState = KeycloakWebViewAuthState.ready;
}
});
}
/// Gère les erreurs de ressources web
void _onWebResourceError(WebResourceError error) {
debugPrint('💥 Erreur WebView: ${error.description}');
// Ignorer certaines erreurs non critiques
if (error.errorCode == -999) { // Code d'erreur pour annulation
return;
}
_handleError('Erreur de chargement: ${error.description}');
}
/// Gère les requêtes de navigation
NavigationDecision _onNavigationRequest(NavigationRequest request) {
final String url = request.url;
debugPrint('🔗 Navigation vers: $url');
// Vérifier si c'est notre URL de callback
if (url.startsWith('dev.lions.unionflow-mobile://auth/callback')) {
debugPrint('🎯 URL de callback détectée: $url');
_handleAuthCallback(url);
return NavigationDecision.prevent;
}
// Vérifier d'autres patterns de callback possibles
if (url.contains('code=') && url.contains('state=')) {
debugPrint('🎯 Callback potentiel détecté (avec code et state): $url');
_handleAuthCallback(url);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
/// Traite le callback d'authentification
Future<void> _handleAuthCallback(String callbackUrl) async {
try {
setState(() {
_authState = KeycloakWebViewAuthState.authenticating;
});
debugPrint('🔄 Traitement du callback d\'authentification...');
debugPrint('📋 URL de callback reçue: $callbackUrl');
// Traiter le callback via le service
final User user = await KeycloakWebViewAuthService.handleAuthCallback(callbackUrl);
setState(() {
_authState = KeycloakWebViewAuthState.success;
});
// Annuler le timer de timeout
_timeoutTimer?.cancel();
debugPrint('🎉 Authentification réussie pour: ${user.fullName}');
debugPrint('👤 Rôle: ${user.primaryRole.displayName}');
debugPrint('🔐 Permissions: ${user.additionalPermissions.length}');
// Notifier le succès avec un délai pour l'animation
Future.delayed(const Duration(milliseconds: 500), () {
widget.onAuthSuccess(user);
});
} catch (e, stackTrace) {
debugPrint('💥 Erreur traitement callback: $e');
debugPrint('📋 Stack trace: $stackTrace');
// Essayer de donner plus d'informations sur l'erreur
String errorMessage = 'Erreur d\'authentification: $e';
if (e.toString().contains('MISSING_AUTH_STATE')) {
errorMessage = 'Session expirée. Veuillez réessayer.';
} else if (e.toString().contains('INVALID_STATE')) {
errorMessage = 'Erreur de sécurité. Veuillez réessayer.';
} else if (e.toString().contains('MISSING_AUTH_CODE')) {
errorMessage = 'Code d\'autorisation manquant. Veuillez réessayer.';
}
_handleError(errorMessage);
}
}
/// Gère les erreurs
void _handleError(String error) {
setState(() {
_authState = KeycloakWebViewAuthState.error;
_errorMessage = error;
});
_timeoutTimer?.cancel();
// Vibration pour indiquer l'erreur
HapticFeedback.lightImpact();
widget.onAuthError(error);
}
/// Gère le timeout
void _handleTimeout() {
setState(() {
_authState = KeycloakWebViewAuthState.timeout;
_errorMessage = 'Timeout d\'authentification atteint';
});
HapticFeedback.lightImpact();
widget.onAuthError('Timeout d\'authentification');
}
/// Gère l'annulation
void _handleCancel() {
debugPrint('❌ Authentification annulée par l\'utilisateur');
_timeoutTimer?.cancel();
if (widget.onAuthCancel != null) {
widget.onAuthCancel!();
} else {
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
appBar: _buildAppBar(),
body: _buildBody(),
);
}
/// Construit l'AppBar
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
elevation: 0,
title: Text(
'Connexion Keycloak',
style: TypographyTokens.headlineSmall.copyWith(
color: ColorTokens.onPrimary,
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: _handleCancel,
tooltip: 'Annuler',
),
actions: [
if (_authState == KeycloakWebViewAuthState.ready)
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _webViewController.reload(),
tooltip: 'Actualiser',
),
],
bottom: _buildProgressIndicator(),
);
}
/// Construit l'indicateur de progression
PreferredSizeWidget? _buildProgressIndicator() {
if (_authState == KeycloakWebViewAuthState.loading ||
_authState == KeycloakWebViewAuthState.authenticating) {
return PreferredSize(
preferredSize: const Size.fromHeight(4.0),
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return LinearProgressIndicator(
value: _authState == KeycloakWebViewAuthState.authenticating
? null
: _loadingProgress,
backgroundColor: ColorTokens.onPrimary.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(ColorTokens.onPrimary),
);
},
),
);
}
return null;
}
/// Construit le corps de la page
Widget _buildBody() {
switch (_authState) {
case KeycloakWebViewAuthState.initializing:
return _buildInitializingView();
case KeycloakWebViewAuthState.loading:
case KeycloakWebViewAuthState.ready:
return _buildWebView();
case KeycloakWebViewAuthState.authenticating:
return _buildAuthenticatingView();
case KeycloakWebViewAuthState.success:
return _buildSuccessView();
case KeycloakWebViewAuthState.error:
case KeycloakWebViewAuthState.timeout:
return _buildErrorView();
}
}
/// Vue d'initialisation
Widget _buildInitializingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: SpacingTokens.xl),
Text(
'Initialisation...',
style: TypographyTokens.bodyLarge.copyWith(
color: ColorTokens.onSurface,
),
),
],
),
);
}
/// Vue WebView
Widget _buildWebView() {
return WebViewWidget(controller: _webViewController);
}
/// Vue d'authentification en cours
Widget _buildAuthenticatingView() {
return Container(
color: ColorTokens.surface,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: SpacingTokens.xxxl),
Text(
'Authentification en cours...',
style: TypographyTokens.headlineSmall.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.xl),
Text(
'Veuillez patienter pendant que nous\nfinalisons votre connexion.',
textAlign: TextAlign.center,
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurface.withOpacity(0.7),
),
),
],
),
),
);
}
/// Vue de succès
Widget _buildSuccessView() {
return Container(
color: ColorTokens.surface,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 48,
),
),
const SizedBox(height: SpacingTokens.xxxl),
Text(
'Connexion réussie !',
style: TypographyTokens.headlineSmall.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.xl),
Text(
'Redirection vers l\'application...',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurface.withOpacity(0.7),
),
),
],
),
),
);
}
/// Vue d'erreur
Widget _buildErrorView() {
return Container(
color: ColorTokens.surface,
padding: const EdgeInsets.all(SpacingTokens.xxxl),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: ColorTokens.error,
shape: BoxShape.circle,
),
child: Icon(
_authState == KeycloakWebViewAuthState.timeout
? Icons.access_time
: Icons.error_outline,
color: ColorTokens.onError,
size: 48,
),
),
const SizedBox(height: SpacingTokens.xxxl),
Text(
_authState == KeycloakWebViewAuthState.timeout
? 'Timeout d\'authentification'
: 'Erreur d\'authentification',
style: TypographyTokens.headlineSmall.copyWith(
color: ColorTokens.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.xl),
Text(
_errorMessage ?? 'Une erreur inattendue s\'est produite',
textAlign: TextAlign.center,
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: SpacingTokens.huge),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: _initializeAuthentication,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: ColorTokens.primary,
foregroundColor: ColorTokens.onPrimary,
),
),
OutlinedButton.icon(
onPressed: _handleCancel,
icon: const Icon(Icons.close),
label: const Text('Annuler'),
style: OutlinedButton.styleFrom(
foregroundColor: ColorTokens.onSurface,
),
),
],
),
],
),
),
);
}
}

View File

@@ -1,16 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/temp_auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/login_form.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Page de Connexion Adaptative - Démonstration des Rôles
/// Interface de connexion avec sélection de rôles pour démonstration
library login_page;
/// Écran de connexion avec interface sophistiquée
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/models/user_role.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'keycloak_webview_auth_page.dart';
/// Page de connexion avec démonstration des rôles
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -22,37 +21,27 @@ class _LoginPageState extends State<LoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
_initializeAnimations();
}
void _setupAnimations() {
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _initializeAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
@@ -61,238 +50,348 @@ class _LoginPageState extends State<LoginPage>
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
_slideAnimation = Tween<Offset>(
begin: const Offset(0.0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
curve: const Interval(0.3, 1.0, curve: Curves.easeOutCubic),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticIn,
));
}
void _startEntryAnimation() {
_animationController.forward();
}
void _startShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
/// Ouvre la page WebView d'authentification
void _openWebViewAuth(BuildContext context, AuthWebViewRequired state) {
debugPrint('🚀 Ouverture WebView avec URL: ${state.authUrl}');
debugPrint('🔑 State: ${state.state}');
debugPrint('🔐 Code verifier: ${state.codeVerifier.substring(0, 10)}...');
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
debugPrint('📱 Tentative de navigation vers KeycloakWebViewAuthPage...');
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => KeycloakWebViewAuthPage(
onAuthSuccess: (user) {
debugPrint('✅ Authentification réussie pour: ${user.fullName}');
// Notifier le BLoC du succès
context.read<AuthBloc>().add(AuthWebViewCallback('success'));
// Fermer la WebView et naviguer vers le dashboard
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/dashboard');
},
onAuthError: (error) {
debugPrint('❌ Erreur d\'authentification: $error');
// Fermer la WebView et afficher l'erreur
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur d\'authentification: $error'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
},
onAuthCancel: () {
debugPrint('❌ Authentification annulée par l\'utilisateur');
// Fermer la WebView
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Authentification annulée'),
backgroundColor: Colors.orange,
),
);
},
),
),
);
debugPrint('✅ Navigation vers KeycloakWebViewAuthPage lancée');
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocConsumer<TempAuthBloc, AuthState>(
body: BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
setState(() {
_isLoading = state.status == AuthStatus.checking;
});
if (state.status == AuthStatus.error) {
_startShakeAnimation();
_showErrorSnackBar(state.errorMessage ?? 'Erreur de connexion');
} else if (state.status == AuthStatus.authenticated) {
_showSuccessSnackBar('Connexion réussie !');
debugPrint('🔄 État BLoC reçu: ${state.runtimeType}');
if (state is AuthAuthenticated) {
debugPrint('✅ Utilisateur authentifié, navigation vers dashboard');
Navigator.of(context).pushReplacementNamed('/dashboard');
} else if (state is AuthError) {
debugPrint('❌ Erreur d\'authentification: ${state.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
} else if (state is AuthWebViewRequired) {
debugPrint('🚀 État AuthWebViewRequired reçu, ouverture WebView...');
// Ouvrir la page WebView d'authentification immédiatement
WidgetsBinding.instance.addPostFrameCallback((_) {
_openWebViewAuth(context, state);
});
} else if (state is AuthLoading) {
debugPrint('⏳ État de chargement...');
} else {
debugPrint(' État non géré: ${state.runtimeType}');
}
},
builder: (context, state) {
return SafeArea(
child: _buildLoginContent(),
);
// Vérification supplémentaire dans le builder
if (state is AuthWebViewRequired) {
debugPrint('🔄 Builder détecte AuthWebViewRequired, ouverture WebView...');
WidgetsBinding.instance.addPostFrameCallback((_) {
_openWebViewAuth(context, state);
});
}
return _buildLoginContent(context, state);
},
),
);
}
Widget _buildLoginContent() {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
const LoginHeader(),
const SizedBox(height: 40),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
((_shakeAnimation.value * 10).round() % 2 == 0 ? 1 : -1),
0,
),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: LoginForm(
formKey: _formKey,
emailController: _emailController,
passwordController: _passwordController,
obscurePassword: _obscurePassword,
rememberMe: _rememberMe,
isLoading: _isLoading,
onObscureToggle: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
onRememberMeToggle: (value) {
setState(() {
_rememberMe = value;
});
HapticFeedback.selectionClick();
},
onSubmit: _handleLogin,
),
),
),
);
},
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
),
),
);
},
);
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_startShakeAnimation();
return;
}
HapticFeedback.lightImpact();
final loginRequest = LoginRequest(
email: _emailController.text.trim(),
password: _passwordController.text,
rememberMe: _rememberMe,
);
context.read<TempAuthBloc>().add(AuthLoginRequested(loginRequest));
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(
Icons.error_outline,
color: Colors.white,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
Widget _buildLoginContent(BuildContext context, AuthState state) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'Fermer',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
),
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: _buildLoginUI(),
),
);
},
),
),
);
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
Widget _buildLoginUI() {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.check_circle_outline,
color: Colors.white,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
// Logo et titre
_buildHeader(),
const SizedBox(height: 48),
// Information Keycloak
_buildKeycloakInfo(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(),
const SizedBox(height: 32),
// Informations de démonstration
_buildDemoInfo(),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2),
),
);
}
Widget _buildHeader() {
return Column(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(50),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.account_circle,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 24),
Text(
'UnionFlow',
style: TypographyTokens.headlineLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Dashboard Adaptatif Révolutionnaire',
style: TypographyTokens.bodyLarge.copyWith(
color: Colors.white.withOpacity(0.9),
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildKeycloakInfo() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.security,
color: Colors.white.withOpacity(0.9),
size: 32,
),
const SizedBox(height: 12),
Text(
'Authentification Keycloak',
style: TypographyTokens.bodyLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Connectez-vous avec vos identifiants UnionFlow',
style: TypographyTokens.bodyMedium.copyWith(
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'localhost:8180/realms/unionflow',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.7),
fontFamily: 'monospace',
),
),
),
],
),
);
}
Widget _buildLoginButton() {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoading = state is AuthLoading;
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF6C5CE7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 20),
const SizedBox(width: 8),
Text(
'Se Connecter avec Keycloak',
style: TypographyTokens.bodyLarge.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
);
}
Widget _buildDemoInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Column(
children: [
Icon(
Icons.info_outline,
color: Colors.white.withOpacity(0.8),
size: 24,
),
const SizedBox(height: 8),
Text(
'Mode Démonstration',
style: TypographyTokens.bodyMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Sélectionnez un rôle ci-dessus pour voir le dashboard adaptatif correspondant. Chaque rôle affiche une interface unique !',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
),
);
}
void _handleLogin() {
// Démarrer l'authentification Keycloak
context.read<AuthBloc>().add(const AuthLoginRequested());
}
}

View File

@@ -1,478 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/temp_auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion temporaire simplifié
class TempLoginPage extends StatefulWidget {
const TempLoginPage({super.key});
@override
State<TempLoginPage> createState() => _TempLoginPageState();
}
class _TempLoginPageState extends State<TempLoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'admin@unionflow.dev');
final _passwordController = TextEditingController(text: 'admin123');
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<TempAuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
const LoginHeader(),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: _buildLoginForm(),
);
},
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ email
_buildEmailField(),
const SizedBox(height: 20),
// Champ mot de passe
_buildPasswordField(),
const SizedBox(height: 16),
// Options
_buildOptionsRow(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(),
],
),
);
}
Widget _buildEmailField() {
return TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: const Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
);
}
Widget _buildPasswordField() {
return TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
enabled: !_isLoading,
onFieldSubmitted: (_) => _handleLogin(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: const Icon(
Icons.lock_outlined,
color: AppTheme.primaryColor,
),
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: AppTheme.primaryColor,
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
);
}
Widget _buildOptionsRow() {
return Row(
children: [
// Se souvenir de moi
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_rememberMe = !_rememberMe;
});
HapticFeedback.selectionClick();
},
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: _rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: _rememberMe
? const Icon(
Icons.check,
size: 14,
color: Colors.white,
)
: null,
),
const SizedBox(width: 8),
const Text(
'Se souvenir de moi',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// Compte de test
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Compte de test',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 4,
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login, size: 20),
SizedBox(width: 8),
Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<TempAuthBloc>().add(AuthLoginRequested(loginRequest));
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text('Connexion réussie !'),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}

View File

@@ -1,517 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import '../../../navigation/presentation/pages/main_navigation.dart';
import 'forgot_password_screen.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildLoginForm(),
const SizedBox(height: 24),
_buildForgotPassword(),
const SizedBox(height: 32),
_buildLoginButton(),
const SizedBox(height: 24),
_buildDivider(),
const SizedBox(height: 24),
_buildSocialLogin(),
const SizedBox(height: 32),
_buildSignUpLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.groups_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Bienvenue !',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
const Text(
'Connectez-vous à votre compte UnionFlow',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Champ Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
validator: _validatePassword,
onFieldSubmitted: (_) => _handleLogin(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Remember me
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Text(
'Se souvenir de moi',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
],
),
);
}
Widget _buildForgotPassword() {
return Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _navigateToForgotPassword(),
child: const Text(
'Mot de passe oublié ?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
);
}
Widget _buildLoginButton() {
return LoadingButton(
onPressed: _handleLogin,
isLoading: _isLoading,
text: 'Se connecter',
width: double.infinity,
height: 56,
);
}
Widget _buildDivider() {
return const Row(
children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'ou',
style: TextStyle(
color: AppTheme.textHint,
fontSize: 14,
),
),
),
Expanded(child: Divider()),
],
);
}
Widget _buildSocialLogin() {
return Column(
children: [
// Google Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleGoogleLogin(),
icon: Image.asset(
'assets/icons/google.png',
width: 20,
height: 20,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.g_mobiledata,
color: Colors.red,
size: 20,
),
),
label: const Text('Continuer avec Google'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 12),
// Microsoft Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleMicrosoftLogin(),
icon: const Icon(
Icons.business,
color: Color(0xFF00A4EF),
size: 20,
),
label: const Text('Continuer avec Microsoft'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildSignUpLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Pas encore de compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToRegister(),
child: const Text(
'S\'inscrire',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'authentification
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Navigation vers le dashboard
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const MainNavigation(),
transitionDuration: const Duration(milliseconds: 600),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
),
);
},
),
);
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur de connexion: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _handleGoogleLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Google - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _handleMicrosoftLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Microsoft - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _navigateToForgotPassword() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const ForgotPasswordScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -1,624 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _acceptTerms = false;
bool _acceptNewsletter = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 32),
_buildRegistrationForm(),
const SizedBox(height: 24),
_buildTermsAndConditions(),
const SizedBox(height: 32),
_buildRegisterButton(),
const SizedBox(height: 24),
_buildLoginLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.person_add_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Créer un compte',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
const Text(
'Rejoignez UnionFlow et gérez votre association',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildRegistrationForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Nom et Prénom
Row(
children: [
Expanded(
child: CustomTextField(
controller: _firstNameController,
label: 'Prénom',
hintText: 'Jean',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateFirstName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
const SizedBox(width: 16),
Expanded(
child: CustomTextField(
controller: _lastNameController,
label: 'Nom',
hintText: 'Dupont',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateLastName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
],
),
const SizedBox(height: 16),
// Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'jean.dupont@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Minimum 8 caractères',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
validator: _validatePassword,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Confirmer mot de passe
CustomTextField(
controller: _confirmPasswordController,
label: 'Confirmer le mot de passe',
hintText: 'Retapez votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
validator: _validateConfirmPassword,
onFieldSubmitted: (_) => _handleRegister(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
const SizedBox(height: 16),
// Indicateur de force du mot de passe
_buildPasswordStrengthIndicator(),
],
),
);
}
Widget _buildPasswordStrengthIndicator() {
final password = _passwordController.text;
final strength = _calculatePasswordStrength(password);
Color strengthColor;
String strengthText;
if (strength < 0.3) {
strengthColor = AppTheme.errorColor;
strengthText = 'Faible';
} else if (strength < 0.7) {
strengthColor = AppTheme.warningColor;
strengthText = 'Moyen';
} else {
strengthColor = AppTheme.successColor;
strengthText = 'Fort';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Force du mot de passe',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
if (password.isNotEmpty)
Text(
strengthText,
style: TextStyle(
fontSize: 12,
color: strengthColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Container(
height: 4,
decoration: BoxDecoration(
color: AppTheme.borderColor,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: password.isEmpty ? 0 : strength,
child: Container(
decoration: BoxDecoration(
color: strengthColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
}
double _calculatePasswordStrength(String password) {
if (password.isEmpty) return 0.0;
double strength = 0.0;
// Longueur
if (password.length >= 8) strength += 0.25;
if (password.length >= 12) strength += 0.25;
// Contient des minuscules
if (password.contains(RegExp(r'[a-z]'))) strength += 0.15;
// Contient des majuscules
if (password.contains(RegExp(r'[A-Z]'))) strength += 0.15;
// Contient des chiffres
if (password.contains(RegExp(r'[0-9]'))) strength += 0.1;
// Contient des caractères spéciaux
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) strength += 0.1;
return strength.clamp(0.0, 1.0);
}
Widget _buildTermsAndConditions() {
return Column(
children: [
// Accepter les conditions
Row(
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
Expanded(
child: RichText(
text: const TextSpan(
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
children: [
TextSpan(text: 'J\'accepte les '),
TextSpan(
text: 'Conditions d\'utilisation',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
TextSpan(text: ' et la '),
TextSpan(
text: 'Politique de confidentialité',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
],
),
),
),
],
),
// Newsletter (optionnel)
Row(
children: [
Checkbox(
value: _acceptNewsletter,
onChanged: (value) {
setState(() {
_acceptNewsletter = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Expanded(
child: Text(
'Je souhaite recevoir des actualités et conseils par email (optionnel)',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
),
],
),
],
);
}
Widget _buildRegisterButton() {
return LoadingButton(
onPressed: _acceptTerms ? _handleRegister : null,
isLoading: _isLoading,
text: 'Créer mon compte',
width: double.infinity,
height: 56,
enabled: _acceptTerms,
);
}
Widget _buildLoginLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Déjà un compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToLogin(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateFirstName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre prénom';
}
if (value.length < 2) {
return 'Le prénom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateLastName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre nom';
}
if (value.length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir un mot de passe';
}
if (value.length < 8) {
return 'Le mot de passe doit contenir au moins 8 caractères';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Le mot de passe doit contenir au moins une majuscule';
}
if (!value.contains(RegExp(r'[a-z]'))) {
return 'Le mot de passe doit contenir au moins une minuscule';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Le mot de passe doit contenir au moins un chiffre';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez confirmer votre mot de passe';
}
if (value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez accepter les conditions d\'utilisation'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'inscription
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Afficher message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Compte créé avec succès ! Vérifiez votre email.'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
// Navigation vers l'écran de connexion
_navigateToLogin();
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du compte: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _navigateToLogin() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -1,400 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 200));
_fadeController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
const Color(0xFF0D47A1),
],
),
),
child: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header avec logo
Expanded(
flex: 3,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo principal
Container(
width: 140,
height: 140,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(35),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: const Icon(
Icons.groups_rounded,
size: 70,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 32),
// Titre principal
const Text(
'UnionFlow',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.5,
),
),
const SizedBox(height: 16),
// Sous-titre
Text(
'Gestion moderne d\'associations\net de mutuelles',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
height: 1.4,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Points forts
_buildFeatureHighlights(),
],
),
),
),
// Boutons d'action
Expanded(
flex: 2,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Connexion
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () => _navigateToLogin(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryColor,
elevation: 8,
shadowColor: Colors.black.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login, size: 20),
SizedBox(width: 8),
Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 16),
// Bouton Inscription
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: () => _navigateToRegister(),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(
color: Colors.white,
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_add, size: 20),
SizedBox(width: 8),
Text(
'Créer un compte',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 24),
// Lien mode démo
TextButton(
onPressed: () => _navigateToDemo(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility,
size: 16,
color: Colors.white.withOpacity(0.8),
),
const SizedBox(width: 6),
Text(
'Découvrir en mode démo',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
// Footer
Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
Text(
'Version 1.0.0 • Sécurisé et confidentiel',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
'© 2024 Lions Club International',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 10,
),
),
],
),
),
],
),
),
);
},
),
),
),
);
}
Widget _buildFeatureHighlights() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFeatureItem(Icons.security, 'Sécurisé'),
_buildFeatureItem(Icons.analytics, 'Analytique'),
_buildFeatureItem(Icons.cloud_sync, 'Synchronisé'),
],
),
);
}
Widget _buildFeatureItem(IconData icon, String label) {
return Column(
children: [
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
);
}
void _navigateToLogin() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToDemo() {
// TODO: Implémenter la navigation vers le mode démo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Mode démo - En cours de développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}

View File

@@ -1,362 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Pied de page de la connexion avec informations et liens
class LoginFooter extends StatelessWidget {
const LoginFooter({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Séparateur
_buildDivider(),
const SizedBox(height: 24),
// Informations sur l'application
_buildAppInfo(),
const SizedBox(height: 20),
// Liens utiles
_buildUsefulLinks(context),
const SizedBox(height: 20),
// Version et copyright
_buildVersionInfo(),
],
);
}
Widget _buildDivider() {
return Row(
children: [
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Icon(
Icons.star,
size: 16,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
],
);
}
Widget _buildAppInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.1),
),
),
child: const Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.security,
size: 20,
color: AppTheme.successColor,
),
SizedBox(width: 8),
Text(
'Connexion sécurisée',
style: TextStyle(
fontSize: 14,
color: AppTheme.successColor,
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 8),
Text(
'Vos données sont protégées par un cryptage de niveau bancaire',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildUsefulLinks(BuildContext context) {
return Wrap(
spacing: 20,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
_buildLinkButton(
icon: Icons.help_outline,
label: 'Aide',
onTap: () => _showHelpDialog(context),
),
_buildLinkButton(
icon: Icons.info_outline,
label: 'À propos',
onTap: () => _showAboutDialog(context),
),
_buildLinkButton(
icon: Icons.privacy_tip_outlined,
label: 'Confidentialité',
onTap: () => _showPrivacyDialog(context),
),
],
);
}
Widget _buildLinkButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: AppTheme.textSecondary,
),
const SizedBox(width: 6),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildVersionInfo() {
return Column(
children: [
Text(
'UnionFlow Mobile v1.0.0',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'© 2025 Lions Dev Team. Tous droits réservés.',
style: TextStyle(
fontSize: 10,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
],
);
}
void _showHelpDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(
Icons.help_outline,
color: AppTheme.infoColor,
),
SizedBox(width: 12),
Text('Aide'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHelpItem(
'Connexion',
'Utilisez votre email et mot de passe fournis par votre association.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Mot de passe oublié',
'Contactez votre administrateur pour réinitialiser votre mot de passe.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Problèmes techniques',
'Vérifiez votre connexion internet et réessayez.',
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showAboutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.primaryColor,
),
SizedBox(width: 12),
Text('À propos'),
],
),
content: const Text(
'UnionFlow est une solution complète de gestion d\'associations développée par Lions Dev Team.\n\n'
'Cette application mobile vous permet de gérer vos membres, cotisations, événements et bien plus encore, où que vous soyez.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showPrivacyDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(
Icons.privacy_tip_outlined,
color: AppTheme.warningColor,
),
SizedBox(width: 12),
Text('Confidentialité'),
],
),
content: const Text(
'Nous respectons votre vie privée. Toutes vos données sont stockées de manière sécurisée et ne sont jamais partagées avec des tiers.\n\n'
'Les données sont chiffrées en transit et au repos selon les standards de sécurité les plus élevés.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Compris',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildHelpItem(String title, String description) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
}

View File

@@ -1,444 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
/// Formulaire de connexion sophistiqué avec validation
class LoginForm extends StatefulWidget {
final GlobalKey<FormState> formKey;
final TextEditingController emailController;
final TextEditingController passwordController;
final bool obscurePassword;
final bool rememberMe;
final bool isLoading;
final VoidCallback onObscureToggle;
final ValueChanged<bool> onRememberMeToggle;
final VoidCallback onSubmit;
const LoginForm({
super.key,
required this.formKey,
required this.emailController,
required this.passwordController,
required this.obscurePassword,
required this.rememberMe,
required this.isLoading,
required this.onObscureToggle,
required this.onRememberMeToggle,
required this.onSubmit,
});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm>
with TickerProviderStateMixin {
late AnimationController _fieldAnimationController;
late List<Animation<Offset>> _fieldAnimations;
final FocusNode _emailFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
bool _emailHasFocus = false;
bool _passwordHasFocus = false;
@override
void initState() {
super.initState();
_setupAnimations();
_setupFocusListeners();
_startFieldAnimations();
}
void _setupAnimations() {
_fieldAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fieldAnimations = List.generate(4, (index) {
// Calcul sécurisé pour éviter end > 1.0
final start = index * 0.15; // Réduit l'espacement
final end = (start + 0.4).clamp(0.0, 1.0); // Assure end <= 1.0
return Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _fieldAnimationController,
curve: Interval(
start,
end,
curve: Curves.easeOut,
),
));
});
}
void _setupFocusListeners() {
_emailFocusNode.addListener(() {
setState(() {
_emailHasFocus = _emailFocusNode.hasFocus;
});
});
_passwordFocusNode.addListener(() {
setState(() {
_passwordHasFocus = _passwordFocusNode.hasFocus;
});
});
}
void _startFieldAnimations() {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
_fieldAnimationController.forward();
}
});
}
@override
void dispose() {
_fieldAnimationController.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: widget.formKey,
child: Column(
children: [
// Champ email
SlideTransition(
position: _fieldAnimations[0],
child: _buildEmailField(),
),
const SizedBox(height: 20),
// Champ mot de passe
SlideTransition(
position: _fieldAnimations[1],
child: _buildPasswordField(),
),
const SizedBox(height: 16),
// Options (Se souvenir de moi, Mot de passe oublié)
SlideTransition(
position: _fieldAnimations[2],
child: _buildOptionsRow(),
),
const SizedBox(height: 32),
// Bouton de connexion
SlideTransition(
position: _fieldAnimations[3],
child: _buildLoginButton(),
),
],
),
);
}
Widget _buildEmailField() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: _emailHasFocus ? [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
] : [],
),
child: TextFormField(
controller: widget.emailController,
focusNode: _emailFocusNode,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !widget.isLoading,
onFieldSubmitted: (_) {
FocusScope.of(context).requestFocus(_passwordFocusNode);
},
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.email_outlined,
color: _emailHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
),
);
}
Widget _buildPasswordField() {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: _passwordHasFocus ? [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
] : [],
),
child: TextFormField(
controller: widget.passwordController,
focusNode: _passwordFocusNode,
obscureText: widget.obscurePassword,
textInputAction: TextInputAction.done,
enabled: !widget.isLoading,
onFieldSubmitted: (_) => widget.onSubmit(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: AnimatedContainer(
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.lock_outlined,
color: _passwordHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
suffixIcon: IconButton(
onPressed: widget.onObscureToggle,
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Icon(
widget.obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
key: ValueKey(widget.obscurePassword),
color: _passwordHasFocus
? AppTheme.primaryColor
: AppTheme.textSecondary,
),
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: AppTheme.errorColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
);
}
Widget _buildOptionsRow() {
return Row(
children: [
// Se souvenir de moi
Expanded(
child: GestureDetector(
onTap: () => widget.onRememberMeToggle(!widget.rememberMe),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: widget.rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: widget.rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: widget.rememberMe
? const Icon(
Icons.check,
size: 14,
color: Colors.white,
)
: null,
),
const SizedBox(width: 8),
const Flexible(
child: Text(
'Se souvenir de moi',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
// Mot de passe oublié
TextButton(
onPressed: widget.isLoading ? null : () {
HapticFeedback.selectionClick();
_showForgotPasswordDialog();
},
child: const Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 14,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: widget.isLoading
? QuickButtons.primary(
text: '',
onPressed: () {},
loading: true,
)
: QuickButtons.primary(
text: 'Se connecter',
icon: Icons.login,
onPressed: widget.onSubmit,
size: ButtonSize.large,
),
);
}
void _showForgotPasswordDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(
Icons.help_outline,
color: AppTheme.primaryColor,
),
SizedBox(width: 12),
Text('Mot de passe oublié'),
],
),
content: const Text(
'Pour récupérer votre mot de passe, veuillez contacter votre administrateur ou utiliser la fonction de récupération sur l\'interface web.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Compris',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}

View File

@@ -1,259 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// En-tête de la page de connexion avec logo et animation
class LoginHeader extends StatefulWidget {
final VoidCallback? onAnimationComplete;
const LoginHeader({
super.key,
this.onAnimationComplete,
});
@override
State<LoginHeader> createState() => _LoginHeaderState();
}
class _LoginHeaderState extends State<LoginHeader>
with TickerProviderStateMixin {
late AnimationController _logoController;
late AnimationController _textController;
late Animation<double> _logoScaleAnimation;
late Animation<double> _logoRotationAnimation;
late Animation<double> _textFadeAnimation;
late Animation<Offset> _textSlideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startAnimations();
}
void _setupAnimations() {
_logoController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_textController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_logoScaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: Curves.elasticOut,
));
_logoRotationAnimation = Tween<double>(
begin: -0.1,
end: 0.0,
).animate(CurvedAnimation(
parent: _logoController,
curve: Curves.easeOut,
));
_textFadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _textController,
curve: Curves.easeOut,
));
_textSlideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _textController,
curve: Curves.easeOut,
));
}
void _startAnimations() {
_logoController.forward().then((_) {
_textController.forward().then((_) {
widget.onAnimationComplete?.call();
});
});
}
@override
void dispose() {
_logoController.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Logo animé
AnimatedBuilder(
animation: _logoController,
builder: (context, child) {
return Transform.scale(
scale: _logoScaleAnimation.value,
child: Transform.rotate(
angle: _logoRotationAnimation.value,
child: _buildLogo(),
),
);
},
),
const SizedBox(height: 32),
// Texte animé
AnimatedBuilder(
animation: _textController,
builder: (context, child) {
return FadeTransition(
opacity: _textFadeAnimation,
child: SlideTransition(
position: _textSlideAnimation,
child: _buildWelcomeText(),
),
);
},
),
],
);
}
Widget _buildLogo() {
return Container(
width: 120,
height: 120,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.secondaryColor,
],
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
// Effet de brillance
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.2),
Colors.transparent,
],
),
borderRadius: BorderRadius.circular(25),
),
),
// Icône ou texte du logo
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.group,
size: 48,
color: Colors.white,
),
const SizedBox(height: 4),
Text(
'UF',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
],
),
],
),
);
}
Widget _buildWelcomeText() {
return Column(
children: [
// Titre principal
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.secondaryColor,
],
).createShader(bounds),
child: Text(
'UnionFlow',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Gestion d\'associations',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
const SizedBox(height: 24),
// Message de bienvenue
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.2),
width: 1,
),
),
child: Text(
'Connectez-vous pour accéder à votre espace',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}