Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/core/animations/page_transitions.dart
DahoudG f89f6167cc 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
2025-09-15 01:44:16 +00:00

300 lines
9.0 KiB
Dart

import 'package:flutter/material.dart';
/// Transitions de pages personnalisées pour une meilleure UX
class PageTransitions {
/// Transition de glissement depuis la droite (par défaut iOS)
static PageRouteBuilder<T> slideFromRight<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 300),
reverseTransitionDuration: const Duration(milliseconds: 250),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
/// Transition de glissement depuis le bas
static PageRouteBuilder<T> slideFromBottom<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 350),
reverseTransitionDuration: const Duration(milliseconds: 300),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeOutCubic;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
/// Transition de fondu
static PageRouteBuilder<T> fadeIn<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 300),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
}
/// Transition d'échelle avec fondu
static PageRouteBuilder<T> scaleWithFade<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 300),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const curve = Curves.easeInOutCubic;
var scaleTween = Tween(begin: 0.8, end: 1.0).chain(
CurveTween(curve: curve),
);
var fadeTween = Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: curve),
);
return ScaleTransition(
scale: animation.drive(scaleTween),
child: FadeTransition(
opacity: animation.drive(fadeTween),
child: child,
),
);
},
);
}
/// Transition de rotation avec échelle
static PageRouteBuilder<T> rotateScale<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 500),
reverseTransitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const curve = Curves.elasticOut;
var scaleTween = Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: curve),
);
var rotationTween = Tween(begin: 0.5, end: 1.0).chain(
CurveTween(curve: Curves.easeInOut),
);
return ScaleTransition(
scale: animation.drive(scaleTween),
child: RotationTransition(
turns: animation.drive(rotationTween),
child: child,
),
);
},
);
}
/// Transition personnalisée avec effet de rebond
static PageRouteBuilder<T> bounceIn<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 600),
reverseTransitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const curve = Curves.bounceOut;
var scaleTween = Tween(begin: 0.3, end: 1.0).chain(
CurveTween(curve: curve),
);
return ScaleTransition(
scale: animation.drive(scaleTween),
child: child,
);
},
);
}
/// Transition de glissement avec parallaxe
static PageRouteBuilder<T> slideWithParallax<T>(Widget page) {
return PageRouteBuilder<T>(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionDuration: const Duration(milliseconds: 350),
reverseTransitionDuration: const Duration(milliseconds: 300),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const primaryBegin = Offset(1.0, 0.0);
const primaryEnd = Offset.zero;
const secondaryBegin = Offset.zero;
const secondaryEnd = Offset(-0.3, 0.0);
const curve = Curves.easeInOut;
var primaryTween = Tween(begin: primaryBegin, end: primaryEnd).chain(
CurveTween(curve: curve),
);
var secondaryTween = Tween(begin: secondaryBegin, end: secondaryEnd).chain(
CurveTween(curve: curve),
);
return Stack(
children: [
SlideTransition(
position: secondaryAnimation.drive(secondaryTween),
child: Container(), // Page précédente
),
SlideTransition(
position: animation.drive(primaryTween),
child: child,
),
],
);
},
);
}
}
/// Extensions pour faciliter l'utilisation des transitions
extension NavigatorTransitions on NavigatorState {
/// Navigation avec transition de glissement depuis la droite
Future<T?> pushSlideFromRight<T>(Widget page) {
return push<T>(PageTransitions.slideFromRight<T>(page));
}
/// Navigation avec transition de glissement depuis le bas
Future<T?> pushSlideFromBottom<T>(Widget page) {
return push<T>(PageTransitions.slideFromBottom<T>(page));
}
/// Navigation avec transition de fondu
Future<T?> pushFadeIn<T>(Widget page) {
return push<T>(PageTransitions.fadeIn<T>(page));
}
/// Navigation avec transition d'échelle et fondu
Future<T?> pushScaleWithFade<T>(Widget page) {
return push<T>(PageTransitions.scaleWithFade<T>(page));
}
/// Navigation avec transition de rebond
Future<T?> pushBounceIn<T>(Widget page) {
return push<T>(PageTransitions.bounceIn<T>(page));
}
/// Navigation avec transition de parallaxe
Future<T?> pushSlideWithParallax<T>(Widget page) {
return push<T>(PageTransitions.slideWithParallax<T>(page));
}
}
/// Widget d'animation pour les éléments de liste
class AnimatedListItem extends StatefulWidget {
final Widget child;
final int index;
final Duration delay;
final Duration duration;
final Curve curve;
final Offset slideOffset;
const AnimatedListItem({
super.key,
required this.child,
required this.index,
this.delay = const Duration(milliseconds: 100),
this.duration = const Duration(milliseconds: 500),
this.curve = Curves.easeOutCubic,
this.slideOffset = const Offset(0, 50),
});
@override
State<AnimatedListItem> createState() => _AnimatedListItemState();
}
class _AnimatedListItemState extends State<AnimatedListItem>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: widget.curve,
));
_slideAnimation = Tween<Offset>(
begin: widget.slideOffset,
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: widget.curve,
));
// Démarrer l'animation avec un délai basé sur l'index
Future.delayed(
Duration(milliseconds: widget.delay.inMilliseconds * widget.index),
() {
if (mounted) {
_controller.forward();
}
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: _slideAnimation.value,
child: Opacity(
opacity: _fadeAnimation.value,
child: widget.child,
),
);
},
);
}
}