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 1–9 (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 l’utilisateur tape **8** ou **9** en premier, la saisie est ignorée (numéros spéciaux `08` / `09`). /// - S’il commence déjà par **0**, aucun préfixe n’est 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 frenchPhoneInputFormatters = [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), const FrenchPhoneNumberFormatter(), ];