Files
unionflow-server-api/unionflow-mobile-apps/lib/features/authentication/presentation/pages/login_page.dart
2025-11-17 16:02:04 +00:00

739 lines
24 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _backgroundController;
late AnimationController _pulseController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _backgroundAnimation;
late Animation<double> _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<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
));
_slideAnimation = Tween<Offset>(
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<double>(
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<double>(
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<double>(
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<void>(
builder: (context) => KeycloakWebViewAuthPage(
onAuthSuccess: (user) {
debugPrint('✅ Authentification réussie pour: ${user.fullName}');
debugPrint('🔄 Notification du BLoC avec les données utilisateur...');
context.read<AuthBloc>().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<AuthBloc, AuthState>(
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<double>(
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<AuthBloc, AuthState>(
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<AuthBloc>().add(const AuthLoginRequested());
}
}