petitspas/frontend/lib/utils/phone_utils.dart
Julien Martin bf05b1d7d7 feat(inscription): email accusé réception, photos enfants, formulaires identité
- Backend: mail après inscription parent avec n° dossier, UPLOAD_PHOTOS_DIR, réponse API
- Frontend: imageBytes + payload photo, utils email/code postal/téléphone, champs dédiés
- Formulaires admin, login (focus), personal_info, child_card, custom_app_text_field

Made-with: Cursor
2026-03-31 00:04:38 +02:00

146 lines
4.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/services.dart';
/// Format français : 10 chiffres affichés par paires (ex. 06 12 34 56 78).
/// Ne garde que les chiffres (max 10).
String normalizePhone(String raw) {
final digits = raw.replaceAll(RegExp(r'\D'), '');
return digits.length > 10 ? digits.substring(0, 10) : digits;
}
/// Indique si [digitsOnly] (déjà normalisé par [normalizePhone]) commence par `0`.
/// Chaîne vide : `true` (pas encore de saisie).
bool frenchNationalPhoneStartsWithZero(String digitsOnly) {
if (digitsOnly.isEmpty) {
return true;
}
return digitsOnly.startsWith('0');
}
/// Validation téléphone France : 10 chiffres, commence par `0`, 2ᵉ chiffre 19 (format national).
///
/// Utiliser sur tout champ « numéro français » (inscription, admin, relais, etc.).
/// Si [allowEmpty] est `true`, une valeur vide ou blanche est acceptée.
String? validateFrenchNationalPhone(String? raw, {bool allowEmpty = false}) {
final trimmed = raw?.trim() ?? '';
if (trimmed.isEmpty) {
return allowEmpty ? null : 'Le numéro de téléphone est obligatoire.';
}
final digits = normalizePhone(trimmed);
if (digits.isEmpty) {
return allowEmpty ? null : 'Le numéro de téléphone est obligatoire.';
}
if (!frenchNationalPhoneStartsWithZero(digits)) {
return 'En France, le numéro doit commencer par 0 (ex. 06 12 34 56 78).';
}
if (digits.length < 10) {
return 'Le numéro doit contenir 10 chiffres.';
}
if (digits.length > 10) {
return 'Le numéro ne peut pas dépasser 10 chiffres.';
}
if (!RegExp(r'^0[1-9]\d{8}$').hasMatch(digits)) {
return 'Numéro de téléphone invalide.';
}
return null;
}
/// Retourne le numéro formaté pour l'affichage (ex. "06 12 34 56 78").
/// Si [raw] est vide après normalisation, retourne [raw] tel quel (pour afficher "" etc.).
String formatPhoneForDisplay(String raw) {
if (raw.trim().isEmpty) {
return raw;
}
final normalized = normalizePhone(raw);
if (normalized.isEmpty) {
return raw;
}
return formatFrenchPhoneDigits(normalized);
}
/// Affiche les chiffres par paires (ex. `0612345678` → `06 12 34 56 78`).
String formatFrenchPhoneDigits(String normalizedDigits) {
if (normalizedDigits.isEmpty) {
return '';
}
final buffer = StringBuffer();
for (var i = 0; i < normalizedDigits.length; i++) {
if (i > 0 && i.isEven) {
buffer.write(' ');
}
buffer.write(normalizedDigits[i]);
}
return buffer.toString();
}
int _cursorOffsetInFormattedPhone(String normalized, int digitCountBeforeCursor) {
final k = digitCountBeforeCursor.clamp(0, normalized.length);
if (k == 0) {
return 0;
}
return formatFrenchPhoneDigits(normalized.substring(0, k)).length;
}
/// Formatter de saisie : chiffres uniquement, paires espacées, max 10 chiffres.
///
/// - Si le premier chiffre est **1 à 7**, un **0** est ajouté automatiquement devant (ex. `6` → `06`).
/// - Si lutilisateur tape **8** ou **9** en premier, la saisie est ignorée (numéros spéciaux `08` / `09`).
/// - Sil commence déjà par **0**, aucun préfixe nest ajouté.
class FrenchPhoneNumberFormatter extends TextInputFormatter {
const FrenchPhoneNumberFormatter();
static const String _autoPrefixFirstDigits = '1234567';
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
var digits = newValue.text.replaceAll(RegExp(r'\D'), '');
var didPrependZero = false;
if (digits.isNotEmpty) {
final first = digits[0];
if (first == '8' || first == '9') {
return oldValue;
}
if (first != '0' && _autoPrefixFirstDigits.contains(first)) {
digits = '0$digits';
didPrependZero = true;
if (digits.length > 10) {
digits = digits.substring(0, 10);
}
} else if (first != '0') {
return oldValue;
}
}
final normalized = digits.length > 10 ? digits.substring(0, 10) : digits;
final formatted = formatFrenchPhoneDigits(normalized);
final sel = newValue.selection;
var digitsBeforeCursor = newValue.text
.substring(0, sel.start.clamp(0, newValue.text.length))
.replaceAll(RegExp(r'\D'), '')
.length;
if (didPrependZero) {
digitsBeforeCursor = (digitsBeforeCursor + 1).clamp(0, normalized.length);
}
final clampedOffset =
_cursorOffsetInFormattedPhone(normalized, digitsBeforeCursor).clamp(0, formatted.length);
return TextEditingValue(
text: formatted,
selection: TextSelection.collapsed(offset: clampedOffset),
);
}
}
/// Formatters à réutiliser sur tout champ téléphone France ([TextFormField], [CustomAppTextField], etc.).
final List<TextInputFormatter> frenchPhoneInputFormatters = [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
const FrenchPhoneNumberFormatter(),
];