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:
dahoud
2026-04-07 20:56:03 +00:00
parent 22f9c7e9a1
commit 70cbd1c873
63 changed files with 9316 additions and 6122 deletions

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import '../bloc/auth_bloc.dart';
@@ -20,16 +19,11 @@ class LoginPage extends StatefulWidget {
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late final AnimationController _fadeController;
late final AnimationController _slideController;
late final Animation<double> _fadeAnim;
late final Animation<Offset> _slideAnim;
bool _obscurePassword = true;
bool _rememberMe = false;
bool _biometricAvailable = false;
final _localAuth = LocalAuthentication();
@@ -50,15 +44,12 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
_fadeController.forward();
_slideController.forward();
_checkBiometrics();
_loadSavedCredentials();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@@ -70,17 +61,6 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
} catch (_) {}
}
Future<void> _loadSavedCredentials() async {
final prefs = await SharedPreferences.getInstance();
final remember = prefs.getBool('uf_remember_me') ?? false;
if (remember && mounted) {
setState(() {
_rememberMe = true;
_emailController.text = prefs.getString('uf_saved_email') ?? '';
});
}
}
Future<void> _authenticateBiometric() async {
try {
final ok = await _localAuth.authenticate(
@@ -88,12 +68,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
options: const AuthenticationOptions(stickyAuth: true, biometricOnly: false),
);
if (ok && mounted) {
final prefs = await SharedPreferences.getInstance();
final email = prefs.getString('uf_saved_email') ?? '';
final pass = prefs.getString('uf_saved_pass') ?? '';
if (email.isNotEmpty && pass.isNotEmpty) {
context.read<AuthBloc>().add(AuthLoginRequested(email, pass));
}
context.read<AuthBloc>().add(const AuthStatusChecked());
}
} catch (_) {}
}
@@ -110,24 +85,8 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
} catch (_) {}
}
Future<void> _onLogin() async {
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) return;
if (_rememberMe) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('uf_remember_me', true);
await prefs.setString('uf_saved_email', email);
await prefs.setString('uf_saved_pass', password);
} else {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('uf_remember_me');
await prefs.remove('uf_saved_email');
await prefs.remove('uf_saved_pass');
}
if (mounted) context.read<AuthBloc>().add(AuthLoginRequested(email, password));
void _onLogin() {
context.read<AuthBloc>().add(const AuthLoginRequested());
}
@override
@@ -267,51 +226,27 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
),
const SizedBox(height: 12),
_GlassTextField(
controller: _emailController,
hint: 'Email ou identifiant',
icon: Icons.person_outline_rounded,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 8),
_GlassTextField(
controller: _passwordController,
hint: 'Mot de passe',
icon: Icons.lock_outline_rounded,
isPassword: true,
obscure: _obscurePassword,
onToggleObscure: () => setState(() => _obscurePassword = !_obscurePassword),
),
const SizedBox(height: 8),
// Remember me + Forgot password
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_RememberMeToggle(
value: _rememberMe,
onChanged: (v) => setState(() => _rememberMe = v),
// Forgot password
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _openForgotPassword,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
TextButton(
onPressed: _openForgotPassword,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.roboto(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
decorationColor: Colors.white.withOpacity(0.7),
),
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.roboto(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
decorationColor: Colors.white.withOpacity(0.7),
),
),
],
),
),
const SizedBox(height: 16),
@@ -361,108 +296,6 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Sous-composants privés
// ─────────────────────────────────────────────────────────────────────────────
class _GlassTextField extends StatelessWidget {
const _GlassTextField({
required this.controller,
required this.hint,
required this.icon,
this.keyboardType,
this.isPassword = false,
this.obscure = false,
this.onToggleObscure,
});
final TextEditingController controller;
final String hint;
final IconData icon;
final TextInputType? keyboardType;
final bool isPassword;
final bool obscure;
final VoidCallback? onToggleObscure;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.13),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white.withOpacity(0.28), width: 1),
),
child: TextField(
controller: controller,
obscureText: isPassword && obscure,
keyboardType: keyboardType,
style: GoogleFonts.roboto(fontSize: 15, color: Colors.white),
decoration: InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.roboto(fontSize: 14.5, color: Colors.white.withOpacity(0.48)),
prefixIcon: Icon(icon, color: Colors.white54, size: 20),
suffixIcon: isPassword
? IconButton(
icon: Icon(
obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.white54,
size: 20,
),
onPressed: onToggleObscure,
)
: null,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
),
),
);
}
}
class _RememberMeToggle extends StatelessWidget {
const _RememberMeToggle({required this.value, required this.onChanged});
final bool value;
final ValueChanged<bool> onChanged;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onChanged(!value),
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 18,
height: 18,
child: Checkbox(
value: value,
onChanged: (v) => onChanged(v ?? false),
fillColor: WidgetStateProperty.resolveWith((s) {
if (s.contains(WidgetState.selected)) return Colors.white;
return Colors.transparent;
}),
checkColor: const Color(0xFF2E7D32),
side: const BorderSide(color: Colors.white60, width: 1.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
const SizedBox(width: 7),
Text(
'Se souvenir de moi',
style: GoogleFonts.roboto(
fontSize: 12,
color: Colors.white.withOpacity(0.78),
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Painters
// ─────────────────────────────────────────────────────────────────────────────