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:
@@ -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
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user