/// Page de Connexion UnionFlow - Design System Unifié (Version Premium) /// Interface de connexion moderne orientée métier avec animations avancées /// Utilise la palette Bleu Roi (#4169E1) + Bleu Pétrole (#2C5F6F) library login_page; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../bloc/auth_bloc.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import 'keycloak_webview_auth_page.dart'; /// Page de connexion UnionFlow /// Présente l'application et permet l'authentification sécurisée class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with TickerProviderStateMixin { late AnimationController _animationController; late AnimationController _backgroundController; late AnimationController _pulseController; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _scaleAnimation; late Animation _backgroundAnimation; late Animation _pulseAnimation; @override void initState() { super.initState(); _initializeAnimations(); } @override void dispose() { _animationController.dispose(); _backgroundController.dispose(); _pulseController.dispose(); super.dispose(); } void _initializeAnimations() { // Animation principale d'entrée _animationController = AnimationController( duration: const Duration(milliseconds: 1400), vsync: this, ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.easeOut), )); _slideAnimation = Tween( begin: const Offset(0.0, 0.4), end: Offset.zero, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic), )); _scaleAnimation = Tween( begin: 0.8, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.6, curve: Curves.easeOutBack), )); // Animation de fond subtile _backgroundController = AnimationController( duration: const Duration(seconds: 8), vsync: this, )..repeat(reverse: true); _backgroundAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _backgroundController, curve: Curves.easeInOut, )); // Animation de pulsation pour le logo _pulseController = AnimationController( duration: const Duration(seconds: 3), vsync: this, )..repeat(reverse: true); _pulseAnimation = Tween( begin: 1.0, end: 1.08, ).animate(CurvedAnimation( parent: _pulseController, curve: Curves.easeInOut, )); _animationController.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)}...'); debugPrint('📱 Tentative de navigation vers KeycloakWebViewAuthPage...'); Navigator.of(context).push( MaterialPageRoute( builder: (context) => KeycloakWebViewAuthPage( onAuthSuccess: (user) { debugPrint('✅ Authentification réussie pour: ${user.fullName}'); debugPrint('🔄 Notification du BLoC avec les données utilisateur...'); context.read().add(AuthWebViewCallback( 'success', user: user, )); Navigator.of(context).pop(); }, onAuthError: (error) { debugPrint('❌ Erreur d\'authentification: $error'); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur d\'authentification: $error'), backgroundColor: ColorTokens.error, duration: const Duration(seconds: 5), behavior: SnackBarBehavior.floating, ), ); }, onAuthCancel: () { debugPrint('❌ Authentification annulée par l\'utilisateur'); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Authentification annulée'), backgroundColor: ColorTokens.warning, behavior: SnackBarBehavior.floating, ), ); }, ), ), ); debugPrint('✅ Navigation vers KeycloakWebViewAuthPage lancée'); } @override Widget build(BuildContext context) { return Scaffold( body: BlocConsumer( listener: (context, state) { 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: ColorTokens.error, behavior: SnackBarBehavior.floating, ), ); } else if (state is AuthWebViewRequired) { debugPrint('🚀 État AuthWebViewRequired reçu, ouverture WebView...'); 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) { if (state is AuthWebViewRequired) { debugPrint('🔄 Builder détecte AuthWebViewRequired, ouverture WebView...'); WidgetsBinding.instance.addPostFrameCallback((_) { _openWebViewAuth(context, state); }); } return _buildLoginContent(context, state); }, ), ); } Widget _buildLoginContent(BuildContext context, AuthState state) { return Stack( children: [ // Fond animé avec dégradé dynamique AnimatedBuilder( animation: _backgroundAnimation, builder: (context, child) { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ ColorTokens.background, Color.lerp( ColorTokens.background, ColorTokens.surface, _backgroundAnimation.value * 0.3, )!, ColorTokens.surface, ], stops: const [0.0, 0.5, 1.0], ), ), ); }, ), // Éléments décoratifs de fond _buildBackgroundDecoration(), // Contenu principal SafeArea( child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: _buildLoginUI(), ), ); }, ), ), ], ); } Widget _buildBackgroundDecoration() { return Positioned.fill( child: AnimatedBuilder( animation: _backgroundAnimation, builder: (context, child) { return Stack( children: [ // Cercle décoratif haut gauche Positioned( top: -100 + (_backgroundAnimation.value * 30), left: -100 + (_backgroundAnimation.value * 20), child: Container( width: 300, height: 300, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ ColorTokens.primary.withOpacity(0.15), ColorTokens.primary.withOpacity(0.0), ], ), ), ), ), // Cercle décoratif bas droit Positioned( bottom: -150 - (_backgroundAnimation.value * 30), right: -120 - (_backgroundAnimation.value * 20), child: Container( width: 400, height: 400, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ ColorTokens.primary.withOpacity(0.12), ColorTokens.primary.withOpacity(0.0), ], ), ), ), ), // Cercle décoratif centre Positioned( top: MediaQuery.of(context).size.height * 0.3, right: -50, child: Container( width: 200, height: 200, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ ColorTokens.secondary.withOpacity(0.1), ColorTokens.secondary.withOpacity(0.0), ], ), ), ), ), ], ); }, ), ); } Widget _buildLoginUI() { return Center( child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.xxxl), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: SpacingTokens.giant), // Logo et branding premium _buildBranding(), const SizedBox(height: SpacingTokens.giant), // Features cards _buildFeatureCards(), const SizedBox(height: SpacingTokens.giant), // Card de connexion principale _buildLoginCard(), const SizedBox(height: SpacingTokens.xxxl), // Footer amélioré _buildFooter(), const SizedBox(height: SpacingTokens.giant), ], ), ), ), ), ); } Widget _buildBranding() { return ScaleTransition( scale: _scaleAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Logo animé avec effet de pulsation AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { return Transform.scale( scale: _pulseAnimation.value, child: Container( width: 64, height: 64, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: ColorTokens.primaryGradient, ), borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: [ BoxShadow( color: ColorTokens.primary.withOpacity(0.3), blurRadius: 24, offset: const Offset(0, 10), spreadRadius: 2, ), ], ), child: const Icon( Icons.account_balance_outlined, size: 32, color: ColorTokens.onPrimary, ), ), ); }, ), const SizedBox(height: SpacingTokens.xxxl), // Titre avec gradient ShaderMask( shaderCallback: (bounds) => const LinearGradient( colors: ColorTokens.primaryGradient, ).createShader(bounds), child: Text( 'Bienvenue', style: TypographyTokens.displaySmall.copyWith( color: Colors.white, fontWeight: FontWeight.w800, letterSpacing: -1, height: 1.1, ), ), ), const SizedBox(height: SpacingTokens.md), // Sous-titre élégant Text( 'Connectez-vous à votre espace UnionFlow', style: TypographyTokens.bodyLarge.copyWith( color: ColorTokens.onSurfaceVariant, fontWeight: FontWeight.w400, height: 1.5, letterSpacing: 0.2, ), ), ], ), ); } Widget _buildFeatureCards() { final features = [ { 'icon': Icons.account_balance_wallet_rounded, 'title': 'Cotisations', 'color': ColorTokens.primary, }, { 'icon': Icons.event_rounded, 'title': 'Événements', 'color': ColorTokens.secondary, }, { 'icon': Icons.volunteer_activism_rounded, 'title': 'Solidarité', 'color': ColorTokens.primary, }, ]; return Row( children: features.map((feature) { final index = features.indexOf(feature); return Expanded( child: Padding( padding: EdgeInsets.only( right: index < features.length - 1 ? SpacingTokens.md : 0, ), child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 600 + (index * 150)), curve: Curves.easeOutBack, builder: (context, value, child) { return Transform.scale( scale: value, child: Container( padding: const EdgeInsets.symmetric( vertical: SpacingTokens.lg, horizontal: SpacingTokens.sm, ), decoration: BoxDecoration( color: ColorTokens.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all( color: (feature['color'] as Color).withOpacity(0.15), width: 1.5, ), boxShadow: [ BoxShadow( color: ColorTokens.shadow.withOpacity(0.05), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( children: [ Container( padding: const EdgeInsets.all(SpacingTokens.sm), decoration: BoxDecoration( color: (feature['color'] as Color).withOpacity(0.1), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), ), child: Icon( feature['icon'] as IconData, size: 24, color: feature['color'] as Color, ), ), const SizedBox(height: SpacingTokens.sm), Text( feature['title'] as String, style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurface, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, ), ], ), ), ); }, ), ), ); }).toList(), ); } Widget _buildLoginCard() { return Container( decoration: BoxDecoration( color: ColorTokens.surface, borderRadius: BorderRadius.circular(SpacingTokens.radiusXxl), border: Border.all( color: ColorTokens.outline.withOpacity(0.08), width: 1, ), boxShadow: [ BoxShadow( color: ColorTokens.shadow.withOpacity(0.1), blurRadius: 32, offset: const Offset(0, 12), spreadRadius: -4, ), ], ), child: Padding( padding: const EdgeInsets.all(SpacingTokens.huge), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Titre de la card Row( children: [ Container( padding: const EdgeInsets.all(SpacingTokens.xs), decoration: BoxDecoration( color: ColorTokens.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(SpacingTokens.radiusSm), ), child: const Icon( Icons.fingerprint_rounded, size: 20, color: ColorTokens.primary, ), ), const SizedBox(width: SpacingTokens.md), Text( 'Authentification', style: TypographyTokens.titleMedium.copyWith( color: ColorTokens.onSurface, fontWeight: FontWeight.w700, ), ), ], ), const SizedBox(height: SpacingTokens.xxl), // Bouton de connexion principal _buildLoginButton(), const SizedBox(height: SpacingTokens.xxl), // Divider avec texte Row( children: [ Expanded( child: Container( height: 1, color: ColorTokens.outline.withOpacity(0.1), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md), child: Text( 'Sécurisé', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), ), Expanded( child: Container( height: 1, color: ColorTokens.outline.withOpacity(0.1), ), ), ], ), const SizedBox(height: SpacingTokens.xxl), // Informations de sécurité améliorées Container( padding: const EdgeInsets.all(SpacingTokens.lg), decoration: BoxDecoration( color: ColorTokens.primary.withOpacity(0.05), borderRadius: BorderRadius.circular(SpacingTokens.radiusMd), border: Border.all( color: ColorTokens.primary.withOpacity(0.1), width: 1, ), ), child: Row( children: [ const Icon( Icons.verified_user_rounded, size: 20, color: ColorTokens.primary, ), const SizedBox(width: SpacingTokens.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Connexion sécurisée', style: TypographyTokens.labelMedium.copyWith( color: ColorTokens.onSurface, fontWeight: FontWeight.w600, ), ), const SizedBox(height: SpacingTokens.xs), Text( 'Vos données sont protégées et chiffrées', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant, height: 1.3, ), ), ], ), ), ], ), ), ], ), ), ); } Widget _buildFooter() { return Column( children: [ // Aide Container( padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.lg, vertical: SpacingTokens.md, ), decoration: BoxDecoration( color: ColorTokens.surface.withOpacity(0.5), borderRadius: BorderRadius.circular(SpacingTokens.radiusLg), border: Border.all( color: ColorTokens.outline.withOpacity(0.08), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.help_outline_rounded, size: 18, color: ColorTokens.onSurfaceVariant.withOpacity(0.7), ), const SizedBox(width: SpacingTokens.sm), Text( 'Besoin d\'aide ?', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant.withOpacity(0.8), fontWeight: FontWeight.w500, ), ), ], ), ), const SizedBox(height: SpacingTokens.xl), // Copyright Text( '© 2025 UnionFlow. Tous droits réservés.', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant.withOpacity(0.5), letterSpacing: 0.3, ), textAlign: TextAlign.center, ), const SizedBox(height: SpacingTokens.xs), Text( 'Version 1.0.0', style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant.withOpacity(0.4), fontWeight: FontWeight.w500, fontSize: 11, ), textAlign: TextAlign.center, ), ], ); } Widget _buildLoginButton() { return BlocBuilder( builder: (context, state) { final isLoading = state is AuthLoading; return UFPrimaryButton( label: 'Se connecter', icon: Icons.login_rounded, onPressed: isLoading ? null : _handleLogin, isLoading: isLoading, isFullWidth: true, height: 56, ); }, ); } void _handleLogin() { // Démarrer l'authentification Keycloak context.read().add(const AuthLoginRequested()); } }