feat(mobile): Implement Keycloak WebView authentication with HTTP callback

- Replace flutter_appauth with custom WebView implementation to resolve deep link issues
- Add KeycloakWebViewAuthService with integrated WebView for seamless authentication
- Configure Android manifest for HTTP cleartext traffic support
- Add network security config for development environment (192.168.1.11)
- Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback)
- Remove obsolete keycloak_auth_service.dart and temporary scripts
- Clean up dependencies and regenerate injection configuration
- Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F)

BREAKING CHANGE: Authentication flow now uses WebView instead of external browser
- Users will see Keycloak login page within the app instead of browser redirect
- Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues
- Maintains full OIDC compliance with PKCE flow and secure token storage

Technical improvements:
- WebView with custom navigation delegate for callback handling
- Automatic token extraction and user info parsing from JWT
- Proper error handling and user feedback
- Consistent authentication state management across app lifecycle
This commit is contained in:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View File

@@ -0,0 +1,446 @@
import 'package:flutter/material.dart';
import '../../shared/theme/app_theme.dart';
/// Animations de chargement personnalisées
class LoadingAnimations {
/// Indicateur de chargement avec points animés
static Widget dots({
Color color = AppTheme.primaryColor,
double size = 8.0,
Duration duration = const Duration(milliseconds: 1200),
}) {
return _DotsLoadingAnimation(
color: color,
size: size,
duration: duration,
);
}
/// Indicateur de chargement avec vagues
static Widget waves({
Color color = AppTheme.primaryColor,
double size = 40.0,
Duration duration = const Duration(milliseconds: 1000),
}) {
return _WavesLoadingAnimation(
color: color,
size: size,
duration: duration,
);
}
/// Indicateur de chargement avec rotation
static Widget spinner({
Color color = AppTheme.primaryColor,
double size = 40.0,
double strokeWidth = 4.0,
Duration duration = const Duration(milliseconds: 1000),
}) {
return _SpinnerLoadingAnimation(
color: color,
size: size,
strokeWidth: strokeWidth,
duration: duration,
);
}
/// Indicateur de chargement avec pulsation
static Widget pulse({
Color color = AppTheme.primaryColor,
double size = 40.0,
Duration duration = const Duration(milliseconds: 1000),
}) {
return _PulseLoadingAnimation(
color: color,
size: size,
duration: duration,
);
}
/// Skeleton loader pour les cartes
static Widget skeleton({
double height = 100.0,
double width = double.infinity,
BorderRadius? borderRadius,
Duration duration = const Duration(milliseconds: 1500),
}) {
return _SkeletonLoadingAnimation(
height: height,
width: width,
borderRadius: borderRadius ?? BorderRadius.circular(8),
duration: duration,
);
}
}
/// Animation de points qui rebondissent
class _DotsLoadingAnimation extends StatefulWidget {
final Color color;
final double size;
final Duration duration;
const _DotsLoadingAnimation({
required this.color,
required this.size,
required this.duration,
});
@override
State<_DotsLoadingAnimation> createState() => _DotsLoadingAnimationState();
}
class _DotsLoadingAnimationState extends State<_DotsLoadingAnimation>
with TickerProviderStateMixin {
late List<AnimationController> _controllers;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controllers = List.generate(3, (index) {
return AnimationController(
duration: widget.duration,
vsync: this,
);
});
_animations = _controllers.map((controller) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
);
}).toList();
_startAnimations();
}
void _startAnimations() {
for (int i = 0; i < _controllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 200), () {
if (mounted) {
_controllers[i].repeat(reverse: true);
}
});
}
}
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Container(
margin: EdgeInsets.symmetric(horizontal: widget.size * 0.2),
child: Transform.translate(
offset: Offset(0, -widget.size * _animations[index].value),
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
),
),
),
);
},
);
}),
);
}
}
/// Animation de vagues
class _WavesLoadingAnimation extends StatefulWidget {
final Color color;
final double size;
final Duration duration;
const _WavesLoadingAnimation({
required this.color,
required this.size,
required this.duration,
});
@override
State<_WavesLoadingAnimation> createState() => _WavesLoadingAnimationState();
}
class _WavesLoadingAnimationState extends State<_WavesLoadingAnimation>
with TickerProviderStateMixin {
late List<AnimationController> _controllers;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controllers = List.generate(4, (index) {
return AnimationController(
duration: widget.duration,
vsync: this,
);
});
_animations = _controllers.map((controller) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: controller, curve: Curves.easeInOut),
);
}).toList();
_startAnimations();
}
void _startAnimations() {
for (int i = 0; i < _controllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 150), () {
if (mounted) {
_controllers[i].repeat();
}
});
}
}
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.size,
height: widget.size,
child: Stack(
alignment: Alignment.center,
children: List.generate(4, (index) {
return AnimatedBuilder(
animation: _animations[index],
builder: (context, child) {
return Container(
width: widget.size * _animations[index].value,
height: widget.size * _animations[index].value,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: widget.color.withOpacity(1 - _animations[index].value),
width: 2,
),
),
);
},
);
}),
),
);
}
}
/// Animation de spinner personnalisé
class _SpinnerLoadingAnimation extends StatefulWidget {
final Color color;
final double size;
final double strokeWidth;
final Duration duration;
const _SpinnerLoadingAnimation({
required this.color,
required this.size,
required this.strokeWidth,
required this.duration,
});
@override
State<_SpinnerLoadingAnimation> createState() => _SpinnerLoadingAnimationState();
}
class _SpinnerLoadingAnimationState extends State<_SpinnerLoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: SizedBox(
width: widget.size,
height: widget.size,
child: CircularProgressIndicator(
strokeWidth: widget.strokeWidth,
valueColor: AlwaysStoppedAnimation<Color>(widget.color),
backgroundColor: widget.color.withOpacity(0.2),
),
),
);
},
);
}
}
/// Animation de pulsation
class _PulseLoadingAnimation extends StatefulWidget {
final Color color;
final double size;
final Duration duration;
const _PulseLoadingAnimation({
required this.color,
required this.size,
required this.duration,
});
@override
State<_PulseLoadingAnimation> createState() => _PulseLoadingAnimationState();
}
class _PulseLoadingAnimationState extends State<_PulseLoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = Tween<double>(begin: 0.8, end: 1.2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: widget.color,
shape: BoxShape.circle,
),
),
);
},
);
}
}
/// Animation skeleton pour le chargement de contenu
class _SkeletonLoadingAnimation extends StatefulWidget {
final double height;
final double width;
final BorderRadius borderRadius;
final Duration duration;
const _SkeletonLoadingAnimation({
required this.height,
required this.width,
required this.borderRadius,
required this.duration,
});
@override
State<_SkeletonLoadingAnimation> createState() => _SkeletonLoadingAnimationState();
}
class _SkeletonLoadingAnimationState extends State<_SkeletonLoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = Tween<double>(begin: -1.0, end: 2.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius,
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
stops: [
(_animation.value - 0.3).clamp(0.0, 1.0),
_animation.value.clamp(0.0, 1.0),
(_animation.value + 0.3).clamp(0.0, 1.0),
],
colors: const [
Color(0xFFE0E0E0),
Color(0xFFF5F5F5),
Color(0xFFE0E0E0),
],
),
),
);
},
);
}
}