feat(login): ajout du lien 'Mot de passe oublié ?' dans l'interface de connexion\n\n- Ajout du lien dans la page de connexion\n- Mise à jour du document d'évolution avec les spécifications de récupération de compte\n- Ajustements mineurs dans l'interface

This commit is contained in:
Julien Martin 2025-05-02 19:44:52 +02:00
parent 482040ba55
commit c8b8ad9318
9 changed files with 592 additions and 76 deletions

View File

@ -1,12 +1,27 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'screens/auth/login_screen.dart';
import 'screens/legal/legal_page.dart';
import 'screens/legal/privacy_page.dart';
void main() => runApp(const PtiPasApp());
final _router = GoRouter(routes: [
GoRoute(path: '/', builder: (_, __) => const LoginPage()),
]);
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (_, __) => const LoginPage(),
),
GoRoute(
path: '/legal',
builder: (_, __) => const LegalPage(),
),
GoRoute(
path: '/privacy',
builder: (_, __) => const PrivacyPage(),
),
],
);
class PtiPasApp extends StatelessWidget {
const PtiPasApp({super.key});
@ -17,6 +32,13 @@ class PtiPasApp extends StatelessWidget {
title: 'P\'titsPas',
routerConfig: _router,
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Merienda',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF8AD0C8),
brightness: Brightness.light,
),
),
);
}
}

View File

@ -2,10 +2,49 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:url_launcher/url_launcher.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
import 'package:go_router/go_router.dart';
class LoginPage extends StatelessWidget {
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -55,10 +94,12 @@ class LoginPage extends StatelessWidget {
Positioned(
right: 0,
bottom: 0,
width: w * 0.5, // Moitié droite de l'écran
height: h * 0.5, // Moitié basse de l'écran
width: w * 0.6, // 60% de la largeur de l'écran
height: h * 0.5, // 50% de la hauteur de l'écran
child: Padding(
padding: EdgeInsets.all(w * 0.02), // 2% de padding
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -100,8 +141,10 @@ class LoginPage extends StatelessWidget {
child: _ImageTextField(
bg: 'assets/images/field_email.png',
width: 400,
height: 80,
height: 53,
hint: 'Email',
controller: _emailController,
validator: _validateEmail,
),
),
const SizedBox(width: 20),
@ -109,28 +152,123 @@ class LoginPage extends StatelessWidget {
child: _ImageTextField(
bg: 'assets/images/field_password.png',
width: 400,
height: 80,
height: 53,
hint: 'Mot de passe',
obscure: true,
controller: _passwordController,
validator: _validatePassword,
),
),
],
),
const Spacer(),
const SizedBox(height: 20), // Réduit l'espacement
// Bouton centré
Center(
child: _ImageButton(
bg: 'assets/images/btn_green.png',
width: 300,
height: 60,
height: 40,
text: 'Se connecter',
textColor: const Color(0xFF8AD0C8), // Vert harmonieux
textColor: const Color(0xFF2D6A4F),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// TODO: Implémenter la logique de connexion
}
},
),
),
const SizedBox(height: 40),
const SizedBox(height: 10),
// Lien mot de passe oublié
Center(
child: TextButton(
onPressed: () {
// TODO: Implémenter la logique de récupération de mot de passe
},
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
fontSize: 14,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 10),
// Lien de création de compte
Center(
child: TextButton(
onPressed: () {
Navigator.pushNamed(context, '/parent-register');
},
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
),
const SizedBox(height: 20), // Réduit l'espacement en bas
],
),
),
),
),
// Pied de page
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_FooterLink(
text: 'Contact support',
onTap: () async {
final Uri emailLaunchUri = Uri(
scheme: 'mailto',
path: 'support@supernounou.local',
);
if (await canLaunchUrl(emailLaunchUri)) {
await launchUrl(emailLaunchUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Impossible d\'ouvrir le client mail',
style: GoogleFonts.merienda(),
),
),
);
}
},
),
_FooterLink(
text: 'Signaler un bug',
onTap: () {
_showBugReportDialog(context);
},
),
_FooterLink(
text: 'Mentions légales',
onTap: () {
Navigator.pushNamed(context, '/legal');
},
),
_FooterLink(
text: 'Politique de confidentialité',
onTap: () {
Navigator.pushNamed(context, '/privacy');
},
),
],
),
),
@ -150,6 +288,89 @@ class LoginPage extends StatelessWidget {
);
}
void _showBugReportDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Signaler un bug',
style: GoogleFonts.merienda(),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: controller,
maxLines: 5,
decoration: InputDecoration(
hintText: 'Décrivez le problème rencontré...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Annuler',
style: GoogleFonts.merienda(),
),
),
TextButton(
onPressed: () async {
if (controller.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Veuillez décrire le problème',
style: GoogleFonts.merienda(),
),
),
);
return;
}
try {
await BugReportService.sendReport(controller.text);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Rapport envoyé avec succès',
style: GoogleFonts.merienda(),
),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur lors de l\'envoi du rapport',
style: GoogleFonts.merienda(),
),
),
);
}
}
},
child: Text(
'Envoyer',
style: GoogleFonts.merienda(),
),
),
],
),
);
}
Future<ImageDimensions> _getImageDimensions() async {
final image = Image.asset('assets/images/river_logo_desktop.png');
final completer = Completer<ImageDimensions>();
@ -181,12 +402,17 @@ class _ImageTextField extends StatelessWidget {
final double height;
final String hint;
final bool obscure;
final TextEditingController? controller;
final String? Function(String?)? validator;
const _ImageTextField({
required this.bg,
required this.width,
required this.height,
required this.hint,
this.obscure = false,
this.controller,
this.validator,
});
@override
@ -200,24 +426,30 @@ class _ImageTextField extends StatelessWidget {
fit: BoxFit.fill,
),
),
child: TextField(
child: TextFormField(
controller: controller,
obscureText: obscure,
textAlign: TextAlign.left,
style: GoogleFonts.merienda(
fontSize: height * 0.25, // Réduction de la taille de la police
fontSize: height * 0.25,
color: Colors.black87,
),
validator: validator,
decoration: InputDecoration(
border: InputBorder.none,
hintText: hint,
hintStyle: GoogleFonts.merienda(
fontSize: height * 0.25, // Même taille pour le placeholder
fontSize: height * 0.25,
color: Colors.black38,
),
contentPadding: EdgeInsets.symmetric(
horizontal: width * 0.1,
vertical: height * 0.3,
),
errorStyle: GoogleFonts.merienda(
fontSize: height * 0.2,
color: Colors.red,
),
),
),
);
@ -255,17 +487,53 @@ class _ImageButton extends StatelessWidget {
fit: BoxFit.fill,
),
),
child: TextButton(
onPressed: onPressed,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
child: Center(
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: height * 0.3,
fontSize: height * 0.4,
color: textColor,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}
//
// Lien du pied de page
//
class _FooterLink extends StatelessWidget {
final String text;
final VoidCallback onTap;
const _FooterLink({
required this.text,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: 14,
color: Colors.black87,
decoration: TextDecoration.underline,
),
),
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class LegalPage extends StatelessWidget {
const LegalPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Mentions légales',
style: GoogleFonts.merienda(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Éditeur',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'P\'titsPas est une application développée pour les collectivités locales.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Hébergeur',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Les données sont hébergées sur des serveurs sécurisés en France.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Responsable du traitement',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Le responsable du traitement des données est la collectivité locale utilisatrice de l\'application.',
style: GoogleFonts.merienda(),
),
],
),
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class PrivacyPage extends StatelessWidget {
const PrivacyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Politique de confidentialité',
style: GoogleFonts.merienda(),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Protection des données personnelles',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'P\'titsPas s\'engage à protéger vos données personnelles conformément au RGPD.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Données collectées',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Les données collectées sont nécessaires au bon fonctionnement de l\'application et à la gestion des contrats de garde d\'enfants.',
style: GoogleFonts.merienda(),
),
const SizedBox(height: 32),
Text(
'Vos droits',
style: GoogleFonts.merienda(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'Vous disposez d\'un droit d\'accès, de rectification, d\'effacement et de portabilité de vos données.',
style: GoogleFonts.merienda(),
),
],
),
),
);
}
}

View File

@ -0,0 +1,28 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
class BugReportService {
static const String _apiUrl = 'https://api.supernounou.local/bug-reports';
static Future<void> sendReport(String description) async {
try {
final response = await http.post(
Uri.parse(_apiUrl),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'description': description,
'timestamp': DateTime.now().toIso8601String(),
'platform': 'web', // TODO: Ajouter la détection de la plateforme
}),
);
if (response.statusCode != 200) {
throw Exception('Erreur lors de l\'envoi du rapport');
}
} catch (e) {
rethrow;
}
}
}

View File

@ -161,7 +161,7 @@ packages:
source: hosted
version: "6.2.1"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
@ -525,6 +525,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
vector_math:
dependency: transitive
description:

View File

@ -15,6 +15,8 @@ dependencies:
shared_preferences: ^2.2.2
image_picker: ^1.0.7
js: ^0.6.7
url_launcher: ^6.2.4
http: ^1.2.0
dev_dependencies:
flutter_test:

View File

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST