Initial commit: app Fideliter client (Flutter)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:32:39 +02:00
commit 01a0f1029f
49 changed files with 2613 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import 'complete_profile_screen.dart';
import 'home_client_screen.dart';
import 'welcome_screen.dart';
/// Aiguille entre l'accueil non connecté, la finalisation de profil et l'app.
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
if (!SupabaseConfig.estConfigure) return const _EcranNonConfigure();
return StreamBuilder<AuthState>(
stream: supabase.auth.onAuthStateChange,
builder: (context, snapshot) {
final session = supabase.auth.currentSession;
if (session == null) return const WelcomeScreen();
return _SessionChargee(key: ValueKey(session.user.id));
},
);
}
}
/// Charge les données du client connecté, puis affiche l'app (ou la
/// finalisation de profil si le compte n'a pas encore de fiche fidélité).
class _SessionChargee extends StatefulWidget {
const _SessionChargee({super.key});
@override
State<_SessionChargee> createState() => _SessionChargeeState();
}
class _SessionChargeeState extends State<_SessionChargee> {
late Future<void> _chargement;
@override
void initState() {
super.initState();
_chargement = SessionClient.instance.charger();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _chargement,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return AnimatedBuilder(
animation: SessionClient.instance,
builder: (context, _) {
final aProfil = SessionClient.instance.monClient != null;
return aProfil
? const HomeClientScreen()
: const CompleteProfileScreen();
},
);
},
);
}
}
class _EcranNonConfigure extends StatelessWidget {
const _EcranNonConfigure();
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.cloud_off, size: 56, color: Colors.grey),
SizedBox(height: 16),
Text('Supabase non configuré',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
SizedBox(height: 8),
Text(
'Renseignez l\'URL et la clé anon dans lib/supabase_config.dart.',
textAlign: TextAlign.center,
style: TextStyle(color: Color(0xFF6B6B72)),
),
],
),
),
),
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter/material.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
/// Affiché quand le compte est connecté mais n'a pas encore de fiche fidélité
/// (ex : après une inscription avec confirmation d'email). On crée le profil.
class CompleteProfileScreen extends StatefulWidget {
const CompleteProfileScreen({super.key});
@override
State<CompleteProfileScreen> createState() => _CompleteProfileScreenState();
}
class _CompleteProfileScreenState extends State<CompleteProfileScreen> {
final _formKey = GlobalKey<FormState>();
final _nomCtrl = TextEditingController();
final _prenomCtrl = TextEditingController();
final _telCtrl = TextEditingController();
bool _enCours = false;
String? _erreur;
@override
void dispose() {
_nomCtrl.dispose();
_prenomCtrl.dispose();
_telCtrl.dispose();
super.dispose();
}
Future<void> _valider() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
await SessionClient.instance.creerProfil(
nom: _nomCtrl.text,
prenom: _prenomCtrl.text,
telephone: _telCtrl.text,
);
// creerProfil recharge la session → l'AuthGate affiche l'accueil.
} catch (e) {
if (mounted) {
setState(() {
_erreur = 'Erreur : $e';
_enCours = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Finaliser mon profil'),
actions: [
IconButton(
tooltip: 'Se déconnecter',
icon: const Icon(Icons.logout),
onPressed: () => supabase.auth.signOut(),
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Encore une étape pour activer votre carte de fidélité.',
style: TextStyle(color: AppTheme.grisTexte),
),
const SizedBox(height: 20),
TextFormField(
controller: _nomCtrl,
textCapitalization: TextCapitalization.words,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person_outline),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Nom obligatoire' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _prenomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Prénom',
prefixIcon: Icon(Icons.badge_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _telCtrl,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Téléphone (facultatif)',
prefixIcon: Icon(Icons.phone_outlined),
),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _valider,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Activer ma carte'),
),
],
),
),
),
),
);
}
}
+309
View File
@@ -0,0 +1,309 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../models/mouvement.dart';
import '../models/recompense.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
import '../utils/format.dart';
/// Écran principal du client : sa carte de fidélité (points + QR), les
/// récompenses qu'il peut viser, et son historique.
class HomeClientScreen extends StatefulWidget {
const HomeClientScreen({super.key});
@override
State<HomeClientScreen> createState() => _HomeClientScreenState();
}
class _HomeClientScreenState extends State<HomeClientScreen>
with WidgetsBindingObserver {
final _session = SessionClient.instance;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Rafraîchit le solde au retour dans l'app (après un passage en caisse).
if (state == AppLifecycleState.resumed) _session.charger();
}
Future<void> _deconnexion() async {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Se déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Déconnexion'),
),
],
),
);
if (ok == true) {
_session.vider();
await supabase.auth.signOut();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_session.nomMagasin.isEmpty
? 'Ma fidélité'
: _session.nomMagasin),
actions: [
IconButton(
tooltip: 'Rafraîchir',
icon: const Icon(Icons.refresh),
onPressed: () => _session.charger(),
),
IconButton(
tooltip: 'Se déconnecter',
icon: const Icon(Icons.logout),
onPressed: _deconnexion,
),
],
),
body: AnimatedBuilder(
animation: _session,
builder: (context, _) {
final client = _session.monClient;
if (client == null) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () => _session.charger(),
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
children: [
_carteFidelite(client.nomComplet, client.code, client.points),
const SizedBox(height: 24),
if (_session.recompenses.isNotEmpty) ...[
_titre('Mes récompenses'),
const SizedBox(height: 8),
..._session.recompenses.map(
(r) => _ligneRecompense(r, client.points),
),
const SizedBox(height: 24),
],
_titre('Historique'),
const SizedBox(height: 8),
_historique(),
],
),
);
},
),
);
}
Widget _carteFidelite(String nom, String code, double pts) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
child: Column(
children: [
Text(nom,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
const Icon(Icons.stars_rounded,
color: AppTheme.accent, size: 34),
const SizedBox(width: 8),
Text(
pointsNombre(pts),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
color: AppTheme.accent),
),
const SizedBox(width: 6),
const Padding(
padding: EdgeInsets.only(bottom: 8),
child: Text('pts',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.accent)),
),
],
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE0E0E4)),
),
child: QrImageView(
data: code,
version: QrVersions.auto,
size: 200,
eyeStyle: const QrEyeStyle(
eyeShape: QrEyeShape.square,
color: AppTheme.noir,
),
dataModuleStyle: const QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: AppTheme.noir,
),
),
),
const SizedBox(height: 12),
Text(code,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 2)),
const SizedBox(height: 6),
const Text(
'Présentez ce code en caisse pour cumuler vos points.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: AppTheme.grisTexte),
),
],
),
),
);
}
Widget _ligneRecompense(Recompense r, double solde) {
final atteignable = solde >= r.coutPoints;
final progression =
r.coutPoints <= 0 ? 1.0 : (solde / r.coutPoints).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
CircleAvatar(
backgroundColor: atteignable
? AppTheme.accent.withValues(alpha: 0.14)
: Colors.grey.withValues(alpha: 0.12),
child: Icon(Icons.card_giftcard,
color: atteignable ? AppTheme.accent : Colors.grey),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(r.nom,
style: const TextStyle(fontWeight: FontWeight.w700)),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progression,
minHeight: 6,
backgroundColor: Colors.grey.withValues(alpha: 0.18),
color: AppTheme.accent,
),
),
const SizedBox(height: 6),
Text(
atteignable
? 'Disponible ! (${points(r.coutPoints)})'
: 'Encore ${points(r.coutPoints - solde)} • coût ${points(r.coutPoints)}',
style: TextStyle(
fontSize: 12,
color: atteignable
? AppTheme.accent
: AppTheme.grisTexte,
fontWeight:
atteignable ? FontWeight.w700 : FontWeight.w500,
),
),
],
),
),
],
),
),
),
);
}
Widget _historique() {
final mvts = _session.mouvements;
if (mvts.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: Text('Aucun mouvement pour l\'instant.',
style: TextStyle(color: AppTheme.grisTexte)),
),
);
}
return Column(children: mvts.map(_ligneMouvement).toList());
}
Widget _ligneMouvement(Mouvement m) {
final gain = m.estGain;
final couleur = gain ? AppTheme.accent : Colors.red.shade600;
final icone = switch (m.type) {
TypeMouvement.facture => Icons.receipt_long,
TypeMouvement.recompense => Icons.card_giftcard,
_ => Icons.tune,
};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: couleur.withValues(alpha: 0.12),
child: Icon(icone, size: 20, color: couleur),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(m.libelle,
style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 2),
Text(dateHeure(m.date),
style: const TextStyle(
fontSize: 12, color: AppTheme.grisTexte)),
],
),
),
Text(
'${gain ? '+' : ''}${points(m.points.abs())}',
style: TextStyle(fontWeight: FontWeight.w700, color: couleur),
),
],
),
);
}
Widget _titre(String texte) => Text(texte,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700));
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../supabase_config.dart';
/// Connexion d'un client existant (email + mot de passe).
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
final _mdpCtrl = TextEditingController();
bool _enCours = false;
bool _voirMdp = false;
String? _erreur;
@override
void dispose() {
_emailCtrl.dispose();
_mdpCtrl.dispose();
super.dispose();
}
Future<void> _connexion() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
await supabase.auth.signInWithPassword(
email: _emailCtrl.text.trim(),
password: _mdpCtrl.text,
);
// L'AuthGate prend le relais automatiquement (pop de cet écran).
if (mounted) Navigator.of(context).pop();
} on AuthException catch (e) {
if (mounted) setState(() => _erreur = e.message);
} catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally {
if (mounted) setState(() => _enCours = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Connexion')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
),
validator: (v) =>
(v == null || !v.contains('@')) ? 'Email invalide' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _mdpCtrl,
obscureText: !_voirMdp,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_voirMdp ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _voirMdp = !_voirMdp),
),
),
validator: (v) =>
(v == null || v.isEmpty) ? 'Mot de passe requis' : null,
onFieldSubmitted: (_) => _connexion(),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _connexion,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Se connecter'),
),
],
),
),
),
),
);
}
}
+192
View File
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../services/session_client.dart';
import '../supabase_config.dart';
import '../theme.dart';
/// Création d'un compte client : email + mot de passe + profil (nom, téléphone).
/// Le code de fidélité et le QR sont générés automatiquement côté serveur.
class SignupScreen extends StatefulWidget {
const SignupScreen({super.key});
@override
State<SignupScreen> createState() => _SignupScreenState();
}
class _SignupScreenState extends State<SignupScreen> {
final _formKey = GlobalKey<FormState>();
final _nomCtrl = TextEditingController();
final _prenomCtrl = TextEditingController();
final _telCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _mdpCtrl = TextEditingController();
bool _enCours = false;
bool _voirMdp = false;
String? _erreur;
@override
void dispose() {
_nomCtrl.dispose();
_prenomCtrl.dispose();
_telCtrl.dispose();
_emailCtrl.dispose();
_mdpCtrl.dispose();
super.dispose();
}
Future<void> _inscription() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_enCours = true;
_erreur = null;
});
try {
final res = await supabase.auth.signUp(
email: _emailCtrl.text.trim(),
password: _mdpCtrl.text,
);
if (res.session != null) {
// Connecté directement (confirmation d'email désactivée) → on crée le
// profil fidélité. L'AuthGate basculera ensuite vers l'accueil.
await SessionClient.instance.creerProfil(
nom: _nomCtrl.text,
prenom: _prenomCtrl.text,
telephone: _telCtrl.text,
);
if (mounted) Navigator.of(context).pop();
} else {
// Confirmation d'email requise : le profil sera finalisé à la 1re
// connexion (après validation du mail).
if (mounted) await _popupVerifierEmail();
}
} on AuthException catch (e) {
if (mounted) setState(() => _erreur = e.message);
} catch (e) {
if (mounted) setState(() => _erreur = 'Erreur : $e');
} finally {
if (mounted) setState(() => _enCours = false);
}
}
Future<void> _popupVerifierEmail() async {
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Vérifiez votre email'),
content: const Text(
'Un email de confirmation vous a été envoyé. Validez-le puis '
'connectez-vous pour finaliser votre carte de fidélité.'),
actions: [
FilledButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
if (mounted) Navigator.of(context).pop(); // retour à l'accueil
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Créer un compte')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Nom',
prefixIcon: Icon(Icons.person_outline),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Nom obligatoire' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _prenomCtrl,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Prénom',
prefixIcon: Icon(Icons.badge_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _telCtrl,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Téléphone (facultatif)',
prefixIcon: Icon(Icons.phone_outlined),
),
),
const SizedBox(height: 14),
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline),
),
validator: (v) =>
(v == null || !v.contains('@')) ? 'Email invalide' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _mdpCtrl,
obscureText: !_voirMdp,
decoration: InputDecoration(
labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_voirMdp ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _voirMdp = !_voirMdp),
),
),
validator: (v) => (v == null || v.length < 6)
? '6 caractères minimum'
: null,
onFieldSubmitted: (_) => _inscription(),
),
if (_erreur != null) ...[
const SizedBox(height: 16),
Text(_erreur!,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.red.shade600)),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _enCours ? null : _inscription,
child: _enCours
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Créer mon compte'),
),
const SizedBox(height: 12),
const Text(
'Votre QR code de fidélité sera généré automatiquement.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13, color: AppTheme.grisTexte),
),
],
),
),
),
),
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import '../theme.dart';
import 'login_screen.dart';
import 'signup_screen.dart';
/// Premier écran : le client choisit de se connecter ou de créer un compte.
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(28),
child: Column(
children: [
const Spacer(flex: 2),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppTheme.accent.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: const Icon(Icons.card_membership,
size: 56, color: AppTheme.accent),
),
const SizedBox(height: 28),
const Text(
'Votre carte de fidélité',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 26, fontWeight: FontWeight.w800),
),
const SizedBox(height: 10),
const Text(
'Cumulez des points à chaque achat et profitez de récompenses.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: AppTheme.grisTexte),
),
const Spacer(flex: 3),
FilledButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SignupScreen()),
),
child: const Text('Créer un compte'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LoginScreen()),
),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
child: const Text('J\'ai déjà un compte'),
),
const SizedBox(height: 12),
],
),
),
),
);
}
}