diff --git a/packages/flutter_libphonenumber_platform_interface/lib/src/types/input_formatter.dart b/packages/flutter_libphonenumber_platform_interface/lib/src/types/input_formatter.dart index 645f06f..fa3c01e 100644 --- a/packages/flutter_libphonenumber_platform_interface/lib/src/types/input_formatter.dart +++ b/packages/flutter_libphonenumber_platform_interface/lib/src/types/input_formatter.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/services.dart'; import 'package:flutter_libphonenumber_platform_interface/flutter_libphonenumber_platform_interface.dart'; +import 'package:flutter_libphonenumber_platform_interface/src/types/recognize_country_data_by_phone.dart'; class LibPhonenumberTextFormatter extends TextInputFormatter { LibPhonenumberTextFormatter({ @@ -10,10 +11,8 @@ class LibPhonenumberTextFormatter extends TextInputFormatter { this.phoneNumberType = PhoneNumberType.mobile, this.phoneNumberFormat = PhoneNumberFormat.international, this.onFormatFinished, - - /// When true, mask will be applied assuming the input contains - /// a country code in it. - final bool inputContainsCountryCode = false, + this.onCountryRecognized, + this.inputContainsCountryCode = false, /// Additional digits to include this.additionalDigits = 0, @@ -52,6 +51,13 @@ class LibPhonenumberTextFormatter extends TextInputFormatter { /// Useful if you need to get the formatted value for something else to use. final FutureOr Function(String val)? onFormatFinished; + /// Optional callback that is called when it is possible to recognize the country as a result + /// of selecting a phone number from OS autofill suggestions or pasting from the clipboard + final void Function(CountryWithPhoneCode country)? onCountryRecognized; + + /// When true, mask will be applied assuming the input contains a country code in it. + final bool inputContainsCountryCode; + /// Allow additional digits on the end of the mask. This is useful for countries like Austria where the /// libphonenumber example number doesn't include all of the possibilities. This way we can still format /// the number but allow additional digits on the end. @@ -67,6 +73,14 @@ class LibPhonenumberTextFormatter extends TextInputFormatter { final TextEditingValue oldValue, final TextEditingValue newValue, ) { + + /// First, let's try to recognize the whole number + final recognizedValue = _tryRecognizePhoneWithCountry(oldValue, newValue); + if (recognizedValue != null) { + return recognizedValue; + } + + late final TextEditingValue result; /// Apply mask to the input @@ -209,4 +223,45 @@ class LibPhonenumberTextFormatter extends TextInputFormatter { return result; } } + + /// Recognizes phone from iOS suggestions or clipboard + TextEditingValue? _tryRecognizePhoneWithCountry( + final TextEditingValue oldValue, + final TextEditingValue newValue, + ) { + // user tapped on suggested phone or pasted from clipboard + // but this not sure, so we have additional checks inside + final seemsUserTappedSuggestionOrPasted = oldValue.text.isEmpty && newValue.text.length > 1 && newValue.text.startsWith('+'); + + if (seemsUserTappedSuggestionOrPasted) { + final CountryWithPhoneCode? recognizedCountry = recognizeCountryDataByPhone(newValue.text); + if (recognizedCountry != null) { + // build mask for recognized country + final rawMask = recognizedCountry.getPhoneMask(format: phoneNumberFormat, type: phoneNumberType, removeCountryCodeFromMask: false); + // print(' mask: $rawMask'); + final mask = PhoneMask(rawMask); + + // apply mask to the input + var maskedStr = mask.apply(newValue.text); + // print('masked: $maskedStr'); + + // tell parent code what we recognized country + // it can be used for update flag or prefix in another widget + onCountryRecognized?.call(recognizedCountry); + + // remove country code from already masked string + // 2 means one for the leading + and one for the space between country code and number + if (!inputContainsCountryCode) { + maskedStr = maskedStr.substring(recognizedCountry.phoneCode.length + 2); + } + + return TextEditingValue( + text: maskedStr, + selection: TextSelection.collapsed(offset: maskedStr.length), // force cursor to the end + ); + } + } + + return null; + } } diff --git a/packages/flutter_libphonenumber_platform_interface/lib/src/types/recognize_country_data_by_phone.dart b/packages/flutter_libphonenumber_platform_interface/lib/src/types/recognize_country_data_by_phone.dart new file mode 100644 index 0000000..2e6912b --- /dev/null +++ b/packages/flutter_libphonenumber_platform_interface/lib/src/types/recognize_country_data_by_phone.dart @@ -0,0 +1,44 @@ +import 'package:dlibphonenumber/dlibphonenumber.dart'; +// import 'package:collection/collection.dart'; // firstWhereOrNull extension, avoid dependency for one method +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter_libphonenumber_platform_interface/flutter_libphonenumber_platform_interface.dart'; + +/// Original method CountryWithPhoneCode.getCountryDataByPhone(phone) +/// has bad implementation because it based in substring comparison. +/// +/// For example for USA numbers it returns Bahamas, because these countries has same prefix +/// but Bahamas is alphabetically first +CountryWithPhoneCode? recognizeCountryDataByPhone(final String phone) { + // working with dlibphonenumber + final PhoneNumberUtil phoneUtil = PhoneNumberUtil.instance; + final PhoneNumber number; + try { + number = phoneUtil.parse(phone, 'ZZ'); + } on NumberParseException catch (e) { + if (kDebugMode) print('NumberParseException was thrown: ${e.toString()}'); + return null; + } + + // working with flutter_libphonenumber + final String? regionCode = phoneUtil.getRegionCodeForNumber(number); + // print('regionCode: $regionCode'); + if (regionCode != null) { + final country = CountryManager().countries.firstWhereOrNull((final country) => country.countryCode == regionCode); + return country; + } + return null; +} + + +/// part of https://pub.dev/packages/collection +extension IterableExtension on Iterable { + /// The first element satisfying [test], or `null` if there are none. + T? firstWhereOrNull(final bool Function(T element) test) { + for (final element in this) { + if (test(element)) { + return element; + } + } + return null; + } +} diff --git a/packages/flutter_libphonenumber_platform_interface/pubspec.yaml b/packages/flutter_libphonenumber_platform_interface/pubspec.yaml index 843d548..b1b1151 100644 --- a/packages/flutter_libphonenumber_platform_interface/pubspec.yaml +++ b/packages/flutter_libphonenumber_platform_interface/pubspec.yaml @@ -9,6 +9,7 @@ environment: flutter: ">=3.0.0" dependencies: + dlibphonenumber: ^1.1.12 flutter: sdk: flutter plugin_platform_interface: ^2.1.4 diff --git a/packages/flutter_libphonenumber_web/CHANGELOG.md b/packages/flutter_libphonenumber_web/CHANGELOG.md index d6659de..f613cab 100644 --- a/packages/flutter_libphonenumber_web/CHANGELOG.md +++ b/packages/flutter_libphonenumber_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.1 + +- Bump JS package. + ## 1.0.0 -* Initial implementation by laynor. +- Initial implementation by laynor. diff --git a/packages/flutter_libphonenumber_web/pubspec.yaml b/packages/flutter_libphonenumber_web/pubspec.yaml index 0482b61..66947fc 100644 --- a/packages/flutter_libphonenumber_web/pubspec.yaml +++ b/packages/flutter_libphonenumber_web/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_libphonenumber_web description: Web implementation of the flutter_libphonenumber plugin. repository: https://github.com/bottlepay/flutter_libphonenumber/tree/main/packages/flutter_libphonenumber_web issue_tracker: https://github.com/bottlepay/flutter_libphonenumber/issues -version: 1.0.0 +version: 1.0.1 environment: sdk: ">=2.19.0 <4.0.0"