739 lines
24 KiB
Dart
739 lines
24 KiB
Dart
/// 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());
|
||
}
|
||
}
|