Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,47 @@ CountryDropdown(
```


### Hiding the country code in formatted output

By default, `PhoneInputFormatter` includes the country code prefix in the formatted value (e.g. `+55 (11) 99999-9999`). Set `showCountryCode: false` to display only the local part of the number while the `unmasked` getter still returns the full international number.

> **Input contract:** The value fed to the formatter must still include the full international number (e.g. `+5511999999999`) so that the country can be auto-detected. Only the *display* is stripped — the digits used for detection are always the full number.

```dart
// Default — country code is visible
PhoneInputFormatter(
showCountryCode: true, // default
)
// input '+5511999999999' → masked: '+55 (11) 99999-9999'

// Hide country code
PhoneInputFormatter(
showCountryCode: false,
)
// input '+5511999999999' → masked: '(11) 99999-9999'
// → unmasked: '+5511999999999' ← full number preserved
```

The same flag is available on the `formatAsPhoneNumber` top-level function:

```dart
formatAsPhoneNumber('+5511999999999', showCountryCode: false);
// → '(11) 99999-9999'
```

**Using with `defaultCountryCode`:** when `defaultCountryCode` is already set the country code is hidden from the mask by default, so `showCountryCode` has no additional effect on the display. However, setting `showCountryCode: false` alongside `defaultCountryCode` ensures that `unmasked` returns the full international number (country code prepended):

```dart
PhoneInputFormatter(
defaultCountryCode: 'BR',
showCountryCode: false,
)
// input '11999999999' → masked: '(11) 99999-9999'
// → unmasked: '+5511999999999'
```

> **Note:** `unmasked` always returns the full international number (with `+` and country code) regardless of `showCountryCode`.

### Formatting a credit / debit card

<img src="https://github.com/caseyryan/images/blob/master/multi_formatter/card_format.gif?raw=true" width="240"/>
Expand Down
93 changes: 77 additions & 16 deletions lib/formatters/phone_input_formatter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class PhoneInputFormatter extends TextInputFormatter {
final bool shouldCorrectNumber;
final String? defaultCountryCode;

/// if [showCountryCode] is false, the formatted output will not include
/// the country code prefix (e.g. `(11) 99999-9999` instead of
/// `+55 (11) 99999-9999`). The [unmasked] getter will still return the
/// full international number with country code.
final bool showCountryCode;

PhoneCountryData? _countryData;
String _lastValue = '';

Expand All @@ -51,25 +57,39 @@ class PhoneInputFormatter extends TextInputFormatter {
/// [defaultCountryCode] if you set a default country code,
/// the phone will be formatted according to its country mask
/// and no leading country code will be present in the masked value
/// [showCountryCode] if false, the country code prefix is hidden from the
/// formatted output while [unmasked] still returns the full international number
PhoneInputFormatter({
this.onCountrySelected,
this.allowEndlessPhone = false,
this.shouldCorrectNumber = true,
this.defaultCountryCode,
this.showCountryCode = true,
});

String get masked => _lastValue;

String get unmasked => '+${toNumericString(
_lastValue,
allowHyphen: false,
allowAllZeroes: true,
)}';
String get unmasked {
final digits = toNumericString(
_lastValue,
allowHyphen: false,
allowAllZeroes: true,
);
if (!showCountryCode && _countryData?.phoneCode != null) {
return '+${_countryData!.phoneCode}$digits';
}
return '+$digits';
}

bool get isFilled => isPhoneValid(
masked,
defaultCountryCode: defaultCountryCode,
);
bool get isFilled {
if (!showCountryCode && defaultCountryCode == null) {
return isPhoneValid(unmasked);
}
return isPhoneValid(
masked,
defaultCountryCode: defaultCountryCode,
);
}

@override
TextEditingValue formatEditUpdate(
Expand Down Expand Up @@ -130,7 +150,8 @@ class PhoneInputFormatter extends TextInputFormatter {
}

final endOffset = newValue.text.length - newValue.selection.end;
final selectionEnd = maskedValue.length - endOffset;
final selectionEnd =
(maskedValue.length - endOffset).clamp(0, maskedValue.length);

_lastValue = maskedValue;
return TextEditingValue(
Expand Down Expand Up @@ -175,10 +196,16 @@ class PhoneInputFormatter extends TextInputFormatter {
}
}
if (_countryData != null) {
return _formatByMask(
final resolved = _resolveCountryCodeDisplay(
_countryData!,
defaultCountryCode,
showCountryCode,
numericString,
_countryData!.getCorrectMask(defaultCountryCode),
_countryData!.getCorrectAltMasks(defaultCountryCode),
);
return _formatByMask(
resolved.inputForMask,
_countryData!.getCorrectMask(resolved.effectiveCountryCode),
_countryData!.getCorrectAltMasks(resolved.effectiveCountryCode),
0,
allowEndlessPhone,
);
Expand Down Expand Up @@ -270,6 +297,33 @@ class PhoneInputFormatter extends TextInputFormatter {
}
}

class _PhoneDisplayResolution {
final String? effectiveCountryCode;
final String inputForMask;
const _PhoneDisplayResolution(this.effectiveCountryCode, this.inputForMask);
}

/// When [showCountryCode] is false and no [defaultCountryCode] is provided,
/// selects the trimmed (no-prefix) mask and strips the phone-code digits from
/// [input] so the formatter displays only the local portion of the number.
_PhoneDisplayResolution _resolveCountryCodeDisplay(
PhoneCountryData countryData,
String? defaultCountryCode,
bool showCountryCode,
String input,
) {
String? effectiveCountryCode = defaultCountryCode;
String inputForMask = input;
if (!showCountryCode && effectiveCountryCode == null) {
effectiveCountryCode = countryData.countryCode;
final phoneCode = countryData.phoneCode;
if (phoneCode != null && input.startsWith(phoneCode)) {
inputForMask = input.substring(phoneCode.length);
}
}
return _PhoneDisplayResolution(effectiveCountryCode, inputForMask);
}

bool isPhoneValid(
String phone, {
bool allowEndlessPhone = false,
Expand Down Expand Up @@ -331,6 +385,7 @@ String? formatAsPhoneNumber(
bool allowEndlessPhone = false,
String? defaultMask,
String? defaultCountryCode,
bool showCountryCode = true,
}) {
if (!isPhoneValid(
phone,
Expand Down Expand Up @@ -366,10 +421,16 @@ String? formatAsPhoneNumber(
}

if (countryData != null) {
return _formatByMask(
final resolved = _resolveCountryCodeDisplay(
countryData,
defaultCountryCode,
showCountryCode,
phone,
countryData.getCorrectMask(defaultCountryCode),
countryData.getCorrectAltMasks(defaultCountryCode),
);
return _formatByMask(
resolved.inputForMask,
countryData.getCorrectMask(resolved.effectiveCountryCode),
countryData.getCorrectAltMasks(resolved.effectiveCountryCode),
0,
allowEndlessPhone,
);
Expand Down
105 changes: 105 additions & 0 deletions test/flutter_multi_formatter_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import '../lib/flutter_multi_formatter.dart';
Expand Down Expand Up @@ -169,4 +170,108 @@ void main() {
});
});
});

group('showCountryCode', () {
test('default behavior (showCountryCode: true) shows country code', () {
final inputNumber = '+5511999999999';
final formattedNumber = PhoneInputFormatter()
.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: inputNumber),
)
.text;
expect(formattedNumber, '+55 (11) 99999-9999');
});

test('showCountryCode: false hides country code for BR number', () {
final inputNumber = '+5511999999999';
final formattedNumber =
PhoneInputFormatter(showCountryCode: false)
.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: inputNumber),
)
.text;
expect(formattedNumber, '(11) 99999-9999');
});

test('showCountryCode: false hides country code for US number', () {
final inputNumber = '+14444444444';
final formattedNumber =
PhoneInputFormatter(showCountryCode: false)
.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: inputNumber),
)
.text;
expect(formattedNumber, '(444) 444 4444');
});

test('showCountryCode: false handles partial input', () {
final inputNumber = '+5511';
// An explicit selection is required here because with showCountryCode: false
// the masked output is shorter than the raw input (country-code digits are
// hidden), so the default invalid selection (end = -1) would produce a
// negative selectionEnd and trigger a Flutter assertion.
final formattedNumber =
PhoneInputFormatter(showCountryCode: false)
.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(
text: inputNumber,
selection: TextSelection.collapsed(offset: inputNumber.length),
),
)
.text;
expect(formattedNumber, '(11');
});

test('unmasked getter returns full international number when showCountryCode: false', () {
final inputNumber = '+5511999999999';
final formatter = PhoneInputFormatter(showCountryCode: false);
formatter.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: inputNumber),
);
expect(formatter.unmasked, '+5511999999999');
});

test('formatAsPhoneNumber with showCountryCode: false hides country code', () {
final result = formatAsPhoneNumber(
'+5511999999999',
showCountryCode: false,
);
expect(result, '(11) 99999-9999');
});

test('isPhoneValid still works for full number', () {
expect(isPhoneValid('+5511999999999'), isTrue);
});

group('combined with defaultCountryCode', () {
// When defaultCountryCode is set, showCountryCode: false has no additional
// effect on the masked output (defaultCountryCode already hides the prefix),
// but unmasked should still return the full international number.
test('masked output is unaffected — defaultCountryCode already hides prefix', () {
final formattedNumber =
PhoneInputFormatter(defaultCountryCode: 'BR', showCountryCode: false)
.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: '11999999999'),
)
.text;
expect(formattedNumber, '(11) 99999-9999');
});

test('unmasked returns full international number', () {
final formatter =
PhoneInputFormatter(defaultCountryCode: 'BR', showCountryCode: false);
formatter.formatEditUpdate(
TextEditingValue(text: ''),
TextEditingValue(text: '11999999999'),
);
expect(formatter.unmasked, '+5511999999999');
});
});
});
}