@@ -142,6 +142,7 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
142142 final phoneUtil = PhoneNumberUtil .instance;
143143 int ? maxLength;
144144
145+ /// Digits tapés par l'utilisateur
145146 @override
146147 void initState () {
147148 super .initState ();
@@ -266,19 +267,19 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
266267 /// Center block: main text input
267268 Expanded (
268269 child: TextField (
269- inputFormatters: [
270- FilteringTextInputFormatter .digitsOnly,
271- if (maxLength != null ) LengthLimitingTextInputFormatter (maxLength),
272- ],
270+ controller: widget.controller,
273271 cursorColor: inputTextTextModifier.getCursorTextColor (state, isError),
274272 focusNode: effectiveFocusNode,
275- controller: widget.controller,
276273 keyboardType: widget.keyboardType,
277274 style: theme.typographyTokens.typeLabelDefaultLarge (context).copyWith (
278275 color: inputTextTextModifier.getTextColor (state, isError),
279276 ),
280277 enabled: widget.enabled,
281278 readOnly: widget.readOnly ?? false ,
279+ inputFormatters: [
280+ FilteringTextInputFormatter .digitsOnly,
281+ MaxDigitsFormatter (getMaxDigitsFromLib (countrySelected.code)), // block extra digits
282+ ],
282283 onTap: () {
283284 // send text tapped to parent
284285 widget.onEditingComplete? .call (widget.controller? .text ?? '' );
@@ -296,7 +297,7 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
296297 widget.onEditingComplete? .call (value);
297298 },
298299 onChanged: (value) {
299- _onCountryChanged (value, limitedDigits, formattedNumber );
300+ _onCountryChanged (value);
300301 },
301302 decoration: InputDecoration (
302303 border: InputBorder .none,
@@ -356,73 +357,74 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
356357
357358 /// Function `_onCountryChanged`
358359 ///
359- /// This function is triggered when the selected country or phone number changes.
360- /// It updates the phone number formatting based on the selected country,
361- /// limits the number length according to the country, and updates the text controller.
362- ///
363- /// Parameters :
364- /// - `value` : The new input value of the phone number entered by the user.
365- /// - `limitedDigits` : The string containing only digits of the number, limited to the maximum length.
366- /// - `formattedNumber` : The formatted version of the phone number, which will be updated in the controller.
367- ///
368- /// Behavior :
369- /// - Determines the selected country based on the prefix or current selection.
370- /// - Cleans the number to keep only digits.
371- /// - Limits the number length based on the country configuration.
372- /// - Formats the number in national or international format depending on the configuration.
373- /// - Updates the text controller with the formatted number, maintaining the cursor position.
374- /// - If an error occurs during parsing or formatting, retains the raw digit version.
375- void _onCountryChanged (String value, String limitedDigits, String ? formattedNumber) {
376- // Select a Country if prefix of decoration is add
377- // if we take the prefix from current local or country selected
378- if (widget.decoration.prefix != null ) {
379- countrySelected = CountryService ().findCountryByPrefix (widget.decoration.prefix! ) ?? Country .empty ();
380- } else {
381- countrySelected = widget.countrySelector? .selectedCountry ?? Country .empty ();
382- }
383- // Clean the input to keep only digits
384- final digitsOnly = value.replaceAll (RegExp (r'\D' ), '' );
385-
386- // Retrieve the maximum allowed length for the selected country
387- maxLength = getMaxDigitsFromLib (countrySelected.code);
388- // Store the maximum length for future reference
389- //maxLength = maxDigits;
390-
391- // Limit the input to the maximum length
392- limitedDigits = digitsOnly;
393- if (maxLength != null && digitsOnly.length > maxLength! ) {
394- limitedDigits = digitsOnly.substring (0 , maxLength);
395- }
360+ /// This function is triggered when the user changes the country selection or updates the phone number input.
361+ /// It formats the phone number based on the selected country, enforces a maximum digit length,
362+ /// and updates the text controller with the formatted number while maintaining cursor position.
363+ ///
364+ /// Parameters:
365+ /// - `value` : The current input value of the phone number entered by the user.
366+ ///
367+ /// Behavior:
368+ /// - Checks if the formatter is already processing to prevent re-entrancy.
369+ /// - Retrieves the selected country or defaults to an empty country.
370+ /// - Strips all non-digit characters from the input.
371+ /// - Retrieves the maximum allowed digits for the selected country.
372+ /// - Limits the number of digits to the maximum allowed.
373+ /// - Parses and validates the number using a phone utility library.
374+ /// - Formats the number in national format if valid.
375+ /// - Updates the text controller with the formatted number, preserving cursor position.
376+ /// - Handles parsing errors gracefully and retains the raw digit string if formatting fails.
377+ bool _isFormatting = false ;
378+
379+ void _onCountryChanged (String value) {
380+ if (_isFormatting) return ;
381+ _isFormatting = true ;
396382
397- // Format the number
398383 try {
399- if (widget.countrySelector == null ) {
400- // Parse and format as national number if country selector is disabled
401- final parsedNumber = phoneUtil.parse (limitedDigits, countrySelected.code.toUpperCase ());
402- formattedNumber = phoneUtil.format (parsedNumber, PhoneNumberFormat .national);
403- } else {
404- // Parse and format as international number if country selector is enabled
405- final parsedNumber = phoneUtil.parse (limitedDigits, countrySelected.code.toUpperCase ());
406- String phoneNumber = phoneUtil.format (parsedNumber, PhoneNumberFormat .international);
407- // Convert international number to national format
408- formattedNumber = getNationalNumber (phoneNumber, countrySelected.code.toUpperCase ());
384+ countrySelected = widget.countrySelector? .selectedCountry ?? Country .empty ();
385+
386+ // Remove non-digit characters
387+ String digitsOnly = value.replaceAll (RegExp (r'\D' ), '' );
388+
389+ // Get max digits for country
390+ int ? maxLength = getMaxDigitsFromLib (countrySelected.code);
391+ debugPrint ("🌍 Country selected: ${countrySelected .code }" );
392+ debugPrint ("🔢 Digits only: $digitsOnly (raw input: $value )" );
393+ debugPrint ("📏 Max length from lib: $maxLength " );
394+
395+ // Limit digits if longer than max
396+ if (digitsOnly.length > maxLength) {
397+ digitsOnly = digitsOnly.substring (0 , maxLength);
398+ debugPrint ("✂️ Cutting digits from ${digitsOnly .length } to $maxLength " );
409399 }
410400
411- // Update the controller's value with the formatted number
412- final selectionIndex = widget.controller ? .selection.baseOffset ?? 0 ;
401+ String formattedNumber = digitsOnly;
402+ bool isValidNumber = false ;
413403
414- widget.controller? .value = TextEditingValue (
415- text: formattedNumber! ,
416- // Keep the cursor position after the inserted formatted number
417- selection: TextSelection .collapsed (offset: selectionIndex + (formattedNumber.length)),
418- );
419- } catch (e) {
420- // If an error occurs during parsing or formatting, keep the raw digits
421- widget.controller? .value = TextEditingValue (
422- text: limitedDigits,
423- // Place cursor at the end of the input
424- selection: TextSelection .collapsed (offset: limitedDigits.length),
425- );
404+ try {
405+ final parsed = phoneUtil.parse (digitsOnly, countrySelected.code.toUpperCase ());
406+ isValidNumber = phoneUtil.isValidNumber (parsed);
407+
408+ if (isValidNumber) {
409+ formattedNumber = phoneUtil.format (parsed, PhoneNumberFormat .national);
410+ debugPrint ("🎯 Formatted national: $formattedNumber " );
411+ }
412+ } catch (e) {
413+ debugPrint ("🚨 Parsing error: $e " );
414+ }
415+
416+ debugPrint ("✅ Is valid: $isValidNumber " );
417+
418+ // Update controller only if text actually changes
419+ if (widget.controller? .text != formattedNumber) {
420+ debugPrint ("✏️ Final text update: $formattedNumber " );
421+ widget.controller? .value = TextEditingValue (
422+ text: formattedNumber,
423+ selection: TextSelection .collapsed (offset: formattedNumber.length),
424+ );
425+ }
426+ } finally {
427+ _isFormatting = false ;
426428 }
427429 }
428430
@@ -639,15 +641,15 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
639641 /// Returns:
640642 /// - The maximum number of digits as an integer if successful.
641643 /// - Null if the example number cannot be retrieved or an error occurs.
642- int ? getMaxDigitsFromLib (String countryCode) {
644+ int getMaxDigitsFromLib (String countryCode) {
643645 try {
644646 final exampleNumber = phoneUtil.getExampleNumber (countryCode.toUpperCase ());
645647 if (exampleNumber != null && exampleNumber.nationalNumber.toString ().isNotEmpty) {
646648 return exampleNumber.nationalNumber.toString ().length;
647649 }
648650 //}
649651 } catch (_) {}
650- return null ;
652+ return 0 ;
651653 }
652654
653655 /// Converts an international phone number to its national format for a specific country.
@@ -675,3 +677,39 @@ class _OudsPhoneNumberInputState extends State<OudsPhoneNumberInput> {
675677 }
676678 }
677679}
680+
681+ /// A custom [TextInputFormatter] that limits the number of digits a user can input.
682+ ///
683+ /// This formatter allows only numeric input and enforces a maximum number of digits.
684+ /// It strips out all non-digit characters and prevents further input once the limit is reached.
685+ ///
686+ /// Parameters:
687+ /// - [maxDigits] : The maximum number of digits allowed in the input.
688+ ///
689+ /// Usage:
690+ /// ```dart
691+ /// TextField(
692+ /// keyboardType: TextInputType.number,
693+ /// inputFormatters: [MaxDigitsFormatter(10)],
694+ /// )
695+ /// ```
696+ /// This example restricts the input to a maximum of 10 digits.
697+ class MaxDigitsFormatter extends TextInputFormatter {
698+ final int maxDigits;
699+
700+ /// Creates a [MaxDigitsFormatter] with the specified maximum number of digits.
701+ MaxDigitsFormatter (this .maxDigits);
702+
703+ @override
704+ TextEditingValue formatEditUpdate (
705+ TextEditingValue oldValue,
706+ TextEditingValue newValue,
707+ ) {
708+ final digits = newValue.text.replaceAll (RegExp (r'\D' ), '' );
709+ if (digits.length > maxDigits) {
710+ debugPrint ("🛑 Blocked at $maxDigits digits" );
711+ return oldValue; // Stop typing
712+ }
713+ return newValue;
714+ }
715+ }
0 commit comments