fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
This commit is contained in:
@@ -1,327 +1,121 @@
|
||||
/// Page d'Authentification UnionFlow
|
||||
///
|
||||
/// Interface utilisateur pour la connexion sécurisée
|
||||
/// avec gestion complète des états et des erreurs.
|
||||
/// Interface utilisateur pour la connexion sécurisée via AppAuth (RFC 8252).
|
||||
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 '../../data/datasources/keycloak_webview_auth_service.dart';
|
||||
import 'package:flutter_appauth/flutter_appauth.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import '../../data/datasources/keycloak_auth_service.dart';
|
||||
import '../../data/datasources/keycloak_role_mapper.dart';
|
||||
import '../../data/models/user.dart';
|
||||
import '../../../../shared/design_system/tokens/color_tokens.dart';
|
||||
import '../../../../shared/design_system/tokens/spacing_tokens.dart';
|
||||
import '../../../../shared/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
|
||||
/// Page d'authentification Keycloak via AppAuth
|
||||
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
|
||||
this.timeoutSeconds = 300,
|
||||
});
|
||||
|
||||
@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;
|
||||
class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage> {
|
||||
bool _loading = true;
|
||||
String? _errorMessage;
|
||||
double _loadingProgress = 0.0;
|
||||
|
||||
|
||||
|
||||
// Paramètres d'authentification
|
||||
String? _authUrl;
|
||||
static const _appAuth = FlutterAppAuth();
|
||||
static const _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
_initializeAuthentication();
|
||||
_authenticate();
|
||||
}
|
||||
|
||||
@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 {
|
||||
Future<void> _authenticate() 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'];
|
||||
|
||||
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,
|
||||
final result = await _appAuth.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
KeycloakConfig.clientId,
|
||||
'dev.lions.unionflow-mobile://auth/callback',
|
||||
serviceConfiguration: AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint:
|
||||
'${KeycloakConfig.baseUrl}/realms/${KeycloakConfig.realm}/protocol/openid-connect/auth',
|
||||
tokenEndpoint: KeycloakConfig.tokenEndpoint,
|
||||
),
|
||||
scopes: ['openid', 'profile', 'email', 'roles', 'offline_access'],
|
||||
additionalParameters: {'kc_locale': 'fr'},
|
||||
allowInsecureConnections: true,
|
||||
),
|
||||
);
|
||||
|
||||
// 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(() {
|
||||
_loadingProgress = 0.0;
|
||||
});
|
||||
|
||||
_progressAnimationController.reset();
|
||||
}
|
||||
|
||||
/// Gère la fin du chargement d'une page
|
||||
void _onPageFinished(String url) {
|
||||
debugPrint('✅ Page chargée: $url');
|
||||
|
||||
setState(() {
|
||||
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.';
|
||||
if (result?.accessToken == null) {
|
||||
_onError('Authentification annulée ou échouée.');
|
||||
return;
|
||||
}
|
||||
|
||||
_handleError(errorMessage);
|
||||
await _storage.write(key: 'kc_access', value: result!.accessToken);
|
||||
if (result.refreshToken != null) {
|
||||
await _storage.write(key: 'kc_refresh', value: result.refreshToken);
|
||||
}
|
||||
if (result.idToken != null) {
|
||||
await _storage.write(key: 'kc_id', value: result.idToken);
|
||||
}
|
||||
|
||||
final accessPayload = JwtDecoder.decode(result.accessToken!);
|
||||
final idPayload = result.idToken != null ? JwtDecoder.decode(result.idToken!) : accessPayload;
|
||||
|
||||
final roles = _extractRoles(accessPayload);
|
||||
final primaryRole = KeycloakRoleMapper.mapToUserRole(roles);
|
||||
|
||||
final user = User(
|
||||
id: idPayload['sub'] ?? '',
|
||||
email: idPayload['email'] ?? '',
|
||||
firstName: idPayload['given_name'] ?? '',
|
||||
lastName: idPayload['family_name'] ?? '',
|
||||
primaryRole: primaryRole,
|
||||
additionalPermissions: KeycloakRoleMapper.mapToPermissions(roles),
|
||||
isActive: true,
|
||||
lastLoginAt: DateTime.now(),
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
widget.onAuthSuccess(user);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
_onError('Erreur d\'authentification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les erreurs
|
||||
void _handleError(String error) {
|
||||
setState(() {
|
||||
_authState = KeycloakWebViewAuthState.error;
|
||||
_errorMessage = error;
|
||||
});
|
||||
|
||||
_timeoutTimer?.cancel();
|
||||
|
||||
// Vibration pour indiquer l'erreur
|
||||
void _onError(String error) {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (mounted) setState(() { _loading = false; _errorMessage = error; });
|
||||
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 {
|
||||
@@ -329,92 +123,45 @@ class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage>
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _extractRoles(Map<String, dynamic> payload) {
|
||||
final roles = <String>[];
|
||||
if (payload['realm_access']?['roles'] != null) {
|
||||
roles.addAll((payload['realm_access']['roles'] as List).cast<String>());
|
||||
}
|
||||
if (payload['resource_access'] != null) {
|
||||
(payload['resource_access'] as Map).values.forEach((v) {
|
||||
if (v['roles'] != null) roles.addAll((v['roles'] as List).cast<String>());
|
||||
});
|
||||
}
|
||||
return roles.where((r) => !r.startsWith('default-roles-') && r != 'offline_access').toList();
|
||||
}
|
||||
|
||||
@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 Sécurisée',
|
||||
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',
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorTokens.primary,
|
||||
foregroundColor: ColorTokens.onPrimary,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
'Connexion Sécurisée',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
color: ColorTokens.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
],
|
||||
bottom: _buildProgressIndicator(),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _handleCancel,
|
||||
tooltip: 'Annuler',
|
||||
),
|
||||
),
|
||||
body: _errorMessage != null ? _buildErrorView() : _buildLoadingView(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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: const 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() {
|
||||
Widget _buildLoadingView() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -422,95 +169,14 @@ class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage>
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
Text(
|
||||
'Initialisation...',
|
||||
style: TypographyTokens.bodyLarge.copyWith(
|
||||
color: ColorTokens.onSurface,
|
||||
),
|
||||
'Connexion en cours...',
|
||||
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(
|
||||
'Connexion en cours...',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
color: ColorTokens.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xl),
|
||||
Text(
|
||||
'Veuillez patienter pendant que nous\nvérifions vos informations.',
|
||||
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: const 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,
|
||||
@@ -526,19 +192,11 @@ class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage>
|
||||
color: ColorTokens.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
_authState == KeycloakWebViewAuthState.timeout
|
||||
? Icons.access_time
|
||||
: Icons.error_outline,
|
||||
color: ColorTokens.onError,
|
||||
size: 48,
|
||||
),
|
||||
child: const Icon(Icons.error_outline, color: ColorTokens.onError, size: 48),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xxxl),
|
||||
Text(
|
||||
_authState == KeycloakWebViewAuthState.timeout
|
||||
? 'Délai d\'attente dépassé'
|
||||
: 'Erreur de connexion',
|
||||
'Erreur de connexion',
|
||||
style: TypographyTokens.headlineSmall.copyWith(
|
||||
color: ColorTokens.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -557,7 +215,10 @@ class _KeycloakWebViewAuthPageState extends State<KeycloakWebViewAuthPage>
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initializeAuthentication,
|
||||
onPressed: () {
|
||||
setState(() { _loading = true; _errorMessage = null; });
|
||||
_authenticate();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
Reference in New Issue
Block a user