From 97b23880f906c5ce3de369e0afedf55dbb83dff1 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 5 Feb 2024 00:54:29 +0100 Subject: [PATCH] Refactor (#212) * fix merge issue * remove generated files from git * saving * refactor * improve search ux * refactor * saving * refactor * rm * upgrade flag cache * cache * saving * refa * refactor * comment * validation * moving tests * saving * language * fixing tests * fix tests * saving * saving * saving * fix all tests --- .gitignore | 6 +- .pubignore | 36 - CHANGELOG.md | 18 + README.md | 82 +- example/.pubignore | 76 -- example/lib/main.dart | 104 ++- example/pubspec.lock | 24 +- example/windows/flutter/CMakeLists.txt | 7 +- .../flutter/generated_plugin_registrant.cc | 11 - .../flutter/generated_plugin_registrant.h | 15 - example/windows/runner/Runner.rc | 10 +- l10n.yaml | 5 +- {lib/l10n => l10n}/ar.arb | 0 {lib/l10n => l10n}/ckb.arb | 0 {lib/l10n => l10n}/de.arb | 0 {lib/l10n => l10n}/el.arb | 0 {lib/l10n => l10n}/en.arb | 1 + {lib/l10n => l10n}/es.arb | 0 {lib/l10n => l10n}/fa.arb | 0 {lib/l10n => l10n}/fr.arb | 0 {lib/l10n => l10n}/hi.arb | 0 {lib/l10n => l10n}/it.arb | 0 {lib/l10n => l10n}/ku.arb | 0 {lib/l10n => l10n}/nb.arb | 0 {lib/l10n => l10n}/nl.arb | 0 {lib/l10n => l10n}/pt.arb | 0 {lib/l10n => l10n}/ru.arb | 0 {lib/l10n => l10n}/sv.arb | 0 {lib/l10n => l10n}/tr.arb | 0 {lib/l10n => l10n}/uk.arb | 0 {lib/l10n => l10n}/uz.arb | 0 {lib/l10n => l10n}/zh.arb | 0 lib/phone_form_field.dart | 15 +- lib/src/controllers/phone_controller.dart | 28 - .../controllers/phone_field_controller.dart | 64 -- lib/src/country/country_button.dart | 80 ++ lib/src/country/localized_country.dart | 43 + .../country_finder.dart | 62 +- .../country_list_view.dart} | 45 +- .../country_selector.dart | 108 ++- .../country_selector_controller.dart | 61 ++ .../country_selector_navigator.dart | 74 +- .../country_selector_page.dart | 151 ++++ lib/src/country_selection/no_result_view.dart | 20 + lib/src/country_selection/search_box.dart | 70 ++ .../generated/phone_field_localization.dart | 120 ++- .../phone_field_localization_ar.dart | 5 +- .../phone_field_localization_ckb.dart | 758 ++++++++++++++++++ .../phone_field_localization_de.dart | 5 +- .../phone_field_localization_el.dart | 8 +- .../phone_field_localization_en.dart | 5 +- .../phone_field_localization_es.dart | 5 +- .../phone_field_localization_fa.dart | 5 +- .../phone_field_localization_fr.dart | 8 +- .../phone_field_localization_hi.dart | 5 +- .../phone_field_localization_it.dart | 5 +- .../phone_field_localization_ku.dart | 758 ++++++++++++++++++ .../phone_field_localization_nb.dart | 5 +- .../phone_field_localization_nl.dart | 5 +- .../phone_field_localization_pt.dart | 5 +- .../phone_field_localization_ru.dart | 8 +- .../phone_field_localization_sv.dart | 5 +- .../phone_field_localization_tr.dart | 8 +- .../phone_field_localization_uk.dart | 8 +- .../phone_field_localization_uz.dart | 8 +- .../phone_field_localization_zh.dart | 5 +- lib/src/localization/localization.dart | 251 ++++++ lib/src/phone_controller.dart | 108 +++ lib/src/phone_form_field.dart | 196 +++++ lib/src/phone_form_field_state.dart | 196 +++++ .../allowed_characters.dart} | 2 +- .../localized_validation_error.dart | 3 + lib/src/validation/phone_validator.dart | 141 +--- lib/src/validation/validator_translator.dart | 45 -- lib/src/widgets/country_code_chip.dart | 63 -- lib/src/widgets/country_selector/country.dart | 35 - .../country_selector_page.dart | 152 ---- .../localized_country_registry.dart | 287 ------- .../widgets/country_selector/search_box.dart | 105 --- lib/src/widgets/phone_field.dart | 127 --- lib/src/widgets/phone_field_state.dart | 196 ----- lib/src/widgets/phone_form_field.dart | 229 ------ lib/src/widgets/phone_form_field_state.dart | 114 --- pubspec.yaml | 13 +- test/_country_selector_navigator_test.dart | 9 +- test/_country_selector_test.dart | 304 ++----- test/phone_form_field_test.dart | 524 +++++++----- test/phone_validator_test.dart | 203 ----- 88 files changed, 3591 insertions(+), 2672 deletions(-) delete mode 100644 .pubignore delete mode 100644 example/.pubignore delete mode 100644 example/windows/flutter/generated_plugin_registrant.cc delete mode 100644 example/windows/flutter/generated_plugin_registrant.h rename {lib/l10n => l10n}/ar.arb (100%) rename {lib/l10n => l10n}/ckb.arb (100%) rename {lib/l10n => l10n}/de.arb (100%) rename {lib/l10n => l10n}/el.arb (100%) rename {lib/l10n => l10n}/en.arb (99%) rename {lib/l10n => l10n}/es.arb (100%) rename {lib/l10n => l10n}/fa.arb (100%) rename {lib/l10n => l10n}/fr.arb (100%) rename {lib/l10n => l10n}/hi.arb (100%) rename {lib/l10n => l10n}/it.arb (100%) rename {lib/l10n => l10n}/ku.arb (100%) rename {lib/l10n => l10n}/nb.arb (100%) rename {lib/l10n => l10n}/nl.arb (100%) rename {lib/l10n => l10n}/pt.arb (100%) rename {lib/l10n => l10n}/ru.arb (100%) rename {lib/l10n => l10n}/sv.arb (100%) rename {lib/l10n => l10n}/tr.arb (100%) rename {lib/l10n => l10n}/uk.arb (100%) rename {lib/l10n => l10n}/uz.arb (100%) rename {lib/l10n => l10n}/zh.arb (100%) delete mode 100644 lib/src/controllers/phone_controller.dart delete mode 100644 lib/src/controllers/phone_field_controller.dart create mode 100644 lib/src/country/country_button.dart create mode 100644 lib/src/country/localized_country.dart rename lib/src/{widgets/country_selector => country_selection}/country_finder.dart (54%) rename lib/src/{widgets/country_selector/country_list.dart => country_selection/country_list_view.dart} (74%) rename lib/src/{widgets/country_selector => country_selection}/country_selector.dart (55%) create mode 100644 lib/src/country_selection/country_selector_controller.dart rename lib/src/{widgets/country_selector => country_selection}/country_selector_navigator.dart (86%) create mode 100644 lib/src/country_selection/country_selector_page.dart create mode 100644 lib/src/country_selection/no_result_view.dart create mode 100644 lib/src/country_selection/search_box.dart rename lib/{l10n => src/localization}/generated/phone_field_localization.dart (93%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ar.dart (99%) create mode 100644 lib/src/localization/generated/phone_field_localization_ckb.dart rename lib/{l10n => src/localization}/generated/phone_field_localization_de.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_el.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_en.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_es.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_fa.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_fr.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_hi.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_it.dart (99%) create mode 100644 lib/src/localization/generated/phone_field_localization_ku.dart rename lib/{l10n => src/localization}/generated/phone_field_localization_nb.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_nl.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_pt.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_ru.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_sv.dart (99%) rename lib/{l10n => src/localization}/generated/phone_field_localization_tr.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_uk.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_uz.dart (98%) rename lib/{l10n => src/localization}/generated/phone_field_localization_zh.dart (99%) create mode 100644 lib/src/localization/localization.dart create mode 100644 lib/src/phone_controller.dart create mode 100644 lib/src/phone_form_field.dart create mode 100644 lib/src/phone_form_field_state.dart rename lib/src/{constants/patterns.dart => validation/allowed_characters.dart} (90%) create mode 100644 lib/src/validation/localized_validation_error.dart delete mode 100644 lib/src/validation/validator_translator.dart delete mode 100644 lib/src/widgets/country_code_chip.dart delete mode 100644 lib/src/widgets/country_selector/country.dart delete mode 100644 lib/src/widgets/country_selector/country_selector_page.dart delete mode 100644 lib/src/widgets/country_selector/localized_country_registry.dart delete mode 100644 lib/src/widgets/country_selector/search_box.dart delete mode 100644 lib/src/widgets/phone_field.dart delete mode 100644 lib/src/widgets/phone_field_state.dart delete mode 100644 lib/src/widgets/phone_form_field.dart delete mode 100644 lib/src/widgets/phone_form_field_state.dart delete mode 100644 test/phone_validator_test.dart diff --git a/.gitignore b/.gitignore index 5bce2af0..2aad77e9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ .dart_tool/ .packages build/ -example/build \ No newline at end of file +example/build +**/generated_plugin_registrant.dart +**/generated_plugin_registrant.cc +**/generated_plugin_registrant.h +**/generated_plugin_registrant.cmake diff --git a/.pubignore b/.pubignore deleted file mode 100644 index d01dc04c..00000000 --- a/.pubignore +++ /dev/null @@ -1,36 +0,0 @@ -docs/ -example/build -example/windows -example/web -example/android -example/ios -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ -demo_image.png - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.packages -build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3524e354..aa938b90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [9.0.0] + +- Big Internal refactor in the hope of making contribution easier +- Various fixes for country selection UX +- Various fixes for input cursor issues +- Improve accessibility touches surfaces +- Improve accessibility labels +- Some visual tweaks +- Added some missing countries +- [Breaking] : no validation done by default +- [Breaking] : provided validators now require a context parameter +- [Breaking] : `LocalizedCountryRegistry` removed. If you were using it to localize a country name, you should use `PhoneFieldLocalization.of(context).countryName(isoCode)`. +- [Deprecated] : `isCountryChipPersistent` in favor of `isCountryButtonPersistent`. +- [Deprecated] : `shouldFormat`, it is now always ON by default +- [Deprecated] : `defaultCountry`, you should now use either `initialValue` or provide a controller with an initial value. +- [Deprecated] : `CountrySelectorNavigator.searchDelegate()` changed into `CountrySelectorNavigator.PageNavigator()`. + + ## [8.1.1] - Upgraded phone_numbers_parser lib to 8.1.0 - Added norwegian language (PR #203) thanks @sidlatau diff --git a/README.md b/README.md index 9ea08993..cd176039 100644 --- a/README.md +++ b/README.md @@ -5,52 +5,42 @@ Flutter phone input integrated with flutter internationalization ## Features - Totally cross platform, this is a dart only package / dependencies -- Internationalization +- Internationalization: many languages supported +- Semantics - Phone formatting localized by region - Phone number validation (built-in validators included for main use cases) -- Support autofill and copy paste -- Extends Flutter's FormField +- Support auto fill and copy paste +- Form field - Uses dart phone_numbers_parser for parsing - ## Demo Demo available at https://cedvdb.github.io/phone_form_field/ - ## Usage ```dart - -// works without any param PhoneFormField(); -// all params +/// params PhoneFormField( - key: Key('phone-field') - controller: null, // controller & initialValue value - initialValue: null, // can't be supplied simultaneously - shouldFormat: true // default - defaultCountry: IsoCode.US, // default - decoration: InputDecoration( - labelText: 'Phone', // default to null - border: OutlineInputBorder() // default to UnderlineInputBorder(), - // ... - ), - validator: PhoneValidator.validMobile(), // default PhoneValidator.valid() - isCountryChipPersistent: false, // default - isCountrySelectionEnabled: true, // default - countrySelectorNavigator: CountrySelectorNavigator.bottomSheet(), - showFlagInInput: true, // default - flagSize: 16, // default - autofillHints: [AutofillHints.telephoneNumber], // default to null - enabled: true, // default - autofocus: false, // default - onSaved: (PhoneNumber p) => print('saved $p'), // default null - onChanged: (PhoneNumber p) => print('saved $p'), // default null - // ... + other textfield params -) - + initialValue: PhoneNumber.parse('+33'), // or use the controller + validator: PhoneValidator.compose( + [PhoneValidator.required(), PhoneValidator.validMobile()]), + countrySelectorNavigator: const CountrySelectorNavigator.page(), + onChanged: (phoneNumber) => print('changed into $phoneNumber'), + enabled: true, + countryButtonPadding: null, + isCountrySelectionEnabled: true, + isCountryButtonPersistent: true, + showDialCode: true, + showIsoCodeInInput: true, + showFlagInInput: true, + flagSize: 16 + // + all parameters of TextField + // + all parameters of FormField + // ... +); ``` ## Validation @@ -68,7 +58,6 @@ PhoneFormField( ### Validators details * Each validator has an optional `errorText` property to override built-in translated text -* Most of them have an optional `allowEmpty` (default is true) preventing to flag an empty field as valid. Consider using a composed validator with a first `PhoneValidator.required` when a different text is needed for empty field. ### Composing validators @@ -103,8 +92,8 @@ Here are the list of the parameters available for all built-in country selector ### Built-in country selector -* **CountrySelectorNavigator.searchDelegate** - Open a dialog to select the country. +* **CountrySelectorNavigator.page** + Open a page to select the country. No extra parameters * **CountrySelectorNavigator.dialog** @@ -134,11 +123,11 @@ Here are the list of the parameters available for all built-in country selector ### Custom Country Selector Navigator You can use your own country selector by creating a class that implements `CountrySelectorNavigator` -It has one required method `navigate` expected to return the selected country: +It has one required method `show` expected to return the selected country: ```dart class CustomCountrySelectorNavigator implements CountrySelectorNavigator { - Future navigate(BuildContext context) { + Future show(BuildContext context) { // ask user for a country and return related `Country` class } } @@ -179,22 +168,23 @@ PhoneFormField( - 'ar', - 'de', + - 'el', - 'en', - - 'el' - 'es', + - 'fa', - 'fr', - - 'hin', + - 'hi', - 'it', + - 'ku', - 'nb', - 'nl', - 'pt', - 'ru', - - 'uz', - - 'uk', - - 'tr', - - 'zh', - 'sv', + - 'tr', + - 'uk', + - 'uz', + - 'zh', - - If one of the language you target is not supported you can submit a - pull request with the translated file in src/l10n +If one of the language you target is not supported you can submit a + pull request diff --git a/example/.pubignore b/example/.pubignore deleted file mode 100644 index b349b8a0..00000000 --- a/example/.pubignore +++ /dev/null @@ -1,76 +0,0 @@ -docs/ - -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 diff --git a/example/lib/main.dart b/example/lib/main.dart index 2f0b915b..1cb7192d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,35 +11,33 @@ void main() { // For a simpler example see the README class PhoneFieldView extends StatelessWidget { - final Key inputKey; final PhoneController controller; + final FocusNode focusNode; final CountrySelectorNavigator selectorNavigator; final bool withLabel; final bool outlineBorder; - final bool shouldFormat; - final bool isCountryChipPersistent; + final bool isCountryButtonPersistant; final bool mobileOnly; final bool useRtl; const PhoneFieldView({ Key? key, - required this.inputKey, required this.controller, + required this.focusNode, required this.selectorNavigator, required this.withLabel, required this.outlineBorder, - required this.shouldFormat, - required this.isCountryChipPersistent, + required this.isCountryButtonPersistant, required this.mobileOnly, required this.useRtl, }) : super(key: key); - PhoneNumberInputValidator? _getValidator() { + PhoneNumberInputValidator? _getValidator(BuildContext context) { List validators = []; if (mobileOnly) { - validators.add(PhoneValidator.validMobile()); + validators.add(PhoneValidator.validMobile(context)); } else { - validators.add(PhoneValidator.valid()); + validators.add(PhoneValidator.valid(context)); } return validators.isNotEmpty ? PhoneValidator.compose(validators) : null; } @@ -50,13 +48,12 @@ class PhoneFieldView extends StatelessWidget { child: Directionality( textDirection: useRtl ? TextDirection.rtl : TextDirection.ltr, child: PhoneFormField( - key: inputKey, + focusNode: focusNode, controller: controller, - shouldFormat: shouldFormat && !useRtl, + isCountryButtonPersistent: isCountryButtonPersistant, autofocus: false, autofillHints: const [AutofillHints.telephoneNumber], countrySelectorNavigator: selectorNavigator, - defaultCountry: IsoCode.US, decoration: InputDecoration( label: withLabel ? const Text('Phone') : null, border: outlineBorder @@ -67,14 +64,13 @@ class PhoneFieldView extends StatelessWidget { enabled: true, showIsoCodeInInput: false, showFlagInInput: true, - validator: _getValidator(), + validator: _getValidator(context), autovalidateMode: AutovalidateMode.onUserInteraction, cursorColor: Theme.of(context).colorScheme.primary, // ignore: avoid_print onSaved: (p) => print('saved $p'), // ignore: avoid_print onChanged: (p) => print('changed $p'), - isCountryChipPersistent: isCountryChipPersistent, ), ), ); @@ -106,7 +102,7 @@ class MyApp extends StatelessWidget { ], title: 'Phone field demo', theme: ThemeData( - brightness: Brightness.light, + brightness: Brightness.dark, primarySwatch: Colors.blue, ), home: const PhoneFormFieldScreen(), @@ -123,21 +119,21 @@ class PhoneFormFieldScreen extends StatefulWidget { class PhoneFormFieldScreenState extends State { late PhoneController controller; + final FocusNode focusNode = FocusNode(); + bool outlineBorder = true; bool mobileOnly = true; - bool shouldFormat = true; - bool isCountryChipPersistent = false; + bool isCountryButtonPersistent = true; bool withLabel = true; bool useRtl = false; CountrySelectorNavigator selectorNavigator = - const CountrySelectorNavigator.searchDelegate(); + const CountrySelectorNavigator.page(); final formKey = GlobalKey(); - final phoneKey = GlobalKey>(); @override initState() { super.initState(); - controller = PhoneController(null); + controller = PhoneController(); controller.addListener(() => setState(() {})); } @@ -150,7 +146,6 @@ class PhoneFormFieldScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - // drawer: AppDrawer(), appBar: AppBar( title: const Text('Phone_form_field'), ), @@ -174,9 +169,9 @@ class PhoneFormFieldScreenState extends State { title: const Text('Label'), ), SwitchListTile( - value: isCountryChipPersistent, + value: isCountryButtonPersistent, onChanged: (v) => - setState(() => isCountryChipPersistent = v), + setState(() => isCountryButtonPersistent = v), title: const Text('Persistent country chip'), ), SwitchListTile( @@ -184,11 +179,6 @@ class PhoneFormFieldScreenState extends State { onChanged: (v) => setState(() => mobileOnly = v), title: const Text('Mobile phone number only'), ), - SwitchListTile( - value: shouldFormat, - onChanged: (v) => setState(() => shouldFormat = v), - title: const Text('Should format'), - ), SwitchListTile( value: useRtl, onChanged: (v) { @@ -211,7 +201,8 @@ class PhoneFormFieldScreenState extends State { }, items: const [ DropdownMenuItem( - value: CountrySelectorNavigator.bottomSheet(), + value: CountrySelectorNavigator.bottomSheet( + favorites: [IsoCode.GU, IsoCode.GY]), child: Text('Bottom sheet'), ), DropdownMenuItem( @@ -221,19 +212,17 @@ class PhoneFormFieldScreenState extends State { ), DropdownMenuItem( value: - CountrySelectorNavigator.modalBottomSheet( - favorites: [IsoCode.US, IsoCode.BE], - ), + CountrySelectorNavigator.modalBottomSheet(), child: Text('Modal sheet'), ), DropdownMenuItem( - value: - CountrySelectorNavigator.dialog(width: 720), + value: CountrySelectorNavigator.dialog( + width: 720, + ), child: Text('Dialog'), ), DropdownMenuItem( - value: - CountrySelectorNavigator.searchDelegate(), + value: CountrySelectorNavigator.page(), child: Text('Page'), ), ], @@ -244,42 +233,45 @@ class PhoneFormFieldScreenState extends State { const SizedBox(height: 40), Form( key: formKey, - child: PhoneFieldView( - inputKey: phoneKey, - controller: controller, - selectorNavigator: selectorNavigator, - withLabel: withLabel, - outlineBorder: outlineBorder, - isCountryChipPersistent: isCountryChipPersistent, - mobileOnly: mobileOnly, - shouldFormat: shouldFormat, - useRtl: useRtl, + child: Column( + children: [ + PhoneFieldView( + controller: controller, + focusNode: focusNode, + selectorNavigator: selectorNavigator, + withLabel: withLabel, + outlineBorder: outlineBorder, + isCountryButtonPersistant: + isCountryButtonPersistent, + mobileOnly: mobileOnly, + useRtl: useRtl, + ), + ], ), ), const SizedBox(height: 12), Text(controller.value.toString()), Text('is valid mobile number ' - '${controller.value?.isValid(type: PhoneNumberType.mobile) ?? 'false'}'), + '${controller.value.isValid(type: PhoneNumberType.mobile)}'), Text( - 'is valid fixed line number ${controller.value?.isValid(type: PhoneNumberType.fixedLine) ?? 'false'}'), + 'is valid fixed line number ${controller.value.isValid(type: PhoneNumberType.fixedLine)}'), const SizedBox(height: 12), ElevatedButton( - onPressed: controller.value == null - ? null - : () => controller.reset(), + onPressed: () => formKey.currentState?.reset(), child: const Text('reset'), ), const SizedBox(height: 12), ElevatedButton( - onPressed: () => controller.selectNationalNumber(), + onPressed: () { + controller.selectNationalNumber(); + focusNode.requestFocus(); + }, child: const Text('Select national number'), ), const SizedBox(height: 12), ElevatedButton( - onPressed: () => controller.value = PhoneNumber.parse( - '699999999', - destinationCountry: IsoCode.FR, - ), + onPressed: () => controller.value = + PhoneNumber.parse('+33 699 999 999'), child: const Text('Set +33 699 999 999'), ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index eff4efaa..8656b817 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: circle_flags - sha256: cac0fe72ad731cae5984e30be536814d7df37eeb7efc388ba76fdb84dab47ac4 + sha256: bf798dd8a651ee4301e35b85d8227b1171ee5d59cf1c1d003d5a9b5cfb256611 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" clock: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: diacritic - sha256: a84e03ec2779375fb86430dbe9d8fba62c68376f2499097a5f6e75556babe706 + sha256: "96db5db6149cbe4aa3cfcbfd170aca9b7648639be7e48025f9d458517f807fe4" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" fake_async: dependency: transitive description: @@ -179,10 +179,10 @@ packages: dependency: transitive description: name: phone_numbers_parser - sha256: "17a6686350c574a08f7beb839c5f908cc19b9c0eabd6e97029b517527a49da02" + sha256: d0dad4f5b61c3d959b069df088ef7242ffed42a3cf74c7549fd7c324e1eb964e url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.3" sky_engine: dependency: transitive description: flutter @@ -240,26 +240,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_math: dependency: transitive description: diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index b2e4bd8d..4f2af69b 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +96,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680..00000000 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85..00000000 --- a/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 5fdea291..0f5c0857 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/l10n.yaml b/l10n.yaml index 5482d2f7..02604888 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,6 +1,7 @@ -arb-dir: lib/l10n +arb-dir: l10n template-arb-file: en.arb output-localization-file: phone_field_localization.dart output-class: PhoneFieldLocalization -output-dir: lib/l10n/generated +output-dir: lib/src/localization/generated synthetic-package: false +format: true diff --git a/lib/l10n/ar.arb b/l10n/ar.arb similarity index 100% rename from lib/l10n/ar.arb rename to l10n/ar.arb diff --git a/lib/l10n/ckb.arb b/l10n/ckb.arb similarity index 100% rename from lib/l10n/ckb.arb rename to l10n/ckb.arb diff --git a/lib/l10n/de.arb b/l10n/de.arb similarity index 100% rename from lib/l10n/de.arb rename to l10n/de.arb diff --git a/lib/l10n/el.arb b/l10n/el.arb similarity index 100% rename from lib/l10n/el.arb rename to l10n/el.arb diff --git a/lib/l10n/en.arb b/l10n/en.arb similarity index 99% rename from lib/l10n/en.arb rename to l10n/en.arb index 1de71e5d..0d59cb97 100644 --- a/lib/l10n/en.arb +++ b/l10n/en.arb @@ -6,6 +6,7 @@ "invalidFixedLinePhoneNumber": "Invalid fixed line phone number", "requiredPhoneNumber": "Required phone number", "noResultMessage": "No result", + "search": "Search", "ac_": "Ascension Island", "ad_": "Andorra", "ae_": "United Arab Emirates", diff --git a/lib/l10n/es.arb b/l10n/es.arb similarity index 100% rename from lib/l10n/es.arb rename to l10n/es.arb diff --git a/lib/l10n/fa.arb b/l10n/fa.arb similarity index 100% rename from lib/l10n/fa.arb rename to l10n/fa.arb diff --git a/lib/l10n/fr.arb b/l10n/fr.arb similarity index 100% rename from lib/l10n/fr.arb rename to l10n/fr.arb diff --git a/lib/l10n/hi.arb b/l10n/hi.arb similarity index 100% rename from lib/l10n/hi.arb rename to l10n/hi.arb diff --git a/lib/l10n/it.arb b/l10n/it.arb similarity index 100% rename from lib/l10n/it.arb rename to l10n/it.arb diff --git a/lib/l10n/ku.arb b/l10n/ku.arb similarity index 100% rename from lib/l10n/ku.arb rename to l10n/ku.arb diff --git a/lib/l10n/nb.arb b/l10n/nb.arb similarity index 100% rename from lib/l10n/nb.arb rename to l10n/nb.arb diff --git a/lib/l10n/nl.arb b/l10n/nl.arb similarity index 100% rename from lib/l10n/nl.arb rename to l10n/nl.arb diff --git a/lib/l10n/pt.arb b/l10n/pt.arb similarity index 100% rename from lib/l10n/pt.arb rename to l10n/pt.arb diff --git a/lib/l10n/ru.arb b/l10n/ru.arb similarity index 100% rename from lib/l10n/ru.arb rename to l10n/ru.arb diff --git a/lib/l10n/sv.arb b/l10n/sv.arb similarity index 100% rename from lib/l10n/sv.arb rename to l10n/sv.arb diff --git a/lib/l10n/tr.arb b/l10n/tr.arb similarity index 100% rename from lib/l10n/tr.arb rename to l10n/tr.arb diff --git a/lib/l10n/uk.arb b/l10n/uk.arb similarity index 100% rename from lib/l10n/uk.arb rename to l10n/uk.arb diff --git a/lib/l10n/uz.arb b/l10n/uz.arb similarity index 100% rename from lib/l10n/uz.arb rename to l10n/uz.arb diff --git a/lib/l10n/zh.arb b/l10n/zh.arb similarity index 100% rename from lib/l10n/zh.arb rename to l10n/zh.arb diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index d685b512..b99ef406 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -1,17 +1,14 @@ library phone_number_input; -export 'src/widgets/phone_form_field.dart'; -export 'src/widgets/country_selector/country_selector_navigator.dart'; -export 'src/widgets/country_selector/country_selector.dart'; -export 'src/widgets/country_code_chip.dart'; +export 'src/phone_form_field.dart'; +export 'src/country_selection/country_selector_navigator.dart'; +export 'src/country_selection/country_selector.dart'; +export 'src/country/country_button.dart'; export 'src/validation/phone_validator.dart'; +export 'src/localization/localization.dart'; -export 'l10n/generated/phone_field_localization.dart'; - -export 'src/controllers/phone_controller.dart'; -export 'src/widgets/country_selector/country.dart'; -export 'src/widgets/country_selector/localized_country_registry.dart'; +export 'src/country/localized_country.dart'; export 'package:phone_numbers_parser/phone_numbers_parser.dart' show PhoneNumber, PhoneNumberType, IsoCode; diff --git a/lib/src/controllers/phone_controller.dart b/lib/src/controllers/phone_controller.dart deleted file mode 100644 index 94c95ecc..00000000 --- a/lib/src/controllers/phone_controller.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:phone_form_field/phone_form_field.dart'; - -class PhoneController extends ValueNotifier { - final PhoneNumber? initialValue; - // when we want to select the national number - final StreamController _selectionRequestController = - StreamController.broadcast(); - Stream get selectionRequestStream => _selectionRequestController.stream; - - PhoneController(this.initialValue) : super(initialValue); - - selectNationalNumber() { - _selectionRequestController.add(null); - } - - reset() { - value = null; - } - - @override - void dispose() { - _selectionRequestController.close(); - super.dispose(); - } -} diff --git a/lib/src/controllers/phone_field_controller.dart b/lib/src/controllers/phone_field_controller.dart deleted file mode 100644 index 7ef9e85c..00000000 --- a/lib/src/controllers/phone_field_controller.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/phone_form_field.dart'; - -class PhoneFieldController extends ChangeNotifier { - late final ValueNotifier isoCodeController; - late final TextEditingController nationalNumberController; - - /// focus node of the national number - final FocusNode focusNode; - - IsoCode get isoCode => isoCodeController.value; - String? get national => nationalNumberController.text; - - set isoCode(IsoCode isoCode) => isoCodeController.value = isoCode; - - set national(String? national) { - national = national ?? ''; - final currentSelectionOffset = - nationalNumberController.selection.extentOffset; - final isCursorAtEnd = - currentSelectionOffset == nationalNumberController.text.length; - var offset = national.length; - - if (isCursorAtEnd) { - offset = national.length; - } else if (currentSelectionOffset <= national.length) { - offset = currentSelectionOffset; - } - // when the cursor is at the end we need to preserve that - // since there is formatting going on we need to explicitely do it - nationalNumberController.value = TextEditingValue( - text: national, - selection: TextSelection.fromPosition( - TextPosition(offset: offset), - ), - ); - } - - PhoneFieldController({ - required String? national, - required IsoCode isoCode, - required this.focusNode, - }) { - isoCodeController = ValueNotifier(isoCode); - nationalNumberController = TextEditingController(text: national); - isoCodeController.addListener(notifyListeners); - nationalNumberController.addListener(notifyListeners); - } - - selectNationalNumber() { - nationalNumberController.selection = TextSelection( - baseOffset: 0, - extentOffset: nationalNumberController.value.text.length, - ); - focusNode.requestFocus(); - } - - @override - void dispose() { - isoCodeController.dispose(); - nationalNumberController.dispose(); - super.dispose(); - } -} diff --git a/lib/src/country/country_button.dart b/lib/src/country/country_button.dart new file mode 100644 index 00000000..dcc5286c --- /dev/null +++ b/lib/src/country/country_button.dart @@ -0,0 +1,80 @@ +import 'package:circle_flags/circle_flags.dart'; +import 'package:flutter/material.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +import 'localized_country.dart'; + +@Deprecated('Use [CountryButton] instead') +typedef CountryChip = CountryButton; + +class CountryButton extends StatelessWidget { + final Function()? onTap; + final IsoCode isoCode; + final bool showFlag; + final bool showDialCode; + final TextStyle? textStyle; + final EdgeInsets padding; + final double flagSize; + final TextDirection? textDirection; + final bool showIsoCode; + final bool enabled; + + const CountryButton({ + super.key, + required this.isoCode, + required this.onTap, + this.textStyle, + this.showFlag = true, + this.showDialCode = true, + this.padding = const EdgeInsets.fromLTRB(12, 16, 4, 16), + this.flagSize = 20, + this.textDirection, + this.showIsoCode = false, + this.enabled = true, + }); + + @override + Widget build(BuildContext context) { + final textStyle = this.textStyle ?? + Theme.of(context).textTheme.labelMedium ?? + const TextStyle(); + final country = LocalizedCountry.fromContext(context, isoCode); + return InkWell( + onTap: onTap, + child: Padding( + padding: padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showIsoCode) ...[ + Text( + country.isoCode.name, + style: textStyle.copyWith( + color: enabled ? null : Theme.of(context).disabledColor, + ), + ), + const SizedBox(width: 8), + ], + if (showFlag) ...[ + CircleFlag( + country.isoCode.name, + size: flagSize, + ), + const SizedBox(width: 8), + ], + if (showDialCode) ...[ + Text( + country.formattedCountryDialingCode, + style: textStyle.copyWith( + color: enabled ? null : Theme.of(context).disabledColor, + ), + textDirection: textDirection, + ), + ], + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } +} diff --git a/lib/src/country/localized_country.dart b/lib/src/country/localized_country.dart new file mode 100644 index 00000000..5d2837cf --- /dev/null +++ b/lib/src/country/localized_country.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_numbers_parser/metadata.dart'; + + +/// Country regroup informations for displaying a list of countries +class LocalizedCountry { + /// Country alpha-2 iso code + final IsoCode isoCode; + + /// localized name of the country + final String name; + + /// country dialing code to call them internationally + final String countryDialingCode; + + /// returns "+ [countryDialingCode]" + String get formattedCountryDialingCode => '+ $countryDialingCode'; + + factory LocalizedCountry.fromContext(BuildContext context, IsoCode isoCode) { + final localization = + PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); + return LocalizedCountry(isoCode, localization.countryName(isoCode)); + } + + LocalizedCountry(this.isoCode, this.name) + : countryDialingCode = metadataByIsoCode[isoCode]?.countryCode ?? ''; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LocalizedCountry && + runtimeType == other.runtimeType && + isoCode == other.isoCode; + + @override + int get hashCode => isoCode.hashCode; + + @override + String toString() { + return 'Country{isoCode: $isoCode}'; + } +} diff --git a/lib/src/widgets/country_selector/country_finder.dart b/lib/src/country_selection/country_finder.dart similarity index 54% rename from lib/src/widgets/country_selector/country_finder.dart rename to lib/src/country_selection/country_finder.dart index 00788f8b..f75456b1 100644 --- a/lib/src/widgets/country_selector/country_finder.dart +++ b/lib/src/country_selection/country_finder.dart @@ -4,56 +4,50 @@ import 'package:diacritic/diacritic.dart'; import 'package:phone_form_field/phone_form_field.dart'; class CountryFinder { - late final List _allCountries; - late List _filteredCountries; - List get filteredCountries => _filteredCountries; - - bool get isNotEmpty => _filteredCountries.isNotEmpty; - String _searchedText = ''; - String get searchedText => _searchedText; - - CountryFinder(List allCountries, {bool sort = true}) { - _allCountries = [...allCountries]; - if (sort) { - _allCountries.sort((a, b) => a.name.compareTo(b.name)); - } - _filteredCountries = [..._allCountries]; - } - - // filter a - void filter(String txt) { - if (txt == _searchedText) { - return; + List whereText({ + required String text, + required List countries, + }) { + // remove + if search text starts with + + if (text.startsWith('+')) { + text = text.substring(1); } - _searchedText = txt; // reset search - if (txt.isEmpty) { - _filteredCountries = [..._allCountries]; + if (text.isEmpty) { + return countries; } // if the txt is a number we check the country code instead - final asInt = int.tryParse(txt); + final asInt = int.tryParse(text); final isInt = asInt != null; if (isInt) { // toString to remove any + in front if its an int - _filterByCountryCallingCode(txt); + return _filterByCountryCallingCode( + countryCallingCode: text, countries: countries); } else { - _filterByName(txt); + return _filterByName(searchTxt: text, countries: countries); } } - void _filterByCountryCallingCode(String countryCallingCode) { - int getSortPoint(Country country) => - country.countryCode == countryCallingCode ? 1 : 0; + List _filterByCountryCallingCode({ + required String countryCallingCode, + required List countries, + }) { + int getSortPoint(LocalizedCountry country) => + country.countryDialingCode == countryCallingCode ? 1 : 0; - _filteredCountries = _allCountries - .where((country) => country.countryCode.contains(countryCallingCode)) + return countries + .where((country) => + country.countryDialingCode.contains(countryCallingCode)) .toList() // puts the closest match at the top ..sort((a, b) => getSortPoint(b) - getSortPoint(a)); } - void _filterByName(String searchTxt) { + List _filterByName({ + required String searchTxt, + required List countries, + }) { searchTxt = removeDiacritics(searchTxt.toLowerCase()); // since we keep countries that contain the searched text, // we need to put the countries that start with that text in front. @@ -63,14 +57,14 @@ class CountryFinder { return isStartOfString ? 1 : 0; } - int compareCountries(Country a, Country b) { + int compareCountries(LocalizedCountry a, LocalizedCountry b) { final sortPoint = getSortPoint(b.name, b.isoCode) - getSortPoint(a.name, a.isoCode); // sort alphabetically when comparison with search term get same result return sortPoint == 0 ? a.name.compareTo(b.name) : sortPoint; } - _filteredCountries = _allCountries.where((country) { + return countries.where((country) { final countryName = removeDiacritics(country.name.toLowerCase()); return countryName.contains(searchTxt) || country.isoCode.name.toLowerCase().contains(searchTxt); diff --git a/lib/src/widgets/country_selector/country_list.dart b/lib/src/country_selection/country_list_view.dart similarity index 74% rename from lib/src/widgets/country_selector/country_list.dart rename to lib/src/country_selection/country_list_view.dart index 613ccff9..81ee23fa 100644 --- a/lib/src/widgets/country_selector/country_list.dart +++ b/lib/src/country_selection/country_list_view.dart @@ -1,19 +1,20 @@ import 'package:circle_flags/circle_flags.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../../l10n/generated/phone_field_localization.dart'; -import 'country.dart'; +import '../country/localized_country.dart'; +import 'no_result_view.dart'; -class CountryList extends StatelessWidget { +class CountryListView extends StatelessWidget { /// Callback function triggered when user select a country - final Function(Country) onTap; + final Function(LocalizedCountry) onTap; /// List of countries to display - final List countries; + final List countries; final double flagSize; /// list of favorite countries to display at the top - final List favorites; + final List favorites; /// proxy to the ListView.builder controller (ie: [ScrollView.controller]) final ScrollController? scrollController; @@ -26,19 +27,17 @@ class CountryList extends StatelessWidget { final String? noResultMessage; - late final List _allListElement; + late final List _allListElement; final TextStyle? subtitleStyle; final TextStyle? titleStyle; - final FlagCache? flagCache; - CountryList({ + CountryListView({ super.key, required this.countries, required this.favorites, required this.onTap, required this.noResultMessage, - required this.flagCache, this.scrollController, this.scrollPhysics, this.showDialCode = true, @@ -46,24 +45,21 @@ class CountryList extends StatelessWidget { this.subtitleStyle, this.titleStyle, }) { - _allListElement = [ - ...favorites, - if (favorites.isNotEmpty) null, // delimiter - ...countries, - ]; + if (listEquals(countries, favorites)) { + _allListElement = countries; + } else { + _allListElement = [ + ...favorites, + if (favorites.isNotEmpty) null, // delimiter + ...countries, + ]; + } } @override Widget build(BuildContext context) { if (_allListElement.isEmpty) { - return Center( - child: Text( - noResultMessage ?? - PhoneFieldLocalization.of(context)?.noResultMessage ?? - 'No result found', - key: const ValueKey('no-result'), - ), - ); + return NoResultView(title: noResultMessage); } return ListView.builder( physics: scrollPhysics, @@ -81,7 +77,6 @@ class CountryList extends StatelessWidget { country.isoCode.name, key: ValueKey('circle-flag-${country.isoCode.name}'), size: flagSize, - cache: flagCache, ), title: Align( alignment: AlignmentDirectional.centerStart, @@ -95,7 +90,7 @@ class CountryList extends StatelessWidget { ? Align( alignment: AlignmentDirectional.centerStart, child: Text( - country.displayCountryCode, + country.formattedCountryDialingCode, textDirection: TextDirection.ltr, textAlign: TextAlign.start, style: subtitleStyle, diff --git a/lib/src/widgets/country_selector/country_selector.dart b/lib/src/country_selection/country_selector.dart similarity index 55% rename from lib/src/widgets/country_selector/country_selector.dart rename to lib/src/country_selection/country_selector.dart index 4771b89b..d9238141 100644 --- a/lib/src/widgets/country_selector/country_selector.dart +++ b/lib/src/country_selection/country_selector.dart @@ -1,24 +1,27 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/widgets/country_selector/localized_country_registry.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; -import 'country_finder.dart'; -import 'country.dart'; -import 'country_list.dart'; +import '../country/localized_country.dart'; +import 'country_list_view.dart'; import 'search_box.dart'; +/// Displays a country selector with a search box at the top +/// and a list of countries underneath. class CountrySelector extends StatefulWidget { /// List of countries to display in the selector /// Value optional in constructor. /// when omitted, the full country list is displayed - final List? countries; + final List countries; + + /// Determine the countries to be displayed on top of the list + /// Check [addFavoritesSeparator] property to enable/disable adding a + /// list divider between favorites and others defaults countries + final List favoriteCountries; /// Callback triggered when user select a country - final ValueChanged onCountrySelected; + final ValueChanged onCountrySelected; /// ListView.builder scroll controller (ie: [ScrollView.controller]) final ScrollController? scrollController; @@ -26,11 +29,6 @@ class CountrySelector extends StatefulWidget { /// The [ScrollPhysics] of the Country List final ScrollPhysics? scrollPhysics; - /// Determine the countries to be displayed on top of the list - /// Check [addFavoritesSeparator] property to enable/disable adding a - /// list divider between favorites and others defaults countries - final List favoriteCountries; - /// Whether to add a list divider between favorites & defaults /// countries. final bool addFavoritesSeparator; @@ -60,19 +58,17 @@ class CountrySelector extends StatefulWidget { /// The [Color] of the Search Icon in the Search Box final Color? searchBoxIconColor; final double flagSize; - final FlagCache flagCache; const CountrySelector({ super.key, required this.onCountrySelected, - required this.flagCache, this.scrollController, this.scrollPhysics, this.addFavoritesSeparator = true, this.showCountryCode = false, this.noResultMessage, this.favoriteCountries = const [], - this.countries, + this.countries = IsoCode.values, this.searchAutofocus = kIsWeb, this.subtitleStyle, this.titleStyle, @@ -87,35 +83,30 @@ class CountrySelector extends StatefulWidget { } class CountrySelectorState extends State { - late CountryFinder _countryFinder; - late CountryFinder _favoriteCountryFinder; + late final CountrySelectorController _controller; + String _searchedText = ''; @override didChangeDependencies() { super.didChangeDependencies(); - final localization = - PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - final isoCodes = widget.countries ?? IsoCode.values; - final countryRegistry = LocalizedCountryRegistry.cached(localization); - final notFavoriteCountries = - countryRegistry.whereIsoIn(isoCodes, omit: widget.favoriteCountries); - final favoriteCountries = - countryRegistry.whereIsoIn(widget.favoriteCountries); - _countryFinder = CountryFinder(notFavoriteCountries); - _favoriteCountryFinder = CountryFinder(favoriteCountries, sort: false); + _controller = CountrySelectorController( + context, + widget.countries, + widget.favoriteCountries, + ); + // language might have changed + _controller.search(_searchedText); } _onSearch(String searchedText) { - _countryFinder.filter(searchedText); - _favoriteCountryFinder.filter(searchedText); - setState(() {}); + _searchedText = searchedText; + _controller.search(searchedText); } onSubmitted() { - if (_favoriteCountryFinder.filteredCountries.isNotEmpty) { - widget.onCountrySelected(_favoriteCountryFinder.filteredCountries.first); - } else if (_countryFinder.filteredCountries.isNotEmpty) { - widget.onCountrySelected(_countryFinder.filteredCountries.first); + final first = _controller.findFirst(); + if (first != null) { + widget.onCountrySelected(first); } } @@ -135,30 +126,37 @@ class CountrySelectorState extends State { SizedBox( height: 70, width: double.infinity, - child: SearchBox( - autofocus: widget.searchAutofocus, - onChanged: _onSearch, - onSubmitted: onSubmitted, - decoration: widget.searchBoxDecoration, - style: widget.searchBoxTextStyle, - searchIconColor: widget.searchBoxIconColor, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: SearchBox( + autofocus: widget.searchAutofocus, + onChanged: _onSearch, + onSubmitted: onSubmitted, + decoration: widget.searchBoxDecoration, + style: widget.searchBoxTextStyle, + searchIconColor: widget.searchBoxIconColor, + ), ), ), const SizedBox(height: 16), const Divider(height: 0, thickness: 1.2), Flexible( - child: CountryList( - favorites: _favoriteCountryFinder.filteredCountries, - countries: _countryFinder.filteredCountries, - showDialCode: widget.showCountryCode, - onTap: widget.onCountrySelected, - flagSize: widget.flagSize, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - noResultMessage: widget.noResultMessage, - titleStyle: widget.titleStyle, - subtitleStyle: widget.subtitleStyle, - flagCache: widget.flagCache, + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return CountryListView( + countries: _controller.filteredCountries, + favorites: _controller.filteredFavorites, + showDialCode: widget.showCountryCode, + onTap: widget.onCountrySelected, + flagSize: widget.flagSize, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + noResultMessage: widget.noResultMessage, + titleStyle: widget.titleStyle, + subtitleStyle: widget.subtitleStyle, + ); + }, ), ), ], diff --git a/lib/src/country_selection/country_selector_controller.dart b/lib/src/country_selection/country_selector_controller.dart new file mode 100644 index 00000000..9714013a --- /dev/null +++ b/lib/src/country_selection/country_selector_controller.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:phone_form_field/phone_form_field.dart'; +import 'package:phone_form_field/src/country_selection/country_finder.dart'; + + +class CountrySelectorController with ChangeNotifier { + final _finder = CountryFinder(); + List _countries = []; + List _filteredCountries = []; + List _favoriteCountries = []; + List _filteredFavoriteCountries = []; + + List get filteredCountries => _filteredCountries; + List get filteredFavorites => _filteredFavoriteCountries; + + CountrySelectorController( + BuildContext context, + List countriesIsoCode, + List favoriteCountriesIsoCode, + ) { + _countries = _buildLocalizedCountryList(context, countriesIsoCode); + _favoriteCountries = + _buildLocalizedCountryList(context, favoriteCountriesIsoCode); + _filteredCountries = _countries; + } + + void search(String searchedText) { + _filteredCountries = _finder.whereText( + text: searchedText, + countries: _countries, + ); + _filteredFavoriteCountries = _finder.whereText( + text: searchedText, + countries: _favoriteCountries, + ); + notifyListeners(); + } + + LocalizedCountry? findFirst() { + if (_filteredFavoriteCountries.isNotEmpty) { + return _filteredFavoriteCountries.first; + } else if (_filteredCountries.isNotEmpty) { + return _filteredCountries.first; + } + return null; + } + + List _buildLocalizedCountryList( + BuildContext context, + List isoCodes, + ) { + // we need the localized names in order to search + final localization = + PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); + return isoCodes + .map((isoCode) => + LocalizedCountry(isoCode, localization.countryName(isoCode))) + .toList() + ..sort((a, b) => a.name.compareTo(b.name)); + } +} diff --git a/lib/src/widgets/country_selector/country_selector_navigator.dart b/lib/src/country_selection/country_selector_navigator.dart similarity index 86% rename from lib/src/widgets/country_selector/country_selector_navigator.dart rename to lib/src/country_selection/country_selector_navigator.dart index c7d313f8..0cf4b29a 100644 --- a/lib/src/widgets/country_selector/country_selector_navigator.dart +++ b/lib/src/country_selection/country_selector_navigator.dart @@ -1,8 +1,7 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/country_selector_page.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_page.dart'; abstract class CountrySelectorNavigator { final List? countries; @@ -39,17 +38,19 @@ abstract class CountrySelectorNavigator { this.useRootNavigator = true, }); - Future navigate(BuildContext context, FlagCache flagCache); + @Deprecated('Use [show] instead') + Future navigate(BuildContext context) => show(context); + + Future show(BuildContext context); CountrySelector _getCountrySelector({ - required ValueChanged onCountrySelected, - required FlagCache flagCache, + required ValueChanged onCountrySelected, ScrollController? scrollController, }) { return CountrySelector( - countries: countries, - onCountrySelected: onCountrySelected, + countries: countries ?? IsoCode.values, favoriteCountries: favorites ?? [], + onCountrySelected: onCountrySelected, addFavoritesSeparator: addSeparator, showCountryCode: showCountryCode, noResultMessage: noResultMessage, @@ -62,7 +63,6 @@ abstract class CountrySelectorNavigator { searchBoxIconColor: searchBoxIconColor, scrollPhysics: scrollPhysics, flagSize: flagSize, - flagCache: flagCache, ); } @@ -84,7 +84,11 @@ abstract class CountrySelectorNavigator { ScrollPhysics? scrollPhysics, }) = DialogNavigator._; - const factory CountrySelectorNavigator.searchDelegate({ + @Deprecated('Use [CountrySelectorNavigator.page] instead') + const factory CountrySelectorNavigator.searchDelegate() = + CountrySelectorNavigator.page; + + const factory CountrySelectorNavigator.page({ List? countries, List? favorites, bool addSeparator, @@ -99,7 +103,7 @@ abstract class CountrySelectorNavigator { Color? searchBoxIconColor, ScrollPhysics? scrollPhysics, ThemeData? appBarTheme, - }) = SearchDelegateNavigator._; + }) = PageNavigator._; const factory CountrySelectorNavigator.bottomSheet({ List? countries, @@ -179,7 +183,7 @@ class DialogNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { + Future show(BuildContext context) { return showDialog( context: context, builder: (_) => Dialog( @@ -189,7 +193,6 @@ class DialogNavigator extends CountrySelectorNavigator { child: _getCountrySelector( onCountrySelected: (country) => Navigator.of(context, rootNavigator: true).pop(country), - flagCache: flagCache, ), ), ), @@ -197,8 +200,8 @@ class DialogNavigator extends CountrySelectorNavigator { } } -class SearchDelegateNavigator extends CountrySelectorNavigator { - const SearchDelegateNavigator._({ +class PageNavigator extends CountrySelectorNavigator { + const PageNavigator._({ super.countries, super.favorites, super.addSeparator, @@ -217,34 +220,33 @@ class SearchDelegateNavigator extends CountrySelectorNavigator { final ThemeData? appBarTheme; - CountrySelectorSearchDelegate _getCountrySelectorSearchDelegate({ - required ValueChanged onCountrySelected, - required FlagCache flagCache, + CountrySelectorPage _getCountrySelectorPage({ + required ValueChanged onCountrySelected, ScrollController? scrollController, }) { - return CountrySelectorSearchDelegate( + return CountrySelectorPage( onCountrySelected: onCountrySelected, scrollController: scrollController, addFavoritesSeparator: addSeparator, - countries: countries, + countries: countries ?? IsoCode.values, favoriteCountries: favorites ?? [], noResultMessage: noResultMessage, searchAutofocus: searchAutofocus, showCountryCode: showCountryCode, titleStyle: titleStyle, subtitleStyle: subtitleStyle, - flagCache: flagCache, - customAppBarTheme: appBarTheme, ); } @override - Future navigate(BuildContext context, FlagCache flagCache) { - return showSearch( - context: context, - delegate: _getCountrySelectorSearchDelegate( - onCountrySelected: (country) => Navigator.pop(context, country), - flagCache: flagCache, + Future show( + BuildContext context, + ) { + return Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => _getCountrySelectorPage( + onCountrySelected: (country) => Navigator.pop(context, country), + ), ), ); } @@ -268,8 +270,10 @@ class BottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { - Country? selected; + Future show( + BuildContext context, + ) { + LocalizedCountry? selected; final ctrl = showBottomSheet( context: context, builder: (_) => MediaQuery( @@ -280,7 +284,6 @@ class BottomSheetNavigator extends CountrySelectorNavigator { selected = country; Navigator.pop(context, country); }, - flagCache: flagCache, ), ), ), @@ -310,17 +313,15 @@ class ModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate( + Future show( BuildContext context, - FlagCache flagCache, ) { - return showModalBottomSheet( + return showModalBottomSheet( context: context, builder: (_) => SizedBox( height: height ?? MediaQuery.of(context).size.height - 90, child: _getCountrySelector( onCountrySelected: (country) => Navigator.pop(context, country), - flagCache: flagCache, ), ), isScrollControlled: true, @@ -357,14 +358,14 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { }); @override - Future navigate(BuildContext context, FlagCache flagCache) { + Future show(BuildContext context) { final effectiveBorderRadius = borderRadius ?? const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ); - return showModalBottomSheet( + return showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: effectiveBorderRadius, @@ -381,7 +382,6 @@ class DraggableModalBottomSheetNavigator extends CountrySelectorNavigator { child: _getCountrySelector( onCountrySelected: (country) => Navigator.pop(context, country), scrollController: scrollController, - flagCache: flagCache, ), ); }, diff --git a/lib/src/country_selection/country_selector_page.dart b/lib/src/country_selection/country_selector_page.dart new file mode 100644 index 00000000..5b45ceb7 --- /dev/null +++ b/lib/src/country_selection/country_selector_page.dart @@ -0,0 +1,151 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:phone_form_field/src/country_selection/country_selector_controller.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +import '../country/localized_country.dart'; +import '../localization/localization.dart'; +import 'country_list_view.dart'; +import 'search_box.dart'; + +/// Same as [CountrySelector] but designed as a full page +class CountrySelectorPage extends StatefulWidget { + /// List of countries to display in the selector + /// Value optional in constructor. + /// when omitted, the full country list is displayed + final List countries; + + /// Determine the countries to be displayed on top of the list + /// Check [addFavoritesSeparator] property to enable/disable adding a + /// list divider between favorites and others defaults countries + final List favoriteCountries; + + /// Callback triggered when user select a country + final ValueChanged onCountrySelected; + + /// ListView.builder scroll controller (ie: [ScrollView.controller]) + final ScrollController? scrollController; + + /// The [ScrollPhysics] of the Country List + final ScrollPhysics? scrollPhysics; + + /// Whether to add a list divider between favorites & defaults + /// countries. + final bool addFavoritesSeparator; + + /// Whether to show the country country code (ie: +1 / +33 /...) + /// as a listTile subtitle + final bool showCountryCode; + + /// The message displayed instead of the list when the search has no results + final String? noResultMessage; + + /// whether the search input is auto focussed + final bool searchAutofocus; + + /// The [TextStyle] of the country subtitle + final TextStyle? subtitleStyle; + + /// The [TextStyle] of the country title + final TextStyle? titleStyle; + + /// The [InputDecoration] of the Search Box + final InputDecoration? searchBoxDecoration; + + /// The [TextStyle] of the Search Box + final TextStyle? searchBoxTextStyle; + + /// The [Color] of the Search Icon in the Search Box + final Color? searchBoxIconColor; + final double flagSize; + + const CountrySelectorPage({ + super.key, + required this.onCountrySelected, + this.scrollController, + this.scrollPhysics, + this.addFavoritesSeparator = true, + this.showCountryCode = false, + this.noResultMessage, + this.favoriteCountries = const [], + this.countries = IsoCode.values, + this.searchAutofocus = kIsWeb, + this.subtitleStyle, + this.titleStyle, + this.searchBoxDecoration, + this.searchBoxTextStyle, + this.searchBoxIconColor, + this.flagSize = 40, + }); + + @override + CountrySelectorPageState createState() => CountrySelectorPageState(); +} + +class CountrySelectorPageState extends State { + late final CountrySelectorController _controller; + String searchText = ''; + + @override + didChangeDependencies() { + super.didChangeDependencies(); + _controller = CountrySelectorController( + context, + widget.countries, + widget.favoriteCountries, + ); + // language might have changed + _controller.search(searchText); + } + + _onSearch(String searchedText) { + _controller.search(searchedText); + } + + onSubmitted() { + final first = _controller.findFirst(); + if (first != null) { + widget.onCountrySelected(first); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + title: SearchBox( + autofocus: widget.searchAutofocus, + onChanged: _onSearch, + onSubmitted: onSubmitted, + decoration: widget.searchBoxDecoration ?? + InputDecoration( + border: InputBorder.none, + hintText: PhoneFieldLocalization.of(context)?.search ?? + PhoneFieldLocalizationEn().search, + ), + style: widget.searchBoxTextStyle, + searchIconColor: widget.searchBoxIconColor, + ), + ), + body: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + return CountryListView( + countries: _controller.filteredCountries, + favorites: _controller.filteredFavorites, + showDialCode: widget.showCountryCode, + onTap: widget.onCountrySelected, + flagSize: widget.flagSize, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + noResultMessage: widget.noResultMessage, + titleStyle: widget.titleStyle, + subtitleStyle: widget.subtitleStyle, + ); + }, + ), + ); + } +} diff --git a/lib/src/country_selection/no_result_view.dart b/lib/src/country_selection/no_result_view.dart new file mode 100644 index 00000000..1af8603c --- /dev/null +++ b/lib/src/country_selection/no_result_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import '../localization/localization.dart'; + +class NoResultView extends StatelessWidget { + final String? title; + const NoResultView({super.key, this.title}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + title ?? + PhoneFieldLocalization.of(context)?.noResultMessage ?? + PhoneFieldLocalizationEn().noResultMessage, + key: const ValueKey('no-result'), + ), + ); + } +} diff --git a/lib/src/country_selection/search_box.dart b/lib/src/country_selection/search_box.dart new file mode 100644 index 00000000..c7a61036 --- /dev/null +++ b/lib/src/country_selection/search_box.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../localization/localization.dart'; + +class SearchBox extends StatefulWidget { + final Function(String) onChanged; + final Function() onSubmitted; + final bool autofocus; + final InputDecoration? decoration; + final TextStyle? style; + final Color? searchIconColor; + + const SearchBox({ + super.key, + required this.onChanged, + required this.onSubmitted, + required this.autofocus, + this.decoration, + this.style, + this.searchIconColor, + }); + + @override + State createState() => _SearchBoxState(); +} + +class _SearchBoxState extends State { + String _previousValue = ''; + + @override + void initState() { + super.initState(); + } + + void handleChange(text) { + widget.onChanged(text); + + final isAutofill = text.length > 3 && _previousValue == ''; + if (isAutofill) { + widget.onSubmitted(); + } + _previousValue = text; + } + + @override + Widget build(BuildContext context) { + return TextField( + autofocus: widget.autofocus, + onChanged: handleChange, + onSubmitted: (_) => widget.onSubmitted(), + cursorColor: widget.style?.color, + style: widget.style ?? Theme.of(context).textTheme.titleLarge, + autofillHints: const [AutofillHints.countryName], + decoration: widget.decoration ?? + InputDecoration( + prefixIcon: const Icon( + Icons.search, + size: 24, + ), + filled: true, + isDense: true, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(20), + ), + hintText: PhoneFieldLocalization.of(context)?.search, + ), + ); + } +} diff --git a/lib/l10n/generated/phone_field_localization.dart b/lib/src/localization/generated/phone_field_localization.dart similarity index 93% rename from lib/l10n/generated/phone_field_localization.dart rename to lib/src/localization/generated/phone_field_localization.dart index d352c0e2..52fb27ce 100644 --- a/lib/l10n/generated/phone_field_localization.dart +++ b/lib/src/localization/generated/phone_field_localization.dart @@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'phone_field_localization_ar.dart'; +import 'phone_field_localization_ckb.dart'; import 'phone_field_localization_de.dart'; import 'phone_field_localization_el.dart'; import 'phone_field_localization_en.dart'; @@ -14,6 +15,7 @@ import 'phone_field_localization_fa.dart'; import 'phone_field_localization_fr.dart'; import 'phone_field_localization_hi.dart'; import 'phone_field_localization_it.dart'; +import 'phone_field_localization_ku.dart'; import 'phone_field_localization_nb.dart'; import 'phone_field_localization_nl.dart'; import 'phone_field_localization_pt.dart'; @@ -76,15 +78,18 @@ import 'phone_field_localization_zh.dart'; /// be consistent with the languages listed in the PhoneFieldLocalization.supportedLocales /// property. abstract class PhoneFieldLocalization { - PhoneFieldLocalization(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + PhoneFieldLocalization(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static PhoneFieldLocalization? of(BuildContext context) { - return Localizations.of(context, PhoneFieldLocalization); + return Localizations.of( + context, PhoneFieldLocalization); } - static const LocalizationsDelegate delegate = _PhoneFieldLocalizationDelegate(); + static const LocalizationsDelegate delegate = + _PhoneFieldLocalizationDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -96,7 +101,8 @@ abstract class PhoneFieldLocalization { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ + static const List> localizationsDelegates = + >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -106,6 +112,7 @@ abstract class PhoneFieldLocalization { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('ar'), + Locale('ckb'), Locale('de'), Locale('el'), Locale('en'), @@ -114,6 +121,7 @@ abstract class PhoneFieldLocalization { Locale('fr'), Locale('hi'), Locale('it'), + Locale('ku'), Locale('nb'), Locale('nl'), Locale('pt'), @@ -161,6 +169,12 @@ abstract class PhoneFieldLocalization { /// **'No result'** String get noResultMessage; + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + /// No description provided for @ac_. /// /// In en, this message translates to: @@ -1620,50 +1634,92 @@ abstract class PhoneFieldLocalization { String get zw_; } -class _PhoneFieldLocalizationDelegate extends LocalizationsDelegate { +class _PhoneFieldLocalizationDelegate + extends LocalizationsDelegate { const _PhoneFieldLocalizationDelegate(); @override Future load(Locale locale) { - return SynchronousFuture(lookupPhoneFieldLocalization(locale)); + return SynchronousFuture( + lookupPhoneFieldLocalization(locale)); } @override - bool isSupported(Locale locale) => ['ar', 'de', 'el', 'en', 'es', 'fa', 'fr', 'hi', 'it', 'nb', 'nl', 'pt', 'ru', 'sv', 'tr', 'uk', 'uz', 'zh'].contains(locale.languageCode); + bool isSupported(Locale locale) => [ + 'ar', + 'ckb', + 'de', + 'el', + 'en', + 'es', + 'fa', + 'fr', + 'hi', + 'it', + 'ku', + 'nb', + 'nl', + 'pt', + 'ru', + 'sv', + 'tr', + 'uk', + 'uz', + 'zh' + ].contains(locale.languageCode); @override bool shouldReload(_PhoneFieldLocalizationDelegate old) => false; } PhoneFieldLocalization lookupPhoneFieldLocalization(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'ar': return PhoneFieldLocalizationAr(); - case 'de': return PhoneFieldLocalizationDe(); - case 'el': return PhoneFieldLocalizationEl(); - case 'en': return PhoneFieldLocalizationEn(); - case 'es': return PhoneFieldLocalizationEs(); - case 'fa': return PhoneFieldLocalizationFa(); - case 'fr': return PhoneFieldLocalizationFr(); - case 'hi': return PhoneFieldLocalizationHi(); - case 'it': return PhoneFieldLocalizationIt(); - case 'nb': return PhoneFieldLocalizationNb(); - case 'nl': return PhoneFieldLocalizationNl(); - case 'pt': return PhoneFieldLocalizationPt(); - case 'ru': return PhoneFieldLocalizationRu(); - case 'sv': return PhoneFieldLocalizationSv(); - case 'tr': return PhoneFieldLocalizationTr(); - case 'uk': return PhoneFieldLocalizationUk(); - case 'uz': return PhoneFieldLocalizationUz(); - case 'zh': return PhoneFieldLocalizationZh(); + case 'ar': + return PhoneFieldLocalizationAr(); + case 'ckb': + return PhoneFieldLocalizationCkb(); + case 'de': + return PhoneFieldLocalizationDe(); + case 'el': + return PhoneFieldLocalizationEl(); + case 'en': + return PhoneFieldLocalizationEn(); + case 'es': + return PhoneFieldLocalizationEs(); + case 'fa': + return PhoneFieldLocalizationFa(); + case 'fr': + return PhoneFieldLocalizationFr(); + case 'hi': + return PhoneFieldLocalizationHi(); + case 'it': + return PhoneFieldLocalizationIt(); + case 'ku': + return PhoneFieldLocalizationKu(); + case 'nb': + return PhoneFieldLocalizationNb(); + case 'nl': + return PhoneFieldLocalizationNl(); + case 'pt': + return PhoneFieldLocalizationPt(); + case 'ru': + return PhoneFieldLocalizationRu(); + case 'sv': + return PhoneFieldLocalizationSv(); + case 'tr': + return PhoneFieldLocalizationTr(); + case 'uk': + return PhoneFieldLocalizationUk(); + case 'uz': + return PhoneFieldLocalizationUz(); + case 'zh': + return PhoneFieldLocalizationZh(); } throw FlutterError( - 'PhoneFieldLocalization.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); + 'PhoneFieldLocalization.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); } diff --git a/lib/l10n/generated/phone_field_localization_ar.dart b/lib/src/localization/generated/phone_field_localization_ar.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_ar.dart rename to lib/src/localization/generated/phone_field_localization_ar.dart index c20d907c..3ba060df 100644 --- a/lib/l10n/generated/phone_field_localization_ar.dart +++ b/lib/src/localization/generated/phone_field_localization_ar.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Arabic (`ar`). class PhoneFieldLocalizationAr extends PhoneFieldLocalization { - PhoneFieldLocalizationAr([String locale = 'ar']) : super(locale); + PhoneFieldLocalizationAr([super.locale = 'ar']); @override String get invalidPhoneNumber => 'رقم الهاتف غير صحيح'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationAr extends PhoneFieldLocalization { @override String get noResultMessage => 'لا نتيجة'; + @override + String get search => 'Search'; + @override String get ac_ => 'جزيرة أسنسيون'; diff --git a/lib/src/localization/generated/phone_field_localization_ckb.dart b/lib/src/localization/generated/phone_field_localization_ckb.dart new file mode 100644 index 00000000..40c07678 --- /dev/null +++ b/lib/src/localization/generated/phone_field_localization_ckb.dart @@ -0,0 +1,758 @@ +import 'phone_field_localization.dart'; + +/// The translations for Central Kurdish (`ckb`). +class PhoneFieldLocalizationCkb extends PhoneFieldLocalization { + PhoneFieldLocalizationCkb([super.locale = 'ckb']); + + @override + String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; + + @override + String get invalidCountry => 'وڵاتێکی نادروست'; + + @override + String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; + + @override + String get invalidFixedLinePhoneNumber => + 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + + @override + String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; + + @override + String get noResultMessage => 'بێ ئه‌نجام'; + + @override + String get search => 'Search'; + + @override + String get ac_ => 'دوورگەی ئاسنشن'; + + @override + String get ad_ => 'ئەندۆرا'; + + @override + String get ae_ => 'شانشینی عەرەبی'; + + @override + String get af_ => 'ئەفغانستان'; + + @override + String get ag_ => 'ئەنتیگوا و باربودا'; + + @override + String get ai_ => 'ئەنگویلا'; + + @override + String get al_ => 'ئەلبانیا'; + + @override + String get am_ => 'ئەرمینیا'; + + @override + String get an_ => 'دورگه‌کانی ئه‌نتیلسی هۆڵه‌ندا'; + + @override + String get ao_ => 'ئەنگۆلا'; + + @override + String get aq_ => 'ئەنتارکتیکا'; + + @override + String get ar_ => 'ئەرجەنتین'; + + @override + String get as_ => 'سامۆای ئەمریکی'; + + @override + String get at_ => 'نەمسا'; + + @override + String get au_ => 'ئوسترالیا'; + + @override + String get aw_ => 'ئاروبا'; + + @override + String get ax_ => 'دوورگەکانی ئالاند'; + + @override + String get az_ => 'ئازەربایجان'; + + @override + String get ba_ => 'بۆسنە و هێرزۆگۆبینیا'; + + @override + String get bb_ => 'باربادۆس'; + + @override + String get bd_ => 'بەنگلادیش'; + + @override + String get be_ => 'بەلجیکا'; + + @override + String get bf_ => 'بورکينا فاسۆ'; + + @override + String get bg_ => 'بولگاریا'; + + @override + String get bh_ => 'بەحرەین'; + + @override + String get bi_ => 'بوروندی'; + + @override + String get bj_ => 'بێنین'; + + @override + String get bl_ => 'سانت بارتێلمی'; + + @override + String get bm_ => 'بەرمودا'; + + @override + String get bn_ => 'برونێی داروسالام'; + + @override + String get bo_ => 'بۆلیڤیا، ویلایەتی فرەنەتەوەیی'; + + @override + String get bq_ => 'بۆنایر'; + + @override + String get br_ => 'بەڕازیل'; + + @override + String get bs_ => 'باهاماس'; + + @override + String get bt_ => 'بۆتان'; + + @override + String get bw_ => 'بۆتسوانا'; + + @override + String get by_ => 'بێلاڕوس'; + + @override + String get bz_ => 'بەلیز'; + + @override + String get ca_ => 'کەنەدا'; + + @override + String get cc_ => 'دوورگەکانی کۆکۆس (کیلینگ).'; + + @override + String get cd_ => 'کۆنگۆ، کۆماری دیموکراتیی کۆنگۆ'; + + @override + String get cf_ => 'کۆماری ئەفریقیای ناوەراست'; + + @override + String get cg_ => 'کۆنگۆ'; + + @override + String get ch_ => 'سویسرا'; + + @override + String get ci_ => 'کۆت دیڤوار'; + + @override + String get ck_ => 'دوورگەکانی کوک'; + + @override + String get cl_ => 'شیلی'; + + @override + String get cm_ => 'کامیرۆن'; + + @override + String get cn_ => 'چین'; + + @override + String get co_ => 'کۆڵۆمبیا'; + + @override + String get cr_ => 'کۆستەریکا'; + + @override + String get cu_ => 'کوبا'; + + @override + String get cv_ => 'کیپ ڤێردی'; + + @override + String get cx_ => 'دوورگەی کریسمس'; + + @override + String get cy_ => 'قوبرس'; + + @override + String get cz_ => 'کۆماری چیک'; + + @override + String get de_ => 'ئەڵمانیا'; + + @override + String get dj_ => 'جیبۆتی'; + + @override + String get dk_ => 'دانیمارک'; + + @override + String get dm_ => 'دۆمینیکا'; + + @override + String get do_ => 'کۆماری دۆمینیکەن'; + + @override + String get dz_ => 'جەزائیر'; + + @override + String get ec_ => 'ئیکوادۆر'; + + @override + String get ee_ => 'ئیستۆنیا'; + + @override + String get eg_ => 'میسر'; + + @override + String get er_ => 'ئێریتریا'; + + @override + String get es_ => 'ئیسپانیا'; + + @override + String get et_ => 'ئەسیوپیا'; + + @override + String get fi_ => 'فینلاند'; + + @override + String get fj_ => 'فیجی'; + + @override + String get fk_ => 'دوورگەکانی فۆڵکلاند (ماڵڤیناس)'; + + @override + String get fm_ => 'مایکرۆنیزیا، ویلایەتە فیدراڵیەکانی مایکرۆنیزیا'; + + @override + String get fo_ => 'دورگەکانی فارۆ'; + + @override + String get fr_ => 'فەرەنسا'; + + @override + String get ga_ => 'گابۆن'; + + @override + String get gb_ => 'شانشینە یەگرتۆکان'; + + @override + String get gd_ => 'گرێنادا'; + + @override + String get ge_ => 'جۆرجیا'; + + @override + String get gf_ => + 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + + @override + String get gg_ => 'گێرنسی'; + + @override + String get gh_ => 'غانا'; + + @override + String get gi_ => 'جبل طارق'; + + @override + String get gl_ => 'گرینلاند'; + + @override + String get gm_ => 'گامبیا'; + + @override + String get gn_ => 'گینیا'; + + @override + String get gp_ => 'گوادلۆپ'; + + @override + String get gq_ => 'گینیا ئیکواتۆریال'; + + @override + String get gr_ => 'یۆنان'; + + @override + String get gs_ => 'باشووری جۆرجیا و دوورگەکانی ساندویچی باشوور'; + + @override + String get gt_ => 'گواتیمالا'; + + @override + String get gu_ => 'گوام'; + + @override + String get gw_ => 'گینیا-بیساو'; + + @override + String get gy_ => 'گویانا'; + + @override + String get hk_ => 'هۆنگ کۆنگ'; + + @override + String get hn_ => 'هندۆراس'; + + @override + String get hr_ => 'کرواتیا'; + + @override + String get ht_ => 'هایتی'; + + @override + String get hu_ => 'هەنگاریا'; + + @override + String get id_ => 'ئەندەنوسیا'; + + @override + String get ie_ => 'ئێرلەندا'; + + @override + String get il_ => 'ئیسرائیل'; + + @override + String get im_ => 'دوورگەی مان'; + + @override + String get in_ => 'هیندستان'; + + @override + String get io_ => 'خاکی زەریای هیندی بەریتانیا'; + + @override + String get iq_ => 'عێراق'; + + @override + String get ir_ => 'ئێران، کۆماری ئیسلامیی...'; + + @override + String get is_ => 'ئایسلەندا'; + + @override + String get it_ => 'ئیتاڵیا'; + + @override + String get je_ => 'جێرسی'; + + @override + String get jm_ => 'جامایکا'; + + @override + String get jo_ => 'ئوردن'; + + @override + String get jp_ => 'ژاپۆن'; + + @override + String get ke_ => 'کینیا'; + + @override + String get kg_ => 'قیرغیزستان'; + + @override + String get kh_ => 'کەمبۆدیا'; + + @override + String get ki_ => 'کیریباتی'; + + @override + String get km_ => 'کۆمۆرۆس'; + + @override + String get kn_ => 'سەینت کیتس و نیڤیس'; + + @override + String get kp_ => 'کۆریا، کۆماری گەلی دیموکراتی کۆریا'; + + @override + String get kr_ => 'کۆریا، کۆماری کۆریای باشوور'; + + @override + String get kw_ => 'کوێت'; + + @override + String get ky_ => 'دوورگەکانی کایمان'; + + @override + String get kz_ => 'کازاخستان'; + + @override + String get la_ => 'لائۆس'; + + @override + String get lb_ => 'لوبنان'; + + @override + String get lc_ => 'سانت لوسیا'; + + @override + String get li_ => 'لیختنشتاین'; + + @override + String get lk_ => 'سری لانکا'; + + @override + String get lr_ => 'لیبێریا'; + + @override + String get ls_ => 'لێسۆتۆ'; + + @override + String get lt_ => 'لیتوانیا'; + + @override + String get lu_ => 'لۆکسمبۆرگ'; + + @override + String get lv_ => 'لاتڤیا'; + + @override + String get ly_ => 'لیبیا'; + + @override + String get ma_ => 'مەغریب'; + + @override + String get mc_ => 'مۆناکۆ'; + + @override + String get md_ => 'مۆڵدۆڤا'; + + @override + String get me_ => 'مۆنتینیگرۆ'; + + @override + String get mf_ => 'سانت مارتن'; + + @override + String get mg_ => 'ماداگاسکار'; + + @override + String get mh_ => 'دوورگەکانی مارشال'; + + @override + String get mk_ => 'مەقدۆنیا'; + + @override + String get ml_ => 'مالی'; + + @override + String get mm_ => 'میانمار'; + + @override + String get mn_ => 'مەنگۆلیا'; + + @override + String get mo_ => 'ماکاو'; + + @override + String get mp_ => 'دوورگەکانی باکووری ماریانا'; + + @override + String get mq_ => 'مارتینیک'; + + @override + String get mr_ => 'مۆریتانیا'; + + @override + String get ms_ => 'مۆنتسێرات'; + + @override + String get mt_ => 'ماڵتا'; + + @override + String get mu_ => 'مۆریس'; + + @override + String get mv_ => 'ماڵدیڤ'; + + @override + String get mw_ => 'مالاوی'; + + @override + String get mx_ => 'مەکسیک'; + + @override + String get my_ => 'مالیزیا'; + + @override + String get mz_ => 'مۆزەمبیق'; + + @override + String get na_ => 'نامیبیا'; + + @override + String get nc_ => 'کاڵێدۆنیای نوێ'; + + @override + String get ne_ => 'نیجەر'; + + @override + String get nf_ => 'دوورگەی نۆرفۆلک'; + + @override + String get ng_ => 'نەیجیریا'; + + @override + String get ni_ => 'نیکاراگوا'; + + @override + String get nl_ => 'هۆڵەندا'; + + @override + String get no_ => 'نەرویج'; + + @override + String get np_ => 'نیپاڵ'; + + @override + String get nr_ => 'ناورو'; + + @override + String get nu_ => 'نیوێ'; + + @override + String get nz_ => 'وڵاتی نیوزله‌ندا'; + + @override + String get om_ => 'عومان'; + + @override + String get pa_ => 'پەنەما'; + + @override + String get pe_ => 'پیرۆ'; + + @override + String get pf_ => 'پۆلینیزیای فەرەنسی'; + + @override + String get pg_ => 'پاپوای نیوگینیا'; + + @override + String get ph_ => 'فلیپین'; + + @override + String get pk_ => 'پاکستان'; + + @override + String get pl_ => 'پۆڵەندا'; + + @override + String get pm_ => 'سانت پیێر و میکێلۆن'; + + @override + String get pn_ => 'پیتکایرن'; + + @override + String get pr_ => 'پورتوگال'; + + @override + String get ps_ => 'خاکی فەلەستین، داگیرکراوە'; + + @override + String get pt_ => 'پورتوگال'; + + @override + String get pw_ => 'پالاو'; + + @override + String get py_ => 'پاراگوای'; + + @override + String get qa_ => 'قەتەر'; + + @override + String get re_ => 'یەکگرتنەوە'; + + @override + String get ro_ => 'ڕۆمانیا'; + + @override + String get rs_ => 'سربیا'; + + @override + String get ru_ => 'ڕووسیا'; + + @override + String get rw_ => 'ڕواندا'; + + @override + String get sa_ => 'عەرەبستانی سوعوودی'; + + @override + String get sb_ => 'دوورگەکانی سلێمان'; + + @override + String get sc_ => 'سیشێل'; + + @override + String get sd_ => 'سودان'; + + @override + String get se_ => 'سویدی'; + + @override + String get sg_ => 'سەنگافورە'; + + @override + String get si_ => 'سلۆڤینیا'; + + @override + String get sk_ => 'سلۆڤاکیا'; + + @override + String get sl_ => 'سیرالیۆن'; + + @override + String get sm_ => 'سان مارینۆ'; + + @override + String get sn_ => 'سەنیگال'; + + @override + String get so_ => 'سۆماڵ'; + + @override + String get sr_ => 'سورینام'; + + @override + String get ss_ => 'باشووری سودان'; + + @override + String get st_ => 'ساو تۆمێ و پرینسیپی'; + + @override + String get sv_ => 'سلڤادۆر'; + + @override + String get sy_ => 'کۆماری عەرەبی سوریا'; + + @override + String get sz_ => 'سوازیلاند'; + + @override + String get ta_ => 'تریستان دا کونها'; + + @override + String get tc_ => 'دوورگەکانی تورک و کایکۆس'; + + @override + String get td_ => 'چاد'; + + @override + String get tg_ => 'تۆگۆ'; + + @override + String get th_ => 'تایلەند'; + + @override + String get tj_ => 'تاجیکستان'; + + @override + String get tk_ => 'تۆکێلاو'; + + @override + String get tl_ => 'تیمۆر-لێستێ'; + + @override + String get tm_ => 'تورکمانستان'; + + @override + String get tn_ => 'تونس'; + + @override + String get to_ => 'تۆنگا'; + + @override + String get tr_ => 'تورکیە'; + + @override + String get tt_ => 'ترینیداد و تۆباگۆ'; + + @override + String get tv_ => 'توڤالو'; + + @override + String get tw_ => 'تایوان'; + + @override + String get tz_ => 'تانزانیا، کۆماری یەکگرتووی تانزانیا'; + + @override + String get ua_ => 'ئۆکرانیا'; + + @override + String get ug_ => 'ئۆگاندا'; + + @override + String get us_ => 'ویلایەتە یەکگرتووەکان'; + + @override + String get uy_ => 'ئۆرۆگوای'; + + @override + String get uz_ => 'ئۆزبەکستان'; + + @override + String get va_ => 'کورسی پیرۆز (وڵاتی ڤاتیکان سیتی)'; + + @override + String get vc_ => 'سانت ڤینسێنت و گرێنادین'; + + @override + String get ve_ => 'ڤەنزوێلا'; + + @override + String get vg_ => 'دوورگەکانی ڤێرجینیا، بەریتانیا'; + + @override + String get vi_ => 'دوورگەکانی ڤێرجینیا، ئەمریکا.'; + + @override + String get vn_ => 'ڤێتنام'; + + @override + String get vu_ => 'ڤانواتو'; + + @override + String get wf_ => 'والیس و فوتونا'; + + @override + String get ws_ => 'ساموا'; + + @override + String get ye_ => 'یەمەن'; + + @override + String get yt_ => 'مایۆت'; + + @override + String get za_ => 'باشوری ئەفریقا'; + + @override + String get zm_ => 'زامبیا'; + + @override + String get zw_ => 'زیمبابۆی'; +} diff --git a/lib/l10n/generated/phone_field_localization_de.dart b/lib/src/localization/generated/phone_field_localization_de.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_de.dart rename to lib/src/localization/generated/phone_field_localization_de.dart index 92c5f78d..d80a0082 100644 --- a/lib/l10n/generated/phone_field_localization_de.dart +++ b/lib/src/localization/generated/phone_field_localization_de.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for German (`de`). class PhoneFieldLocalizationDe extends PhoneFieldLocalization { - PhoneFieldLocalizationDe([String locale = 'de']) : super(locale); + PhoneFieldLocalizationDe([super.locale = 'de']); @override String get invalidPhoneNumber => 'Ungültige Telefonnummer'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationDe extends PhoneFieldLocalization { @override String get noResultMessage => 'Kein Ergebnis'; + @override + String get search => 'Search'; + @override String get ac_ => 'Himmelfahrtsinsel'; diff --git a/lib/l10n/generated/phone_field_localization_el.dart b/lib/src/localization/generated/phone_field_localization_el.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_el.dart rename to lib/src/localization/generated/phone_field_localization_el.dart index b1d36d5a..f55eee66 100644 --- a/lib/l10n/generated/phone_field_localization_el.dart +++ b/lib/src/localization/generated/phone_field_localization_el.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Modern Greek (`el`). class PhoneFieldLocalizationEl extends PhoneFieldLocalization { - PhoneFieldLocalizationEl([String locale = 'el']) : super(locale); + PhoneFieldLocalizationEl([super.locale = 'el']); @override String get invalidPhoneNumber => 'Μη έγκυρος αριθμός τηλεφώνου'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Μη έγκυρος αριθμός κινητού τηλεφώνου'; @override - String get invalidFixedLinePhoneNumber => 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; + String get invalidFixedLinePhoneNumber => + 'Μη έγκυρος αριθμός σταθερού τηλεφώνου'; @override String get requiredPhoneNumber => 'Απαιτούμενος αριθμός τηλεφώνου'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationEl extends PhoneFieldLocalization { @override String get noResultMessage => 'Κανένα αποτέλεσμα'; + @override + String get search => 'Search'; + @override String get ac_ => 'Νησί της Ανάληψης'; diff --git a/lib/l10n/generated/phone_field_localization_en.dart b/lib/src/localization/generated/phone_field_localization_en.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_en.dart rename to lib/src/localization/generated/phone_field_localization_en.dart index 4667b3c5..7ff77099 100644 --- a/lib/l10n/generated/phone_field_localization_en.dart +++ b/lib/src/localization/generated/phone_field_localization_en.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for English (`en`). class PhoneFieldLocalizationEn extends PhoneFieldLocalization { - PhoneFieldLocalizationEn([String locale = 'en']) : super(locale); + PhoneFieldLocalizationEn([super.locale = 'en']); @override String get invalidPhoneNumber => 'Invalid phone number'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationEn extends PhoneFieldLocalization { @override String get noResultMessage => 'No result'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_es.dart b/lib/src/localization/generated/phone_field_localization_es.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_es.dart rename to lib/src/localization/generated/phone_field_localization_es.dart index 7d866ca5..ccedb32e 100644 --- a/lib/l10n/generated/phone_field_localization_es.dart +++ b/lib/src/localization/generated/phone_field_localization_es.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Spanish Castilian (`es`). class PhoneFieldLocalizationEs extends PhoneFieldLocalization { - PhoneFieldLocalizationEs([String locale = 'es']) : super(locale); + PhoneFieldLocalizationEs([super.locale = 'es']); @override String get invalidPhoneNumber => 'Numero de telefono invalido'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationEs extends PhoneFieldLocalization { @override String get noResultMessage => 'Sin resultados'; + @override + String get search => 'Search'; + @override String get ac_ => 'Isla Ascencion'; diff --git a/lib/l10n/generated/phone_field_localization_fa.dart b/lib/src/localization/generated/phone_field_localization_fa.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_fa.dart rename to lib/src/localization/generated/phone_field_localization_fa.dart index 73576037..fef4b6d9 100644 --- a/lib/l10n/generated/phone_field_localization_fa.dart +++ b/lib/src/localization/generated/phone_field_localization_fa.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Persian (`fa`). class PhoneFieldLocalizationFa extends PhoneFieldLocalization { - PhoneFieldLocalizationFa([String locale = 'fa']) : super(locale); + PhoneFieldLocalizationFa([super.locale = 'fa']); @override String get invalidPhoneNumber => 'شماره تلفن نامعتبر است'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationFa extends PhoneFieldLocalization { @override String get noResultMessage => 'بدون نتیجه'; + @override + String get search => 'Search'; + @override String get ac_ => 'جزیره اسنشن'; diff --git a/lib/l10n/generated/phone_field_localization_fr.dart b/lib/src/localization/generated/phone_field_localization_fr.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_fr.dart rename to lib/src/localization/generated/phone_field_localization_fr.dart index 700a6e4e..a680a3b4 100644 --- a/lib/l10n/generated/phone_field_localization_fr.dart +++ b/lib/src/localization/generated/phone_field_localization_fr.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for French (`fr`). class PhoneFieldLocalizationFr extends PhoneFieldLocalization { - PhoneFieldLocalizationFr([String locale = 'fr']) : super(locale); + PhoneFieldLocalizationFr([super.locale = 'fr']); @override String get invalidPhoneNumber => 'Numéro de téléphone invalide'; @@ -11,7 +11,8 @@ class PhoneFieldLocalizationFr extends PhoneFieldLocalization { String get invalidCountry => 'Pays invalide'; @override - String get invalidMobilePhoneNumber => 'Numéro de téléphone portable invalide'; + String get invalidMobilePhoneNumber => + 'Numéro de téléphone portable invalide'; @override String get invalidFixedLinePhoneNumber => 'Numéro de téléphone fixe invalide'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationFr extends PhoneFieldLocalization { @override String get noResultMessage => 'Aucun résultat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Île de l\'Ascension'; diff --git a/lib/l10n/generated/phone_field_localization_hi.dart b/lib/src/localization/generated/phone_field_localization_hi.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_hi.dart rename to lib/src/localization/generated/phone_field_localization_hi.dart index 4d389339..94e8f1ee 100644 --- a/lib/l10n/generated/phone_field_localization_hi.dart +++ b/lib/src/localization/generated/phone_field_localization_hi.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Hindi (`hi`). class PhoneFieldLocalizationHi extends PhoneFieldLocalization { - PhoneFieldLocalizationHi([String locale = 'hi']) : super(locale); + PhoneFieldLocalizationHi([super.locale = 'hi']); @override String get invalidPhoneNumber => 'अवैध फोन नंबर'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationHi extends PhoneFieldLocalization { @override String get noResultMessage => 'कोई परिणाम नही'; + @override + String get search => 'Search'; + @override String get ac_ => 'असेंशन द्वीप'; diff --git a/lib/l10n/generated/phone_field_localization_it.dart b/lib/src/localization/generated/phone_field_localization_it.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_it.dart rename to lib/src/localization/generated/phone_field_localization_it.dart index c8ff8386..70a33276 100644 --- a/lib/l10n/generated/phone_field_localization_it.dart +++ b/lib/src/localization/generated/phone_field_localization_it.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Italian (`it`). class PhoneFieldLocalizationIt extends PhoneFieldLocalization { - PhoneFieldLocalizationIt([String locale = 'it']) : super(locale); + PhoneFieldLocalizationIt([super.locale = 'it']); @override String get invalidPhoneNumber => 'Numero di telefono invalido'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationIt extends PhoneFieldLocalization { @override String get noResultMessage => 'Nessun risultato'; + @override + String get search => 'Search'; + @override String get ac_ => 'Isola dell\'Ascensione'; diff --git a/lib/src/localization/generated/phone_field_localization_ku.dart b/lib/src/localization/generated/phone_field_localization_ku.dart new file mode 100644 index 00000000..e789ace9 --- /dev/null +++ b/lib/src/localization/generated/phone_field_localization_ku.dart @@ -0,0 +1,758 @@ +import 'phone_field_localization.dart'; + +/// The translations for Kurdish (`ku`). +class PhoneFieldLocalizationKu extends PhoneFieldLocalization { + PhoneFieldLocalizationKu([super.locale = 'ku']); + + @override + String get invalidPhoneNumber => 'ژمارەی تەلەفۆنی نادروست'; + + @override + String get invalidCountry => 'وڵاتێکی نادروست'; + + @override + String get invalidMobilePhoneNumber => 'ژمارەی مۆبایل نادروستە'; + + @override + String get invalidFixedLinePhoneNumber => + 'ژمارەی تەلەفۆنی هێڵی جێگیر نادروستە'; + + @override + String get requiredPhoneNumber => 'ژمارەی تەلەفۆنی پێویست'; + + @override + String get noResultMessage => 'بێ ئه‌نجام'; + + @override + String get search => 'Search'; + + @override + String get ac_ => 'دوورگەی ئاسنشن'; + + @override + String get ad_ => 'ئەندۆرا'; + + @override + String get ae_ => 'شانشینی عەرەبی'; + + @override + String get af_ => 'ئەفغانستان'; + + @override + String get ag_ => 'ئەنتیگوا و باربودا'; + + @override + String get ai_ => 'ئەنگویلا'; + + @override + String get al_ => 'ئەلبانیا'; + + @override + String get am_ => 'ئەرمینیا'; + + @override + String get an_ => 'دورگه‌کانی ئه‌نتیلسی هۆڵه‌ندا'; + + @override + String get ao_ => 'ئەنگۆلا'; + + @override + String get aq_ => 'ئەنتارکتیکا'; + + @override + String get ar_ => 'ئەرجەنتین'; + + @override + String get as_ => 'سامۆای ئەمریکی'; + + @override + String get at_ => 'نەمسا'; + + @override + String get au_ => 'ئوسترالیا'; + + @override + String get aw_ => 'ئاروبا'; + + @override + String get ax_ => 'دوورگەکانی ئالاند'; + + @override + String get az_ => 'ئازەربایجان'; + + @override + String get ba_ => 'بۆسنە و هێرزۆگۆبینیا'; + + @override + String get bb_ => 'باربادۆس'; + + @override + String get bd_ => 'بەنگلادیش'; + + @override + String get be_ => 'بەلجیکا'; + + @override + String get bf_ => 'بورکينا فاسۆ'; + + @override + String get bg_ => 'بولگاریا'; + + @override + String get bh_ => 'بەحرەین'; + + @override + String get bi_ => 'بوروندی'; + + @override + String get bj_ => 'بێنین'; + + @override + String get bl_ => 'سانت بارتێلمی'; + + @override + String get bm_ => 'بەرمودا'; + + @override + String get bn_ => 'برونێی داروسالام'; + + @override + String get bo_ => 'بۆلیڤیا، ویلایەتی فرەنەتەوەیی'; + + @override + String get bq_ => 'بۆنایر'; + + @override + String get br_ => 'بەڕازیل'; + + @override + String get bs_ => 'باهاماس'; + + @override + String get bt_ => 'بۆتان'; + + @override + String get bw_ => 'بۆتسوانا'; + + @override + String get by_ => 'بێلاڕوس'; + + @override + String get bz_ => 'بەلیز'; + + @override + String get ca_ => 'کەنەدا'; + + @override + String get cc_ => 'دوورگەکانی کۆکۆس (کیلینگ).'; + + @override + String get cd_ => 'کۆنگۆ، کۆماری دیموکراتیی کۆنگۆ'; + + @override + String get cf_ => 'کۆماری ئەفریقیای ناوەراست'; + + @override + String get cg_ => 'کۆنگۆ'; + + @override + String get ch_ => 'سویسرا'; + + @override + String get ci_ => 'کۆت دیڤوار'; + + @override + String get ck_ => 'دوورگەکانی کوک'; + + @override + String get cl_ => 'شیلی'; + + @override + String get cm_ => 'کامیرۆن'; + + @override + String get cn_ => 'چین'; + + @override + String get co_ => 'کۆڵۆمبیا'; + + @override + String get cr_ => 'کۆستەریکا'; + + @override + String get cu_ => 'کوبا'; + + @override + String get cv_ => 'کیپ ڤێردی'; + + @override + String get cx_ => 'دوورگەی کریسمس'; + + @override + String get cy_ => 'قوبرس'; + + @override + String get cz_ => 'کۆماری چیک'; + + @override + String get de_ => 'ئەڵمانیا'; + + @override + String get dj_ => 'جیبۆتی'; + + @override + String get dk_ => 'دانیمارک'; + + @override + String get dm_ => 'دۆمینیکا'; + + @override + String get do_ => 'کۆماری دۆمینیکەن'; + + @override + String get dz_ => 'جەزائیر'; + + @override + String get ec_ => 'ئیکوادۆر'; + + @override + String get ee_ => 'ئیستۆنیا'; + + @override + String get eg_ => 'میسر'; + + @override + String get er_ => 'ئێریتریا'; + + @override + String get es_ => 'ئیسپانیا'; + + @override + String get et_ => 'ئەسیوپیا'; + + @override + String get fi_ => 'فینلاند'; + + @override + String get fj_ => 'فیجی'; + + @override + String get fk_ => 'دوورگەکانی فۆڵکلاند (ماڵڤیناس)'; + + @override + String get fm_ => 'مایکرۆنیزیا، ویلایەتە فیدراڵیەکانی مایکرۆنیزیا'; + + @override + String get fo_ => 'دورگەکانی فارۆ'; + + @override + String get fr_ => 'فەرەنسا'; + + @override + String get ga_ => 'گابۆن'; + + @override + String get gb_ => 'شانشینە یەگرتۆکان'; + + @override + String get gd_ => 'گرێنادا'; + + @override + String get ge_ => 'جۆرجیا'; + + @override + String get gf_ => + 'دورگەیەکی فەڕەنسایە کە دەکەوێتە باکوری خۆرهەڵاتی ئەمەریکای باشور'; + + @override + String get gg_ => 'گێرنسی'; + + @override + String get gh_ => 'غانا'; + + @override + String get gi_ => 'جبل طارق'; + + @override + String get gl_ => 'گرینلاند'; + + @override + String get gm_ => 'گامبیا'; + + @override + String get gn_ => 'گینیا'; + + @override + String get gp_ => 'گوادلۆپ'; + + @override + String get gq_ => 'گینیا ئیکواتۆریال'; + + @override + String get gr_ => 'یۆنان'; + + @override + String get gs_ => 'باشووری جۆرجیا و دوورگەکانی ساندویچی باشوور'; + + @override + String get gt_ => 'گواتیمالا'; + + @override + String get gu_ => 'گوام'; + + @override + String get gw_ => 'گینیا-بیساو'; + + @override + String get gy_ => 'گویانا'; + + @override + String get hk_ => 'هۆنگ کۆنگ'; + + @override + String get hn_ => 'هندۆراس'; + + @override + String get hr_ => 'کرواتیا'; + + @override + String get ht_ => 'هایتی'; + + @override + String get hu_ => 'هەنگاریا'; + + @override + String get id_ => 'ئەندەنوسیا'; + + @override + String get ie_ => 'ئێرلەندا'; + + @override + String get il_ => 'ئیسرائیل'; + + @override + String get im_ => 'دوورگەی مان'; + + @override + String get in_ => 'هیندستان'; + + @override + String get io_ => 'خاکی زەریای هیندی بەریتانیا'; + + @override + String get iq_ => 'عێراق'; + + @override + String get ir_ => 'ئێران، کۆماری ئیسلامیی...'; + + @override + String get is_ => 'ئایسلەندا'; + + @override + String get it_ => 'ئیتاڵیا'; + + @override + String get je_ => 'جێرسی'; + + @override + String get jm_ => 'جامایکا'; + + @override + String get jo_ => 'ئوردن'; + + @override + String get jp_ => 'ژاپۆن'; + + @override + String get ke_ => 'کینیا'; + + @override + String get kg_ => 'قیرغیزستان'; + + @override + String get kh_ => 'کەمبۆدیا'; + + @override + String get ki_ => 'کیریباتی'; + + @override + String get km_ => 'کۆمۆرۆس'; + + @override + String get kn_ => 'سەینت کیتس و نیڤیس'; + + @override + String get kp_ => 'کۆریا، کۆماری گەلی دیموکراتی کۆریا'; + + @override + String get kr_ => 'کۆریا، کۆماری کۆریای باشوور'; + + @override + String get kw_ => 'کوێت'; + + @override + String get ky_ => 'دوورگەکانی کایمان'; + + @override + String get kz_ => 'کازاخستان'; + + @override + String get la_ => 'لائۆس'; + + @override + String get lb_ => 'لوبنان'; + + @override + String get lc_ => 'سانت لوسیا'; + + @override + String get li_ => 'لیختنشتاین'; + + @override + String get lk_ => 'سری لانکا'; + + @override + String get lr_ => 'لیبێریا'; + + @override + String get ls_ => 'لێسۆتۆ'; + + @override + String get lt_ => 'لیتوانیا'; + + @override + String get lu_ => 'لۆکسمبۆرگ'; + + @override + String get lv_ => 'لاتڤیا'; + + @override + String get ly_ => 'لیبیا'; + + @override + String get ma_ => 'مەغریب'; + + @override + String get mc_ => 'مۆناکۆ'; + + @override + String get md_ => 'مۆڵدۆڤا'; + + @override + String get me_ => 'مۆنتینیگرۆ'; + + @override + String get mf_ => 'سانت مارتن'; + + @override + String get mg_ => 'ماداگاسکار'; + + @override + String get mh_ => 'دوورگەکانی مارشال'; + + @override + String get mk_ => 'مەقدۆنیا'; + + @override + String get ml_ => 'مالی'; + + @override + String get mm_ => 'میانمار'; + + @override + String get mn_ => 'مەنگۆلیا'; + + @override + String get mo_ => 'ماکاو'; + + @override + String get mp_ => 'دوورگەکانی باکووری ماریانا'; + + @override + String get mq_ => 'مارتینیک'; + + @override + String get mr_ => 'مۆریتانیا'; + + @override + String get ms_ => 'مۆنتسێرات'; + + @override + String get mt_ => 'ماڵتا'; + + @override + String get mu_ => 'مۆریس'; + + @override + String get mv_ => 'ماڵدیڤ'; + + @override + String get mw_ => 'مالاوی'; + + @override + String get mx_ => 'مەکسیک'; + + @override + String get my_ => 'مالیزیا'; + + @override + String get mz_ => 'مۆزەمبیق'; + + @override + String get na_ => 'نامیبیا'; + + @override + String get nc_ => 'کاڵێدۆنیای نوێ'; + + @override + String get ne_ => 'نیجەر'; + + @override + String get nf_ => 'دوورگەی نۆرفۆلک'; + + @override + String get ng_ => 'نەیجیریا'; + + @override + String get ni_ => 'نیکاراگوا'; + + @override + String get nl_ => 'هۆڵەندا'; + + @override + String get no_ => 'نەرویج'; + + @override + String get np_ => 'نیپاڵ'; + + @override + String get nr_ => 'ناورو'; + + @override + String get nu_ => 'نیوێ'; + + @override + String get nz_ => 'وڵاتی نیوزله‌ندا'; + + @override + String get om_ => 'عومان'; + + @override + String get pa_ => 'پەنەما'; + + @override + String get pe_ => 'پیرۆ'; + + @override + String get pf_ => 'پۆلینیزیای فەرەنسی'; + + @override + String get pg_ => 'پاپوای نیوگینیا'; + + @override + String get ph_ => 'فلیپین'; + + @override + String get pk_ => 'پاکستان'; + + @override + String get pl_ => 'پۆڵەندا'; + + @override + String get pm_ => 'سانت پیێر و میکێلۆن'; + + @override + String get pn_ => 'پیتکایرن'; + + @override + String get pr_ => 'پورتوگال'; + + @override + String get ps_ => 'خاکی فەلەستین، داگیرکراوە'; + + @override + String get pt_ => 'پورتوگال'; + + @override + String get pw_ => 'پالاو'; + + @override + String get py_ => 'پاراگوای'; + + @override + String get qa_ => 'قەتەر'; + + @override + String get re_ => 'یەکگرتنەوە'; + + @override + String get ro_ => 'ڕۆمانیا'; + + @override + String get rs_ => 'سربیا'; + + @override + String get ru_ => 'ڕووسیا'; + + @override + String get rw_ => 'ڕواندا'; + + @override + String get sa_ => 'عەرەبستانی سوعوودی'; + + @override + String get sb_ => 'دوورگەکانی سلێمان'; + + @override + String get sc_ => 'سیشێل'; + + @override + String get sd_ => 'سودان'; + + @override + String get se_ => 'سویدی'; + + @override + String get sg_ => 'سەنگافورە'; + + @override + String get si_ => 'سلۆڤینیا'; + + @override + String get sk_ => 'سلۆڤاکیا'; + + @override + String get sl_ => 'سیرالیۆن'; + + @override + String get sm_ => 'سان مارینۆ'; + + @override + String get sn_ => 'سەنیگال'; + + @override + String get so_ => 'سۆماڵ'; + + @override + String get sr_ => 'سورینام'; + + @override + String get ss_ => 'باشووری سودان'; + + @override + String get st_ => 'ساو تۆمێ و پرینسیپی'; + + @override + String get sv_ => 'سلڤادۆر'; + + @override + String get sy_ => 'کۆماری عەرەبی سوریا'; + + @override + String get sz_ => 'سوازیلاند'; + + @override + String get ta_ => 'تریستان دا کونها'; + + @override + String get tc_ => 'دوورگەکانی تورک و کایکۆس'; + + @override + String get td_ => 'چاد'; + + @override + String get tg_ => 'تۆگۆ'; + + @override + String get th_ => 'تایلەند'; + + @override + String get tj_ => 'تاجیکستان'; + + @override + String get tk_ => 'تۆکێلاو'; + + @override + String get tl_ => 'تیمۆر-لێستێ'; + + @override + String get tm_ => 'تورکمانستان'; + + @override + String get tn_ => 'تونس'; + + @override + String get to_ => 'تۆنگا'; + + @override + String get tr_ => 'تورکیە'; + + @override + String get tt_ => 'ترینیداد و تۆباگۆ'; + + @override + String get tv_ => 'توڤالو'; + + @override + String get tw_ => 'تایوان'; + + @override + String get tz_ => 'تانزانیا، کۆماری یەکگرتووی تانزانیا'; + + @override + String get ua_ => 'ئۆکرانیا'; + + @override + String get ug_ => 'ئۆگاندا'; + + @override + String get us_ => 'ویلایەتە یەکگرتووەکان'; + + @override + String get uy_ => 'ئۆرۆگوای'; + + @override + String get uz_ => 'ئۆزبەکستان'; + + @override + String get va_ => 'کورسی پیرۆز (وڵاتی ڤاتیکان سیتی)'; + + @override + String get vc_ => 'سانت ڤینسێنت و گرێنادین'; + + @override + String get ve_ => 'ڤەنزوێلا'; + + @override + String get vg_ => 'دوورگەکانی ڤێرجینیا، بەریتانیا'; + + @override + String get vi_ => 'دوورگەکانی ڤێرجینیا، ئەمریکا.'; + + @override + String get vn_ => 'ڤێتنام'; + + @override + String get vu_ => 'ڤانواتو'; + + @override + String get wf_ => 'والیس و فوتونا'; + + @override + String get ws_ => 'ساموا'; + + @override + String get ye_ => 'یەمەن'; + + @override + String get yt_ => 'مایۆت'; + + @override + String get za_ => 'باشوری ئەفریقا'; + + @override + String get zm_ => 'زامبیا'; + + @override + String get zw_ => 'زیمبابۆی'; +} diff --git a/lib/l10n/generated/phone_field_localization_nb.dart b/lib/src/localization/generated/phone_field_localization_nb.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_nb.dart rename to lib/src/localization/generated/phone_field_localization_nb.dart index 6789e8dc..f7448c8f 100644 --- a/lib/l10n/generated/phone_field_localization_nb.dart +++ b/lib/src/localization/generated/phone_field_localization_nb.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Norwegian Bokmål (`nb`). class PhoneFieldLocalizationNb extends PhoneFieldLocalization { - PhoneFieldLocalizationNb([String locale = 'nb']) : super(locale); + PhoneFieldLocalizationNb([super.locale = 'nb']); @override String get invalidPhoneNumber => 'Ugyldig telefonnummer'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationNb extends PhoneFieldLocalization { @override String get noResultMessage => 'Ingen resultater'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_nl.dart b/lib/src/localization/generated/phone_field_localization_nl.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_nl.dart rename to lib/src/localization/generated/phone_field_localization_nl.dart index d8e4f4b5..7eba73e7 100644 --- a/lib/l10n/generated/phone_field_localization_nl.dart +++ b/lib/src/localization/generated/phone_field_localization_nl.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Dutch Flemish (`nl`). class PhoneFieldLocalizationNl extends PhoneFieldLocalization { - PhoneFieldLocalizationNl([String locale = 'nl']) : super(locale); + PhoneFieldLocalizationNl([super.locale = 'nl']); @override String get invalidPhoneNumber => 'Ongeldig telefoonnummer'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationNl extends PhoneFieldLocalization { @override String get noResultMessage => 'Geen resultaat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Hemelvaart Eiland'; diff --git a/lib/l10n/generated/phone_field_localization_pt.dart b/lib/src/localization/generated/phone_field_localization_pt.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_pt.dart rename to lib/src/localization/generated/phone_field_localization_pt.dart index 76ae0d1a..41bda7f7 100644 --- a/lib/l10n/generated/phone_field_localization_pt.dart +++ b/lib/src/localization/generated/phone_field_localization_pt.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Portuguese (`pt`). class PhoneFieldLocalizationPt extends PhoneFieldLocalization { - PhoneFieldLocalizationPt([String locale = 'pt']) : super(locale); + PhoneFieldLocalizationPt([super.locale = 'pt']); @override String get invalidPhoneNumber => 'Número de telefone inválido'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationPt extends PhoneFieldLocalization { @override String get noResultMessage => 'Sem resultado'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ilha da Ascensão'; diff --git a/lib/l10n/generated/phone_field_localization_ru.dart b/lib/src/localization/generated/phone_field_localization_ru.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_ru.dart rename to lib/src/localization/generated/phone_field_localization_ru.dart index 0d1e0123..a74ed943 100644 --- a/lib/l10n/generated/phone_field_localization_ru.dart +++ b/lib/src/localization/generated/phone_field_localization_ru.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Russian (`ru`). class PhoneFieldLocalizationRu extends PhoneFieldLocalization { - PhoneFieldLocalizationRu([String locale = 'ru']) : super(locale); + PhoneFieldLocalizationRu([super.locale = 'ru']); @override String get invalidPhoneNumber => 'Неправильный номер телефона'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationRu extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Неверный номер мобильного телефона'; @override - String get invalidFixedLinePhoneNumber => 'Недействительный номер стационарного телефона'; + String get invalidFixedLinePhoneNumber => + 'Недействительный номер стационарного телефона'; @override String get requiredPhoneNumber => 'Требуется номер телефона'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationRu extends PhoneFieldLocalization { @override String get noResultMessage => 'Безрезультатно'; + @override + String get search => 'Search'; + @override String get ac_ => 'Остров Вознесения'; diff --git a/lib/l10n/generated/phone_field_localization_sv.dart b/lib/src/localization/generated/phone_field_localization_sv.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_sv.dart rename to lib/src/localization/generated/phone_field_localization_sv.dart index d4decae0..1d8b8595 100644 --- a/lib/l10n/generated/phone_field_localization_sv.dart +++ b/lib/src/localization/generated/phone_field_localization_sv.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Swedish (`sv`). class PhoneFieldLocalizationSv extends PhoneFieldLocalization { - PhoneFieldLocalizationSv([String locale = 'sv']) : super(locale); + PhoneFieldLocalizationSv([super.locale = 'sv']); @override String get invalidPhoneNumber => 'Ogiltigt telefonnummer'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationSv extends PhoneFieldLocalization { @override String get noResultMessage => 'Inget resultat'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_tr.dart b/lib/src/localization/generated/phone_field_localization_tr.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_tr.dart rename to lib/src/localization/generated/phone_field_localization_tr.dart index a04d2cef..cbbd2995 100644 --- a/lib/l10n/generated/phone_field_localization_tr.dart +++ b/lib/src/localization/generated/phone_field_localization_tr.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Turkish (`tr`). class PhoneFieldLocalizationTr extends PhoneFieldLocalization { - PhoneFieldLocalizationTr([String locale = 'tr']) : super(locale); + PhoneFieldLocalizationTr([super.locale = 'tr']); @override String get invalidPhoneNumber => 'Geçersiz telefon numarası'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationTr extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Geçersiz cep telefonu numarası'; @override - String get invalidFixedLinePhoneNumber => 'Geçersiz sabit hat telefon numarası'; + String get invalidFixedLinePhoneNumber => + 'Geçersiz sabit hat telefon numarası'; @override String get requiredPhoneNumber => 'Telefon numarası gerekli'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationTr extends PhoneFieldLocalization { @override String get noResultMessage => 'Sonuç yok'; + @override + String get search => 'Search'; + @override String get ac_ => 'Yükselme adası'; diff --git a/lib/l10n/generated/phone_field_localization_uk.dart b/lib/src/localization/generated/phone_field_localization_uk.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_uk.dart rename to lib/src/localization/generated/phone_field_localization_uk.dart index 1647cfaf..08d56b90 100644 --- a/lib/l10n/generated/phone_field_localization_uk.dart +++ b/lib/src/localization/generated/phone_field_localization_uk.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Ukrainian (`uk`). class PhoneFieldLocalizationUk extends PhoneFieldLocalization { - PhoneFieldLocalizationUk([String locale = 'uk']) : super(locale); + PhoneFieldLocalizationUk([super.locale = 'uk']); @override String get invalidPhoneNumber => 'Невірний номер телефону'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationUk extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Невірний номер мобільного телефону'; @override - String get invalidFixedLinePhoneNumber => 'Невірний номер стаціонарного телефону'; + String get invalidFixedLinePhoneNumber => + 'Невірний номер стаціонарного телефону'; @override String get requiredPhoneNumber => 'Необхідний номер телефону'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationUk extends PhoneFieldLocalization { @override String get noResultMessage => 'Немає результату'; + @override + String get search => 'Search'; + @override String get ac_ => 'Острів Вознесіння'; diff --git a/lib/l10n/generated/phone_field_localization_uz.dart b/lib/src/localization/generated/phone_field_localization_uz.dart similarity index 98% rename from lib/l10n/generated/phone_field_localization_uz.dart rename to lib/src/localization/generated/phone_field_localization_uz.dart index 9451ad63..26e54aa8 100644 --- a/lib/l10n/generated/phone_field_localization_uz.dart +++ b/lib/src/localization/generated/phone_field_localization_uz.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Uzbek (`uz`). class PhoneFieldLocalizationUz extends PhoneFieldLocalization { - PhoneFieldLocalizationUz([String locale = 'uz']) : super(locale); + PhoneFieldLocalizationUz([super.locale = 'uz']); @override String get invalidPhoneNumber => 'Telefon raqami noto‘g‘ri'; @@ -14,7 +14,8 @@ class PhoneFieldLocalizationUz extends PhoneFieldLocalization { String get invalidMobilePhoneNumber => 'Telfon raqami noto‘g‘ri'; @override - String get invalidFixedLinePhoneNumber => 'Ruxsat etilgan telefon raqami yaroqsiz'; + String get invalidFixedLinePhoneNumber => + 'Ruxsat etilgan telefon raqami yaroqsiz'; @override String get requiredPhoneNumber => 'Telfon raqami majburiy'; @@ -22,6 +23,9 @@ class PhoneFieldLocalizationUz extends PhoneFieldLocalization { @override String get noResultMessage => 'Ma\'lumot topilmadi'; + @override + String get search => 'Search'; + @override String get ac_ => 'Ascension Island'; diff --git a/lib/l10n/generated/phone_field_localization_zh.dart b/lib/src/localization/generated/phone_field_localization_zh.dart similarity index 99% rename from lib/l10n/generated/phone_field_localization_zh.dart rename to lib/src/localization/generated/phone_field_localization_zh.dart index a5d67983..0e56aa22 100644 --- a/lib/l10n/generated/phone_field_localization_zh.dart +++ b/lib/src/localization/generated/phone_field_localization_zh.dart @@ -2,7 +2,7 @@ import 'phone_field_localization.dart'; /// The translations for Chinese (`zh`). class PhoneFieldLocalizationZh extends PhoneFieldLocalization { - PhoneFieldLocalizationZh([String locale = 'zh']) : super(locale); + PhoneFieldLocalizationZh([super.locale = 'zh']); @override String get invalidPhoneNumber => '无效的电话号码'; @@ -22,6 +22,9 @@ class PhoneFieldLocalizationZh extends PhoneFieldLocalization { @override String get noResultMessage => '没有结果'; + @override + String get search => 'Search'; + @override String get ac_ => '阿森松岛'; diff --git a/lib/src/localization/localization.dart b/lib/src/localization/localization.dart new file mode 100644 index 00000000..ed37c65f --- /dev/null +++ b/lib/src/localization/localization.dart @@ -0,0 +1,251 @@ +import 'package:phone_form_field/phone_form_field.dart'; + +export 'generated/phone_field_localization.dart'; +export 'generated/phone_field_localization_en.dart'; + +extension DynamicLocalization on PhoneFieldLocalization { + countryName(IsoCode isoCode) { + return switch (isoCode) { + IsoCode.AC => ac_, + IsoCode.AD => ad_, + IsoCode.AE => ae_, + IsoCode.AF => af_, + IsoCode.AG => ag_, + IsoCode.AI => ai_, + IsoCode.AL => al_, + IsoCode.AM => am_, + IsoCode.AO => ao_, + IsoCode.AR => ar_, + IsoCode.AS => as_, + IsoCode.AT => at_, + IsoCode.AU => au_, + IsoCode.AW => aw_, + IsoCode.AX => ax_, + IsoCode.AZ => az_, + IsoCode.BA => ba_, + IsoCode.BB => bb_, + IsoCode.BD => bd_, + IsoCode.BE => be_, + IsoCode.BF => bf_, + IsoCode.BG => bg_, + IsoCode.BH => bh_, + IsoCode.BI => bi_, + IsoCode.BJ => bj_, + IsoCode.BL => bl_, + IsoCode.BM => bm_, + IsoCode.BN => bn_, + IsoCode.BO => bo_, + IsoCode.BQ => bq_, + IsoCode.BR => br_, + IsoCode.BS => bs_, + IsoCode.BT => bt_, + IsoCode.BW => bw_, + IsoCode.BY => by_, + IsoCode.BZ => bz_, + IsoCode.CA => ca_, + IsoCode.CC => cc_, + IsoCode.CD => cd_, + IsoCode.CF => cf_, + IsoCode.CG => cg_, + IsoCode.CH => ch_, + IsoCode.CI => ci_, + IsoCode.CK => ck_, + IsoCode.CL => cl_, + IsoCode.CM => cm_, + IsoCode.CN => cn_, + IsoCode.CO => co_, + IsoCode.CR => cr_, + IsoCode.CU => cu_, + IsoCode.CV => cv_, + IsoCode.CX => cx_, + IsoCode.CY => cy_, + IsoCode.CZ => cz_, + IsoCode.DE => de_, + IsoCode.DJ => dj_, + IsoCode.DK => dk_, + IsoCode.DM => dm_, + IsoCode.DO => do_, + IsoCode.DZ => dz_, + IsoCode.EC => ec_, + IsoCode.EE => ee_, + IsoCode.EG => eg_, + IsoCode.ER => er_, + IsoCode.ES => es_, + IsoCode.ET => et_, + IsoCode.FI => fi_, + IsoCode.FJ => fj_, + IsoCode.FK => fk_, + IsoCode.FM => fm_, + IsoCode.FO => fo_, + IsoCode.FR => fr_, + IsoCode.GA => ga_, + IsoCode.GB => gb_, + IsoCode.GD => gd_, + IsoCode.GE => ge_, + IsoCode.GF => gf_, + IsoCode.GG => gg_, + IsoCode.GH => gh_, + IsoCode.GI => gi_, + IsoCode.GL => gl_, + IsoCode.GM => gm_, + IsoCode.GN => gn_, + IsoCode.GP => gp_, + IsoCode.GQ => gq_, + IsoCode.GR => gr_, + IsoCode.GT => gt_, + IsoCode.GU => gu_, + IsoCode.GW => gw_, + IsoCode.GY => gy_, + IsoCode.HK => hk_, + IsoCode.HN => hn_, + IsoCode.HR => hr_, + IsoCode.HT => ht_, + IsoCode.HU => hu_, + IsoCode.ID => id_, + IsoCode.IE => ie_, + IsoCode.IL => il_, + IsoCode.IM => im_, + IsoCode.IN => in_, + IsoCode.IO => io_, + IsoCode.IQ => iq_, + IsoCode.IR => ir_, + IsoCode.IS => is_, + IsoCode.IT => it_, + IsoCode.JE => je_, + IsoCode.JM => jm_, + IsoCode.JO => jo_, + IsoCode.JP => jp_, + IsoCode.KE => ke_, + IsoCode.KG => kg_, + IsoCode.KH => kh_, + IsoCode.KI => ki_, + IsoCode.KM => km_, + IsoCode.KN => kn_, + IsoCode.KP => kp_, + IsoCode.KR => kr_, + IsoCode.KW => kw_, + IsoCode.KY => ky_, + IsoCode.KZ => kz_, + IsoCode.LA => la_, + IsoCode.LB => lb_, + IsoCode.LC => lc_, + IsoCode.LI => li_, + IsoCode.LK => lk_, + IsoCode.LR => lr_, + IsoCode.LS => ls_, + IsoCode.LT => lt_, + IsoCode.LU => lu_, + IsoCode.LV => lv_, + IsoCode.LY => ly_, + IsoCode.MA => ma_, + IsoCode.MC => mc_, + IsoCode.MD => md_, + IsoCode.ME => me_, + IsoCode.MF => mf_, + IsoCode.MG => mg_, + IsoCode.MH => mh_, + IsoCode.MK => mk_, + IsoCode.ML => ml_, + IsoCode.MM => mm_, + IsoCode.MN => mn_, + IsoCode.MO => mo_, + IsoCode.MP => mp_, + IsoCode.MQ => mq_, + IsoCode.MR => mr_, + IsoCode.MS => ms_, + IsoCode.MT => mt_, + IsoCode.MU => mu_, + IsoCode.MV => mv_, + IsoCode.MW => mw_, + IsoCode.MX => mx_, + IsoCode.MY => my_, + IsoCode.MZ => mz_, + IsoCode.NA => na_, + IsoCode.NC => nc_, + IsoCode.NE => ne_, + IsoCode.NF => nf_, + IsoCode.NG => ng_, + IsoCode.NI => ni_, + IsoCode.NL => nl_, + IsoCode.NO => no_, + IsoCode.NP => np_, + IsoCode.NR => nr_, + IsoCode.NU => nu_, + IsoCode.NZ => nz_, + IsoCode.OM => om_, + IsoCode.PA => pa_, + IsoCode.PE => pe_, + IsoCode.PF => pf_, + IsoCode.PG => pg_, + IsoCode.PH => ph_, + IsoCode.PK => pk_, + IsoCode.PL => pl_, + IsoCode.PM => pm_, + IsoCode.PR => pr_, + IsoCode.PS => ps_, + IsoCode.PT => pt_, + IsoCode.PW => pw_, + IsoCode.PY => py_, + IsoCode.QA => qa_, + IsoCode.RE => re_, + IsoCode.RO => ro_, + IsoCode.RS => rs_, + IsoCode.RU => ru_, + IsoCode.RW => rw_, + IsoCode.SA => sa_, + IsoCode.SB => sb_, + IsoCode.SC => sc_, + IsoCode.SD => sd_, + IsoCode.SE => se_, + IsoCode.SG => sg_, + IsoCode.SI => si_, + IsoCode.SK => sk_, + IsoCode.SL => sl_, + IsoCode.SM => sm_, + IsoCode.SN => sn_, + IsoCode.SO => so_, + IsoCode.SR => sr_, + IsoCode.SS => ss_, + IsoCode.ST => st_, + IsoCode.SV => sv_, + IsoCode.SY => sy_, + IsoCode.SZ => sz_, + IsoCode.TA => ta_, + IsoCode.TC => tc_, + IsoCode.TD => td_, + IsoCode.TG => tg_, + IsoCode.TH => th_, + IsoCode.TJ => tj_, + IsoCode.TK => tk_, + IsoCode.TL => tl_, + IsoCode.TM => tm_, + IsoCode.TN => tn_, + IsoCode.TO => to_, + IsoCode.TR => tr_, + IsoCode.TT => tt_, + IsoCode.TV => tv_, + IsoCode.TW => tw_, + IsoCode.TZ => tz_, + IsoCode.UA => ua_, + IsoCode.UG => ug_, + IsoCode.US => us_, + IsoCode.UY => uy_, + IsoCode.UZ => uz_, + IsoCode.VA => va_, + IsoCode.VC => vc_, + IsoCode.VE => ve_, + IsoCode.VG => vg_, + IsoCode.VI => vi_, + IsoCode.VN => vn_, + IsoCode.VU => vu_, + IsoCode.WF => wf_, + IsoCode.WS => ws_, + IsoCode.YE => ye_, + IsoCode.YT => yt_, + IsoCode.ZA => za_, + IsoCode.ZM => zm_, + IsoCode.ZW => zw_, + _ => '?' + }; + } +} diff --git a/lib/src/phone_controller.dart b/lib/src/phone_controller.dart new file mode 100644 index 00000000..747897dd --- /dev/null +++ b/lib/src/phone_controller.dart @@ -0,0 +1,108 @@ +part of 'phone_form_field.dart'; + +class PhoneController extends ChangeNotifier { + /// focus node of the national number + // final FocusNode focusNode; + final PhoneNumber initialValue; + PhoneNumber _value; + PhoneNumber get value => _value; + + set value(PhoneNumber phoneNumber) { + _value = phoneNumber; + final formattedNsn = _value.formatNsn(); + if (_formattedNationalNumberController.text != formattedNsn) { + changeNationalNumber(formattedNsn); + } else { + notifyListeners(); + } + } + + /// text editing controller of the nsn ( where user types the phone number ) + late final TextEditingController _formattedNationalNumberController; + PhoneController({ + this.initialValue = const PhoneNumber(isoCode: IsoCode.US, nsn: ''), + }) : _value = initialValue, + _formattedNationalNumberController = TextEditingController( + text: initialValue.formatNsn(), + ); + + changeCountry(IsoCode isoCode) { + _value = PhoneNumber.parse( + _value.nsn, + destinationCountry: isoCode, + ); + notifyListeners(); + } + + changeNationalNumber(String? text) { + text = text ?? ''; + var newFormattedText = text; + + // if starts with + then we parse the whole number + final startsWithPlus = + text.startsWith(RegExp('[${AllowedCharacters.plus}]')); + + if (startsWithPlus) { + final phoneNumber = _tryParseWithPlus(text); + // if we could parse the phone number we can change the value inside + // the national number field to remove the "+ country dial code" + if (phoneNumber != null) { + _value = phoneNumber; + newFormattedText = _value.formatNsn(); + } + } else { + final phoneNumber = PhoneNumber.parse( + text, + destinationCountry: _value.isoCode, + ); + _value = phoneNumber; + newFormattedText = phoneNumber.formatNsn(); + } + _formattedNationalNumberController.value = TextEditingValue( + text: newFormattedText, + selection: _computeSelection(text, newFormattedText), + ); + notifyListeners(); + } + + /// When the cursor is at the end of the text we need to preserve that. + /// Since there is formatting going on we need to explicitely do it. + /// We don't want to do it in the middle because the user might have + /// used arrow keys to move inside the text. + TextSelection _computeSelection(String originalText, String newText) { + final currentSelectionOffset = + _formattedNationalNumberController.selection.extentOffset; + final isCursorAtEnd = currentSelectionOffset == originalText.length; + var offset = currentSelectionOffset; + + if (isCursorAtEnd || currentSelectionOffset >= newText.length) { + offset = newText.length; + } + return TextSelection.fromPosition( + TextPosition(offset: offset), + ); + } + + PhoneNumber? _tryParseWithPlus(String text) { + try { + return PhoneNumber.parse(text); + // parsing "+", a country code won't be found + } on PhoneNumberException { + return null; + } + } + + selectNationalNumber() { + _formattedNationalNumberController.selection = TextSelection( + baseOffset: 0, + extentOffset: _formattedNationalNumberController.value.text.length, + ); + notifyListeners(); + } + + @override + void dispose() { + _formattedNationalNumberController.dispose(); + super.dispose(); + } +} diff --git a/lib/src/phone_form_field.dart b/lib/src/phone_form_field.dart new file mode 100644 index 00000000..c58701c4 --- /dev/null +++ b/lib/src/phone_form_field.dart @@ -0,0 +1,196 @@ +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:circle_flags/circle_flags.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:phone_form_field/src/validation/allowed_characters.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +import 'country/country_button.dart'; +import 'country_selection/country_selector_navigator.dart'; + +part 'phone_controller.dart'; +part 'phone_form_field_state.dart'; + +/// Phone input extending form field. +/// +/// ### controller: +/// {@template controller} +/// Use a [PhoneController] for PhoneFormField when you need to dynamically +/// change the value. +/// +/// Whenever the user modifies the phone field with an +/// associated [controller], the phone field updates +/// the display value and the controller notifies its listeners. +/// {@endtemplate} +/// +/// You can also use an [initialValue]: +/// {@template initialValue} +/// The initial value used. +/// +/// Only one of [initialValue] and [controller] can be specified. +/// If [controller] is specified the [initialValue] will be +/// the first value of the [PhoneController] +/// {@endtemplate} +class PhoneFormField extends FormField { + /// {@macro controller} + final PhoneController? controller; + + final bool shouldFormat; + + /// callback called when the input value changes + final Function(PhoneNumber)? onChanged; + + /// country that is displayed when there is no value + final IsoCode defaultCountry; + + /// the focusNode of the national number + final FocusNode? focusNode; + + /// how to display the country selection + final CountrySelectorNavigator countrySelectorNavigator; + + /// padding inside country button, + /// this can be used to align the country button with the phone number + /// and is mostly useful when using [isCountryButtonPersistent] as true. + final EdgeInsets? countryButtonPadding; + + /// whether the user can select a new country when pressing the country button + final bool isCountrySelectionEnabled; + + /// This controls wether the country button is alway shown or is hidden by + /// the label when the national number is empty. In flutter terms this controls + /// whether the country button is shown as a prefix or prefixIcon inside + /// the text field. + final bool isCountryButtonPersistent; + + /// show Dial Code or not in the country button + final bool showDialCode; + + /// show selected iso code or not in the country button + final bool showIsoCodeInInput; + + /// The size of the flag inside the country button + final double flagSize; + + /// whether the flag is shown inside the country button + final bool showFlagInInput; + + // textfield inputs + final InputDecoration decoration; + final TextInputType keyboardType; + final TextInputAction? textInputAction; + final TextStyle? style; + final TextStyle? countryCodeStyle; + final StrutStyle? strutStyle; + final TextAlign textAlign; + final TextAlignVertical? textAlignVertical; + final bool autofocus; + final String obscuringCharacter; + final bool obscureText; + final bool autocorrect; + final SmartDashesType? smartDashesType; + final SmartQuotesType? smartQuotesType; + final bool enableSuggestions; + final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder; + final bool? showCursor; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final AppPrivateCommandCallback? onAppPrivateCommand; + final Function(PointerDownEvent)? onTapOutside; + final double cursorWidth; + final double? cursorHeight; + final Radius? cursorRadius; + final Color? cursorColor; + final ui.BoxHeightStyle selectionHeightStyle; + final ui.BoxWidthStyle selectionWidthStyle; + final Brightness? keyboardAppearance; + final EdgeInsets scrollPadding; + final bool enableInteractiveSelection; + final TextSelectionControls? selectionControls; + bool get selectionEnabled => enableInteractiveSelection; + final MouseCursor? mouseCursor; + final ScrollPhysics? scrollPhysics; + final ScrollController? scrollController; + final Iterable? autofillHints; + final bool enableIMEPersonalizedLearning; + final List? inputFormatters; + + PhoneFormField({ + super.key, + this.controller, + @Deprecated('This is now always true and has no effect anymore') + this.shouldFormat = true, + this.onChanged, + this.focusNode, + this.showFlagInInput = true, + this.countrySelectorNavigator = const CountrySelectorNavigator.page(), + @Deprecated( + 'Use [initialValue] or [controller] to set the initial phone number') + this.defaultCountry = IsoCode.US, + this.flagSize = 16, + this.isCountrySelectionEnabled = true, + bool? isCountryButtonPersistent, + @Deprecated('Use [isCountryButtonPersistent]') + bool? isCountryChipPersistent, + this.showDialCode = true, + this.showIsoCodeInInput = false, + this.countryButtonPadding, + // form field inputs + super.validator, + super.initialValue, + super.onSaved, + super.autovalidateMode = AutovalidateMode.onUserInteraction, + super.restorationId, + super.enabled = true, + // textfield inputs + this.decoration = const InputDecoration(), + this.keyboardType = TextInputType.phone, + this.textInputAction, + this.style, + this.countryCodeStyle, + this.strutStyle, + this.textAlign = TextAlign.start, + this.textAlignVertical, + this.autofocus = false, + this.obscuringCharacter = '*', + this.obscureText = false, + this.autocorrect = true, + this.smartDashesType, + this.smartQuotesType, + this.enableSuggestions = true, + this.contextMenuBuilder, + this.showCursor, + this.onEditingComplete, + this.onSubmitted, + this.onAppPrivateCommand, + this.onTapOutside, + this.inputFormatters, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20.0), + this.enableInteractiveSelection = true, + this.selectionControls, + this.mouseCursor, + this.scrollPhysics, + this.scrollController, + this.autofillHints, + this.enableIMEPersonalizedLearning = true, + }) : assert( + initialValue == null || controller == null, + 'One of initialValue or controller can be specified at a time', + ), + isCountryButtonPersistent = + isCountryButtonPersistent ?? isCountryChipPersistent ?? true, + super( + builder: (state) => (state as PhoneFormFieldState).builder(), + ); + + @override + PhoneFormFieldState createState() => PhoneFormFieldState(); +} diff --git a/lib/src/phone_form_field_state.dart b/lib/src/phone_form_field_state.dart new file mode 100644 index 00000000..9d3464ab --- /dev/null +++ b/lib/src/phone_form_field_state.dart @@ -0,0 +1,196 @@ +part of 'phone_form_field.dart'; + +class PhoneFormFieldState extends FormFieldState { + late final PhoneController controller; + late final FocusNode focusNode; + + @override + PhoneFormField get widget => super.widget as PhoneFormField; + + @override + void initState() { + super.initState(); + + controller = widget.controller ?? + PhoneController( + initialValue: widget.initialValue ?? + // remove this line when defaultCountry is removed (now deprecated) + // and just use the US default country if no initialValue is set + PhoneNumber(isoCode: widget.defaultCountry, nsn: ''), + ); + controller.addListener(_onControllerValueChanged); + focusNode = widget.focusNode ?? FocusNode(); + _preloadFlagsInMemory(); + } + + void _preloadFlagsInMemory() { + CircleFlag.preload(IsoCode.values.map((isoCode) => isoCode.name).toList()); + } + + @override + void dispose() { + controller.removeListener(_onControllerValueChanged); + super.dispose(); + } + + // overriding method from FormFieldState + @override + void didChange(PhoneNumber? value) { + if (value == null) { + return; + } + super.didChange(value); + + if (controller.value != value) { + controller.value = value; + } + } + + void _onControllerValueChanged() { + /// when the controller changes because the user called + /// controller.value = x we need to change the value of the form field + if (controller.value != value) { + didChange(controller.value); + } + } + + void _onTextfieldChanged(String value) { + controller.changeNationalNumber(value); + didChange(controller.value); + widget.onChanged?.call(controller.value); + } + + // overriding method of form field, so when the user resets a form, + // and subsequently every form field descendant, the controller is updated + @override + void reset() { + controller.value = controller.initialValue; + super.reset(); + } + + void _selectCountry() async { + if (!widget.isCountrySelectionEnabled) { + return; + } + final selected = await widget.countrySelectorNavigator.show(context); + if (selected != null) { + controller.changeCountry(selected.isoCode); + } + focusNode.requestFocus(); + } + + Widget builder() { + return TextField( + decoration: widget.decoration.copyWith( + errorText: errorText, + prefixIcon: widget.isCountryButtonPersistent + ? _getCountryCodeChip(context) + : null, + prefix: widget.isCountryButtonPersistent + ? null + : _getCountryCodeChip(context), + ), + controller: controller._formattedNationalNumberController, + focusNode: focusNode, + enabled: widget.enabled, + inputFormatters: widget.inputFormatters ?? + [ + FilteringTextInputFormatter.allow(RegExp( + '[${AllowedCharacters.plus}${AllowedCharacters.digits}${AllowedCharacters.punctuation}]')), + ], + onChanged: _onTextfieldChanged, + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + onTapOutside: widget.onTapOutside, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + ); + } + + Widget _getCountryCodeChip(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, _) => CountryButton( + key: const ValueKey('country-code-chip'), + isoCode: controller.value.isoCode, + onTap: widget.enabled ? _selectCountry : null, + padding: _computeCountryButtonPadding(context), + showFlag: widget.showFlagInInput, + showIsoCode: widget.showIsoCodeInInput, + showDialCode: widget.showDialCode, + textStyle: widget.countryCodeStyle ?? + widget.decoration.labelStyle ?? + TextStyle( + fontSize: 16, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + flagSize: widget.flagSize, + enabled: widget.enabled, + ), + ); + } + + /// computes the padding inside the country button + /// this is used to align the flag and dial code with the rest + /// of the phone number. + /// The padding must work for this matrix: + /// - has label or not + /// - is border underline or outline + /// - is country button shown as a prefix or prefixIcon (isCountryChipPersistent) + /// - text direction + EdgeInsets _computeCountryButtonPadding(BuildContext context) { + final countryButtonPadding = widget.countryButtonPadding; + final isUnderline = widget.decoration.border is UnderlineInputBorder; + final hasLabel = + widget.decoration.label != null || widget.decoration.labelText != null; + final isLtr = Directionality.of(context) == TextDirection.ltr; + + EdgeInsets padding = isLtr + ? const EdgeInsets.fromLTRB(12, 16, 4, 16) + : const EdgeInsets.fromLTRB(4, 16, 12, 16); + if (countryButtonPadding != null) { + padding = countryButtonPadding; + } else if (!widget.isCountryButtonPersistent) { + padding = isLtr + ? const EdgeInsets.only(right: 4, left: 12) + : const EdgeInsets.only(left: 4, right: 12); + } else if (isUnderline && hasLabel) { + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 25, 4, 7) + : const EdgeInsets.fromLTRB(4, 25, 12, 7); + } else if (isUnderline && !hasLabel) { + padding = isLtr + ? const EdgeInsets.fromLTRB(12, 2, 4, 0) + : const EdgeInsets.fromLTRB(4, 2, 12, 0); + } + return padding; + } +} diff --git a/lib/src/constants/patterns.dart b/lib/src/validation/allowed_characters.dart similarity index 90% rename from lib/src/constants/patterns.dart rename to lib/src/validation/allowed_characters.dart index fa0833a0..78871b29 100644 --- a/lib/src/constants/patterns.dart +++ b/lib/src/validation/allowed_characters.dart @@ -1,4 +1,4 @@ -class Patterns { +class AllowedCharacters { /// accepted punctuation within a phone number static const String punctuation = r' ()\[\]\-\.\/\\'; static const String plus = r'\++'; diff --git a/lib/src/validation/localized_validation_error.dart b/lib/src/validation/localized_validation_error.dart new file mode 100644 index 00000000..7da57585 --- /dev/null +++ b/lib/src/validation/localized_validation_error.dart @@ -0,0 +1,3 @@ +// we do not have access to context inside a validator, +// thus we return an error code, and localize that code in the view +// where it is easier to access the context \ No newline at end of file diff --git a/lib/src/validation/phone_validator.dart b/lib/src/validation/phone_validator.dart index 2f62ff45..1e44f870 100644 --- a/lib/src/validation/phone_validator.dart +++ b/lib/src/validation/phone_validator.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; import 'package:phone_form_field/phone_form_field.dart'; typedef PhoneNumberInputValidator = String? Function(PhoneNumber? phoneNumber); -class PhoneValidator { +abstract class PhoneValidator { /// allow to compose several validators /// Note that validator list order is important as first /// validator failing will return according message. @@ -20,174 +21,114 @@ class PhoneValidator { }; } - static PhoneNumberInputValidator required({ + static PhoneNumberInputValidator required( + BuildContext context, { /// custom error message String? errorText, }) { return (PhoneNumber? valueCandidate) { if (valueCandidate == null || (valueCandidate.nsn.trim().isEmpty)) { - return errorText ?? 'requiredPhoneNumber'; + return errorText ?? + PhoneFieldLocalization.of(context)?.requiredPhoneNumber ?? + PhoneFieldLocalizationEn().requiredPhoneNumber; } return null; }; } - static PhoneNumberInputValidator invalid({ + static PhoneNumberInputValidator valid( + BuildContext context, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - valid(errorText: errorText, allowEmpty: allowEmpty); - - static PhoneNumberInputValidator valid({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { - return (PhoneNumber? valueCandidate) { - if (valueCandidate == null && !allowEmpty) { - return errorText ?? 'invalidPhoneNumber'; - } if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + valueCandidate.nsn.isNotEmpty && !valueCandidate.isValid()) { - return errorText ?? 'invalidPhoneNumber'; + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? + PhoneFieldLocalizationEn().invalidPhoneNumber; } return null; }; } - @Deprecated('use validType, invalid type naming was backward') - static PhoneNumberInputValidator invalidType( - /// expected phonetype - PhoneNumberType expectedType, { - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validType( - expectedType, - errorText: errorText, - allowEmpty: allowEmpty, - ); - static PhoneNumberInputValidator validType( + BuildContext context, + /// expected phonetype PhoneNumberType expectedType, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { - final defaultMessage = expectedType == PhoneNumberType.mobile - ? 'invalidMobilePhoneNumber' - : 'invalidFixedLinePhoneNumber'; return (PhoneNumber? valueCandidate) { if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + valueCandidate.nsn.isNotEmpty && !valueCandidate.isValid(type: expectedType)) { - return errorText ?? defaultMessage; + if (expectedType == PhoneNumberType.mobile) { + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidMobilePhoneNumber ?? + PhoneFieldLocalizationEn().invalidMobilePhoneNumber; + } else if (expectedType == PhoneNumberType.fixedLine) { + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidFixedLinePhoneNumber ?? + PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber; + } + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidPhoneNumber ?? + PhoneFieldLocalizationEn().invalidPhoneNumber; } return null; }; } - @Deprecated('use validFixedLine, naming was backward') - static PhoneNumberInputValidator invalidFixedLine({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validFixedLine(errorText: errorText, allowEmpty: allowEmpty); - /// convenience shortcut method for /// invalidType(context, PhoneNumberType.fixedLine, ...) - static PhoneNumberInputValidator validFixedLine({ + static PhoneNumberInputValidator validFixedLine( + BuildContext context, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) => validType( + context, PhoneNumberType.fixedLine, errorText: errorText, - allowEmpty: allowEmpty, - ); - - @Deprecated('Use validMobile, naming was backward') - static PhoneNumberInputValidator invalidMobile({ - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validMobile( - errorText: errorText, - allowEmpty: allowEmpty, ); /// convenience shortcut method for /// invalidType(context, PhoneNumberType.mobile, ...) - static PhoneNumberInputValidator validMobile({ + static PhoneNumberInputValidator validMobile( + BuildContext context, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) => validType( + context, PhoneNumberType.mobile, errorText: errorText, - allowEmpty: allowEmpty, - ); - - @Deprecated('Use valid country, naming was backward') - static invalidCountry( - /// list of valid country isocode - List expectedCountries, { - /// custom error message - String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, - }) => - validCountry( - expectedCountries, - errorText: errorText, - allowEmpty: allowEmpty, ); static PhoneNumberInputValidator validCountry( + BuildContext context, + /// list of valid country isocode List expectedCountries, { /// custom error message String? errorText, - - /// determine whether a missing value should be reported as invalid - bool allowEmpty = true, }) { return (PhoneNumber? valueCandidate) { if (valueCandidate != null && - (!allowEmpty || valueCandidate.nsn.isNotEmpty) && + (valueCandidate.nsn.isNotEmpty) && !expectedCountries.contains(valueCandidate.isoCode)) { - return errorText ?? 'invalidCountry'; + return errorText ?? + PhoneFieldLocalization.of(context)?.invalidCountry ?? + PhoneFieldLocalizationEn().invalidCountry; } return null; }; } + @Deprecated('Use null instead') static PhoneNumberInputValidator get none => (PhoneNumber? valueCandidate) { return null; }; diff --git a/lib/src/validation/validator_translator.dart b/lib/src/validation/validator_translator.dart deleted file mode 100644 index a8ee7af4..00000000 --- a/lib/src/validation/validator_translator.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; - -typedef _PhoneValidatorMessageDelegate = String? Function(BuildContext context); - -class ValidatorTranslator { - static final Map _validatorMessages = - { - 'invalidPhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidPhoneNumber, - 'invalidCountry': (ctx) => PhoneFieldLocalization.of(ctx)?.invalidCountry, - 'invalidMobilePhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidMobilePhoneNumber, - 'invalidFixedLinePhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.invalidFixedLinePhoneNumber, - 'requiredPhoneNumber': (ctx) => - PhoneFieldLocalization.of(ctx)?.requiredPhoneNumber, - }; - - static final Map _defaults = { - 'invalidPhoneNumber': 'Invalid phone number', - 'invalidCountry': 'Invalid country', - 'invalidMobilePhoneNumber': 'Invalid mobile phone number', - 'invalidFixedLinePhoneNumber': 'Invalid fixedline phone number', - 'requiredPhoneNumber': 'required phone number', - }; - - /// Localised name depending on the current application locale - /// If you have many LocalisedName to handle in a same context, consider - /// supplying the second optional PhoneFieldLocalization to avoid - /// walking up the widget to get [PhoneFieldLocalization] instance - /// for each call. - static String message( - BuildContext context, - String key, - ) { - String? name = getMessageFromKey(context, key); - return name ?? _defaults[key] ?? key; - } - - static String? getMessageFromKey(BuildContext ctx, String key) { - final _PhoneValidatorMessageDelegate? translateFn = _validatorMessages[key]; - return translateFn?.call(ctx); - } -} diff --git a/lib/src/widgets/country_code_chip.dart b/lib/src/widgets/country_code_chip.dart deleted file mode 100644 index af7f1638..00000000 --- a/lib/src/widgets/country_code_chip.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:circle_flags/circle_flags.dart'; -import 'package:flutter/material.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -import 'country_selector/country.dart'; - -class CountryCodeChip extends StatelessWidget { - final Country country; - final bool showFlag; - final bool showDialCode; - final TextStyle textStyle; - final EdgeInsets padding; - final double flagSize; - final TextDirection? textDirection; - final bool showIsoCode; - final bool enabled; - - CountryCodeChip({ - super.key, - required IsoCode isoCode, - this.textStyle = const TextStyle(), - this.showFlag = true, - this.showDialCode = true, - this.padding = const EdgeInsets.all(20), - this.flagSize = 20, - this.textDirection, - this.showIsoCode = false, - this.enabled = true, - }) : country = Country(isoCode, ''); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (showIsoCode) ...[ - Text( - country.isoCode.name, - style: textStyle.copyWith( - color: enabled ? null : Theme.of(context).disabledColor, - ), - ), - const SizedBox(width: 8), - ], - if (showFlag) ...[ - CircleFlag( - country.isoCode.name, - size: flagSize, - ), - const SizedBox(width: 8), - ], - if (showDialCode) - Text( - country.displayCountryCode, - style: textStyle.copyWith( - color: enabled ? null : Theme.of(context).disabledColor, - ), - textDirection: textDirection, - ), - ], - ); - } -} diff --git a/lib/src/widgets/country_selector/country.dart b/lib/src/widgets/country_selector/country.dart deleted file mode 100644 index cc978241..00000000 --- a/lib/src/widgets/country_selector/country.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:phone_numbers_parser/metadata.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -/// Country regroup informations for displaying a list of countries -class Country { - /// Country alpha-2 iso code - final IsoCode isoCode; - - /// localized name of the country - final String name; - - /// country dialing code to call them internationally - final String countryCode; - - /// returns "+ [countryCode]" - String get displayCountryCode => '+ $countryCode'; - - Country(this.isoCode, this.name) - : countryCode = metadataByIsoCode[isoCode]?.countryCode ?? ''; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Country && - runtimeType == other.runtimeType && - isoCode == other.isoCode; - - @override - int get hashCode => isoCode.hashCode; - - @override - String toString() { - return 'Country{isoCode: $isoCode}'; - } -} diff --git a/lib/src/widgets/country_selector/country_selector_page.dart b/lib/src/widgets/country_selector/country_selector_page.dart deleted file mode 100644 index a51769c5..00000000 --- a/lib/src/widgets/country_selector/country_selector_page.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:circle_flags/circle_flags.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization.dart'; -import 'package:phone_form_field/l10n/generated/phone_field_localization_en.dart'; -import 'package:phone_form_field/src/widgets/country_selector/localized_country_registry.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -import 'country_finder.dart'; -import 'country.dart'; -import 'country_list.dart'; - -class CountrySelectorSearchDelegate extends SearchDelegate { - late CountryFinder _countryFinder; - late CountryFinder _favoriteCountryFinder; - - /// List of countries to display in the selector - /// Value optional in constructor. - /// when omitted, the full country list is displayed - final List countriesIso; - - /// Callback triggered when user select a country - final ValueChanged onCountrySelected; - - /// ListView.builder scroll controller (ie: [ScrollView.controller]) - final ScrollController? scrollController; - - /// The [ScrollPhysics] of the Country List - final ScrollPhysics? scrollPhysics; - - /// Determine the countries to be displayed on top of the list - /// Check [addFavoritesSeparator] property to enable/disable adding a - /// list divider between favorites and others defaults countries - final List favoriteCountriesIso; - - /// Whether to add a list divider between favorites & defaults - /// countries. - final bool addFavoritesSeparator; - - /// Whether to show the country country code (ie: +1 / +33 /...) - /// as a listTile subtitle - final bool showCountryCode; - - /// The message displayed instead of the list when the search has no results - final String? noResultMessage; - - /// whether the search input is auto focussed - final bool searchAutofocus; - final double flagSize; - - LocalizedCountryRegistry? _localizedCountryRegistry; - - /// Override default title TextStyle - final TextStyle? titleStyle; - - /// Override default subtitle TextStyle - final TextStyle? subtitleStyle; - - final FlagCache? flagCache; - - /// Override default app bar theme - final ThemeData? customAppBarTheme; - - CountrySelectorSearchDelegate({ - Key? key, - required this.onCountrySelected, - required this.flagCache, - this.scrollController, - this.scrollPhysics, - this.addFavoritesSeparator = true, - this.showCountryCode = false, - this.noResultMessage, - List favoriteCountries = const [], - List? countries, - this.searchAutofocus = kIsWeb, - this.flagSize = 40, - this.titleStyle, - this.subtitleStyle, - this.customAppBarTheme, - }) : countriesIso = countries ?? IsoCode.values, - favoriteCountriesIso = favoriteCountries; - - @override - List? buildActions(BuildContext context) { - return [ - IconButton( - onPressed: () => query = '', - icon: const Icon(Icons.clear), - ), - ]; - } - - void _initIfRequired(BuildContext context) { - final localization = - PhoneFieldLocalization.of(context) ?? PhoneFieldLocalizationEn(); - final countryRegistry = LocalizedCountryRegistry.cached(localization); - // if localization has not changed no need to do anything - if (countryRegistry == _localizedCountryRegistry) { - return; - } - _localizedCountryRegistry = countryRegistry; - final notFavoriteCountries = countryRegistry.whereIsoIn( - countriesIso, - omit: favoriteCountriesIso, - ); - final favoriteCountries = countryRegistry.whereIsoIn(favoriteCountriesIso); - _countryFinder = CountryFinder(notFavoriteCountries); - _favoriteCountryFinder = CountryFinder(favoriteCountries, sort: false); - } - - void _updateList() { - _countryFinder.filter(query); - _favoriteCountryFinder.filter(query); - } - - @override - Widget? buildLeading(BuildContext context) { - return BackButton( - onPressed: () => Navigator.of(context).pop(), - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - _initIfRequired(context); - _updateList(); - - return CountryList( - favorites: _favoriteCountryFinder.filteredCountries, - countries: _countryFinder.filteredCountries, - showDialCode: showCountryCode, - onTap: onCountrySelected, - flagSize: flagSize, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - noResultMessage: noResultMessage, - titleStyle: titleStyle, - subtitleStyle: subtitleStyle, - flagCache: flagCache, - ); - } - - @override - Widget buildResults(BuildContext context) { - return buildSuggestions(context); - } - - @override - ThemeData appBarTheme(BuildContext context) { - return customAppBarTheme ?? super.appBarTheme(context); - } -} diff --git a/lib/src/widgets/country_selector/localized_country_registry.dart b/lib/src/widgets/country_selector/localized_country_registry.dart deleted file mode 100644 index 3418e157..00000000 --- a/lib/src/widgets/country_selector/localized_country_registry.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:phone_form_field/phone_form_field.dart'; - -// NOTE: this is ugly, something else needs to be used. -// If someone has the hearth for some refactor... - -/// this saves the localized countries for each country -/// for a given language in a cache, so it does not -/// have to be recreated - -class LocalizedCountryRegistry { - final PhoneFieldLocalization _localization; - - static LocalizedCountryRegistry? _instance; - - late final Map _localizedCountries = Map.fromIterable( - // remove iso codes that do not have a traduction yet.. - IsoCode.values.where((iso) => _names.containsKey(iso)), - value: (isoCode) => Country(isoCode, _names[isoCode]!), - ); - - LocalizedCountryRegistry._(this._localization); - - factory LocalizedCountryRegistry.cached(PhoneFieldLocalization localization) { - final instance = _instance; - if (instance != null && instance._localization == localization) { - return instance; - } - return LocalizedCountryRegistry._(localization); - } - - Country? find(IsoCode isoCode) => _localizedCountries[isoCode]; - - /// gets localized countries from isocodes - List whereIsoIn( - List isoCodes, { - List omit = const [], - }) { - final omitSet = Set.from(omit); - return isoCodes - .where((isoCode) => !omitSet.contains(isoCode)) - .where((isoCode) => _localizedCountries.containsKey(isoCode)) - .map((iso) => _localizedCountries[iso]!) - .toList(); - } - - late final Map _names = { - IsoCode.AC: _localization.ac_, - IsoCode.AD: _localization.ad_, - IsoCode.AE: _localization.ae_, - IsoCode.AF: _localization.af_, - IsoCode.AG: _localization.ag_, - IsoCode.AI: _localization.ai_, - IsoCode.AL: _localization.al_, - IsoCode.AM: _localization.am_, - IsoCode.AO: _localization.ao_, - IsoCode.AR: _localization.ar_, - IsoCode.AS: _localization.as_, - IsoCode.AT: _localization.at_, - IsoCode.AU: _localization.au_, - IsoCode.AW: _localization.aw_, - IsoCode.AX: _localization.ax_, - IsoCode.AZ: _localization.az_, - IsoCode.BA: _localization.ba_, - IsoCode.BB: _localization.bb_, - IsoCode.BD: _localization.bd_, - IsoCode.BE: _localization.be_, - IsoCode.BF: _localization.bf_, - IsoCode.BG: _localization.bg_, - IsoCode.BH: _localization.bh_, - IsoCode.BI: _localization.bi_, - IsoCode.BJ: _localization.bj_, - IsoCode.BL: _localization.bl_, - IsoCode.BM: _localization.bm_, - IsoCode.BN: _localization.bn_, - IsoCode.BO: _localization.bo_, - IsoCode.BQ: _localization.bq_, - IsoCode.BR: _localization.br_, - IsoCode.BS: _localization.bs_, - IsoCode.BT: _localization.bt_, - IsoCode.BW: _localization.bw_, - IsoCode.BY: _localization.by_, - IsoCode.BZ: _localization.bz_, - IsoCode.CA: _localization.ca_, - IsoCode.CC: _localization.cc_, - IsoCode.CD: _localization.cd_, - IsoCode.CF: _localization.cf_, - IsoCode.CG: _localization.cg_, - IsoCode.CH: _localization.ch_, - IsoCode.CI: _localization.ci_, - IsoCode.CK: _localization.ck_, - IsoCode.CL: _localization.cl_, - IsoCode.CM: _localization.cm_, - IsoCode.CN: _localization.cn_, - IsoCode.CO: _localization.co_, - IsoCode.CR: _localization.cr_, - IsoCode.CU: _localization.cu_, - IsoCode.CV: _localization.cv_, - IsoCode.CX: _localization.cx_, - IsoCode.CY: _localization.cy_, - IsoCode.CZ: _localization.cz_, - IsoCode.DE: _localization.de_, - IsoCode.DJ: _localization.dj_, - IsoCode.DK: _localization.dk_, - IsoCode.DM: _localization.dm_, - IsoCode.DO: _localization.do_, - IsoCode.DZ: _localization.dz_, - IsoCode.EC: _localization.ec_, - IsoCode.EE: _localization.ee_, - IsoCode.EG: _localization.eg_, - IsoCode.ER: _localization.er_, - IsoCode.ES: _localization.es_, - IsoCode.ET: _localization.et_, - IsoCode.FI: _localization.fi_, - IsoCode.FJ: _localization.fj_, - IsoCode.FK: _localization.fk_, - IsoCode.FM: _localization.fm_, - IsoCode.FO: _localization.fo_, - IsoCode.FR: _localization.fr_, - IsoCode.GA: _localization.ga_, - IsoCode.GB: _localization.gb_, - IsoCode.GD: _localization.gd_, - IsoCode.GE: _localization.ge_, - IsoCode.GF: _localization.gf_, - IsoCode.GG: _localization.gg_, - IsoCode.GH: _localization.gh_, - IsoCode.GI: _localization.gi_, - IsoCode.GL: _localization.gl_, - IsoCode.GM: _localization.gm_, - IsoCode.GN: _localization.gn_, - IsoCode.GP: _localization.gp_, - IsoCode.GQ: _localization.gq_, - IsoCode.GR: _localization.gr_, - IsoCode.GT: _localization.gt_, - IsoCode.GU: _localization.gu_, - IsoCode.GW: _localization.gw_, - IsoCode.GY: _localization.gy_, - IsoCode.HK: _localization.hk_, - IsoCode.HN: _localization.hn_, - IsoCode.HR: _localization.hr_, - IsoCode.HT: _localization.ht_, - IsoCode.HU: _localization.hu_, - IsoCode.ID: _localization.id_, - IsoCode.IE: _localization.ie_, - IsoCode.IL: _localization.il_, - IsoCode.IM: _localization.im_, - IsoCode.IN: _localization.in_, - IsoCode.IO: _localization.io_, - IsoCode.IQ: _localization.iq_, - IsoCode.IR: _localization.ir_, - IsoCode.IS: _localization.is_, - IsoCode.IT: _localization.it_, - IsoCode.JE: _localization.je_, - IsoCode.JM: _localization.jm_, - IsoCode.JO: _localization.jo_, - IsoCode.JP: _localization.jp_, - IsoCode.KE: _localization.ke_, - IsoCode.KG: _localization.kg_, - IsoCode.KH: _localization.kh_, - IsoCode.KI: _localization.ki_, - IsoCode.KM: _localization.km_, - IsoCode.KN: _localization.kn_, - IsoCode.KP: _localization.kp_, - IsoCode.KR: _localization.kr_, - IsoCode.KW: _localization.kw_, - IsoCode.KY: _localization.ky_, - IsoCode.KZ: _localization.kz_, - IsoCode.LA: _localization.la_, - IsoCode.LB: _localization.lb_, - IsoCode.LC: _localization.lc_, - IsoCode.LI: _localization.li_, - IsoCode.LK: _localization.lk_, - IsoCode.LR: _localization.lr_, - IsoCode.LS: _localization.ls_, - IsoCode.LT: _localization.lt_, - IsoCode.LU: _localization.lu_, - IsoCode.LV: _localization.lv_, - IsoCode.LY: _localization.ly_, - IsoCode.MA: _localization.ma_, - IsoCode.MC: _localization.mc_, - IsoCode.MD: _localization.md_, - IsoCode.ME: _localization.me_, - IsoCode.MF: _localization.mf_, - IsoCode.MG: _localization.mg_, - IsoCode.MH: _localization.mh_, - IsoCode.MK: _localization.mk_, - IsoCode.ML: _localization.ml_, - IsoCode.MM: _localization.mm_, - IsoCode.MN: _localization.mn_, - IsoCode.MO: _localization.mo_, - IsoCode.MP: _localization.mp_, - IsoCode.MQ: _localization.mq_, - IsoCode.MR: _localization.mr_, - IsoCode.MS: _localization.ms_, - IsoCode.MT: _localization.mt_, - IsoCode.MU: _localization.mu_, - IsoCode.MV: _localization.mv_, - IsoCode.MW: _localization.mw_, - IsoCode.MX: _localization.mx_, - IsoCode.MY: _localization.my_, - IsoCode.MZ: _localization.mz_, - IsoCode.NA: _localization.na_, - IsoCode.NC: _localization.nc_, - IsoCode.NE: _localization.ne_, - IsoCode.NF: _localization.nf_, - IsoCode.NG: _localization.ng_, - IsoCode.NI: _localization.ni_, - IsoCode.NL: _localization.nl_, - IsoCode.NO: _localization.no_, - IsoCode.NP: _localization.np_, - IsoCode.NR: _localization.nr_, - IsoCode.NU: _localization.nu_, - IsoCode.NZ: _localization.nz_, - IsoCode.OM: _localization.om_, - IsoCode.PA: _localization.pa_, - IsoCode.PE: _localization.pe_, - IsoCode.PF: _localization.pf_, - IsoCode.PG: _localization.pg_, - IsoCode.PH: _localization.ph_, - IsoCode.PK: _localization.pk_, - IsoCode.PL: _localization.pl_, - IsoCode.PM: _localization.pm_, - IsoCode.PR: _localization.pr_, - IsoCode.PS: _localization.ps_, - IsoCode.PT: _localization.pt_, - IsoCode.PW: _localization.pw_, - IsoCode.PY: _localization.py_, - IsoCode.QA: _localization.qa_, - IsoCode.RE: _localization.re_, - IsoCode.RO: _localization.ro_, - IsoCode.RS: _localization.rs_, - IsoCode.RU: _localization.ru_, - IsoCode.RW: _localization.rw_, - IsoCode.SA: _localization.sa_, - IsoCode.SB: _localization.sb_, - IsoCode.SC: _localization.sc_, - IsoCode.SD: _localization.sd_, - IsoCode.SE: _localization.se_, - IsoCode.SG: _localization.sg_, - IsoCode.SI: _localization.si_, - IsoCode.SK: _localization.sk_, - IsoCode.SL: _localization.sl_, - IsoCode.SM: _localization.sm_, - IsoCode.SN: _localization.sn_, - IsoCode.SO: _localization.so_, - IsoCode.SR: _localization.sr_, - IsoCode.SS: _localization.ss_, - IsoCode.ST: _localization.st_, - IsoCode.SV: _localization.sv_, - IsoCode.SY: _localization.sy_, - IsoCode.SZ: _localization.sz_, - IsoCode.TA: _localization.ta_, - IsoCode.TC: _localization.tc_, - IsoCode.TD: _localization.td_, - IsoCode.TG: _localization.tg_, - IsoCode.TH: _localization.th_, - IsoCode.TJ: _localization.tj_, - IsoCode.TK: _localization.tk_, - IsoCode.TL: _localization.tl_, - IsoCode.TM: _localization.tm_, - IsoCode.TN: _localization.tn_, - IsoCode.TO: _localization.to_, - IsoCode.TR: _localization.tr_, - IsoCode.TT: _localization.tt_, - IsoCode.TV: _localization.tv_, - IsoCode.TW: _localization.tw_, - IsoCode.TZ: _localization.tz_, - IsoCode.UA: _localization.ua_, - IsoCode.UG: _localization.ug_, - IsoCode.US: _localization.us_, - IsoCode.UY: _localization.uy_, - IsoCode.UZ: _localization.uz_, - IsoCode.VA: _localization.va_, - IsoCode.VC: _localization.vc_, - IsoCode.VE: _localization.ve_, - IsoCode.VG: _localization.vg_, - IsoCode.VI: _localization.vi_, - IsoCode.VN: _localization.vn_, - IsoCode.VU: _localization.vu_, - IsoCode.WF: _localization.wf_, - IsoCode.WS: _localization.ws_, - IsoCode.YE: _localization.ye_, - IsoCode.YT: _localization.yt_, - IsoCode.ZA: _localization.za_, - IsoCode.ZM: _localization.zm_, - IsoCode.ZW: _localization.zw_ - }; -} diff --git a/lib/src/widgets/country_selector/search_box.dart b/lib/src/widgets/country_selector/search_box.dart deleted file mode 100644 index d0b1bc08..00000000 --- a/lib/src/widgets/country_selector/search_box.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter/material.dart'; - -class SearchBox extends StatefulWidget { - final Function(String) onChanged; - final Function() onSubmitted; - final bool autofocus; - final InputDecoration? decoration; - final TextStyle? style; - final Color? searchIconColor; - - const SearchBox({ - super.key, - required this.onChanged, - required this.onSubmitted, - required this.autofocus, - this.decoration, - this.style, - this.searchIconColor, - }); - - @override - State createState() => _SearchBoxState(); -} - -class _SearchBoxState extends State { - String _previousValue = ''; - - @override - void initState() { - super.initState(); - } - - void handleChange(e) { - widget.onChanged(e); - - // detect length difference - final diff = e.length - _previousValue.length; - if (diff > 3) { - // more than 3 characters added, probably a paste / autofill of country name - widget.onSubmitted(); - } - - setState(() { - _previousValue = e; - }); - } - - @override - State createState() => _SearchBoxState(); -} - -class _SearchBoxState extends State { - String _previousValue = ''; - - @override - void initState() { - super.initState(); - } - - void handleChange(e) { - widget.onChanged(e); - - // detect length difference - final diff = e.length - _previousValue.length; - if (diff > 3) { - // more than 3 characters added, probably a paste / autofill of country name - widget.onSubmitted(); - } - - setState(() { - _previousValue = e; - }); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: TextField( - autofocus: widget.autofocus, - onChanged: handleChange, - onSubmitted: (_) => widget.onSubmitted(), - cursorColor: widget.style?.color, - style: widget.style, - autofillHints: const [AutofillHints.countryName], - decoration: widget.decoration ?? - InputDecoration( - prefixIcon: Icon( - Icons.search, - color: widget.searchIconColor ?? - (Theme.of(context).brightness == Brightness.dark - ? Colors.white54 - : Colors.black38), - ), - filled: true, - isDense: true, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(20), - ), - ), - ), - ); - } -} diff --git a/lib/src/widgets/phone_field.dart b/lib/src/widgets/phone_field.dart deleted file mode 100644 index 93388c9b..00000000 --- a/lib/src/widgets/phone_field.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; - -import 'package:circle_flags/circle_flags.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:phone_form_field/src/constants/patterns.dart'; -import 'package:phone_form_field/src/controllers/phone_field_controller.dart'; - -import '../../phone_form_field.dart'; - -part 'phone_field_state.dart'; - -/// Phone field -/// -/// This deals with mostly UI and has no dependency on any phone parser library -class PhoneField extends StatefulWidget { - final PhoneFieldController controller; - final bool showFlagInInput; - final bool showIsoCodeInInput; - final bool showDialCode; - final String? errorText; - final double flagSize; - final InputDecoration decoration; - final bool isCountrySelectionEnabled; - final bool isCountryChipPersistent; - - /// configures the way the country picker selector is shown - final CountrySelectorNavigator selectorNavigator; - - // textfield inputs - final TextInputType keyboardType; - final TextInputAction? textInputAction; - final TextStyle? style; - final TextStyle? countryCodeStyle; - final StrutStyle? strutStyle; - final TextAlign textAlign; - final TextAlignVertical? textAlignVertical; - final bool autofocus; - final String obscuringCharacter; - final bool obscureText; - final bool autocorrect; - final SmartDashesType? smartDashesType; - final SmartQuotesType? smartQuotesType; - final bool enableSuggestions; - final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder; - final bool? showCursor; - final VoidCallback? onEditingComplete; - final ValueChanged? onSubmitted; - final AppPrivateCommandCallback? onAppPrivateCommand; - final Function(PointerDownEvent)? onTapOutside; - final bool enabled; - final double cursorWidth; - final double? cursorHeight; - final Radius? cursorRadius; - final Color? cursorColor; - final ui.BoxHeightStyle selectionHeightStyle; - final ui.BoxWidthStyle selectionWidthStyle; - final Brightness? keyboardAppearance; - final EdgeInsets scrollPadding; - final bool enableInteractiveSelection; - final TextSelectionControls? selectionControls; - bool get selectionEnabled => enableInteractiveSelection; - final MouseCursor? mouseCursor; - final ScrollPhysics? scrollPhysics; - final ScrollController? scrollController; - final Iterable? autofillHints; - final String? restorationId; - final bool enableIMEPersonalizedLearning; - final List? inputFormatters; - - const PhoneField({ - // form field params - super.key, - required this.controller, - required this.showFlagInInput, - required this.selectorNavigator, - required this.flagSize, - required this.errorText, - required this.decoration, - required this.isCountrySelectionEnabled, - required this.isCountryChipPersistent, - // textfield inputs - required this.keyboardType, - required this.textInputAction, - required this.style, - required this.countryCodeStyle, - required this.strutStyle, - required this.textAlign, - required this.textAlignVertical, - required this.autofocus, - required this.obscuringCharacter, - required this.obscureText, - required this.autocorrect, - required this.smartDashesType, - required this.smartQuotesType, - required this.enableSuggestions, - required this.contextMenuBuilder, - required this.showCursor, - required this.onEditingComplete, - required this.onSubmitted, - required this.onAppPrivateCommand, - required this.enabled, - required this.cursorWidth, - required this.cursorHeight, - required this.cursorRadius, - required this.cursorColor, - required this.selectionHeightStyle, - required this.selectionWidthStyle, - required this.keyboardAppearance, - required this.scrollPadding, - required this.enableInteractiveSelection, - required this.selectionControls, - required this.mouseCursor, - required this.scrollPhysics, - required this.scrollController, - required this.autofillHints, - required this.restorationId, - required this.enableIMEPersonalizedLearning, - required this.inputFormatters, - required this.showDialCode, - required this.showIsoCodeInInput, - required this.onTapOutside, - }); - - @override - PhoneFieldState createState() => PhoneFieldState(); -} diff --git a/lib/src/widgets/phone_field_state.dart b/lib/src/widgets/phone_field_state.dart deleted file mode 100644 index d2a6d6ae..00000000 --- a/lib/src/widgets/phone_field_state.dart +++ /dev/null @@ -1,196 +0,0 @@ -part of 'phone_field.dart'; - -class PhoneFieldState extends State { - PhoneFieldController get controller => widget.controller; - final _flagCache = FlagCache(); - PhoneFieldState(); - - @override - void initState() { - controller.focusNode.addListener(onFocusChange); - _preloadFlagsInMemory(); - super.initState(); - } - - void _preloadFlagsInMemory() { - _flagCache.preload(IsoCode.values.map((isoCode) => isoCode.name)); - } - - void onFocusChange() { - setState(() {}); - } - - @override - void dispose() { - controller.focusNode.removeListener(onFocusChange); - super.dispose(); - } - - void selectCountry() async { - if (!widget.isCountrySelectionEnabled) { - return; - } - SystemChannels.textInput.invokeMethod('TextInput.hide'); - final selected = - await widget.selectorNavigator.navigate(context, _flagCache); - if (selected != null) { - controller.isoCode = selected.isoCode; - } - controller.focusNode.requestFocus(); - SystemChannels.textInput.invokeMethod('TextInput.show'); - } - - // TODO: Would be cleaner if we could infer it from - // TextField._defaultContextMenuBuilder, but it's private - static Widget _defaultContextMenuBuilder( - BuildContext context, EditableTextState editableTextState) { - return AdaptiveTextSelectionToolbar.editableText( - editableTextState: editableTextState, - ); - } - - @override - Widget build(BuildContext context) { - // the idea here is to have a mouse region that surround the input which - // contains a flag button and a text field. The text field is surrounded - // by padding so we want to request focus even when clicking outside of the - // inner field. - // When the country chip is not shown it request focus to the inner text - // field which doesn't span the whole input - // When the country chip is shown, clicking on it request country selection - return MouseRegion( - cursor: SystemMouseCursors.text, - child: GestureDetector( - onTap: controller.focusNode.requestFocus, - // absorb pointer when the country chip is not shown, else flutter - // still allows the country chip to be clicked even though it is not shown - child: InputDecorator( - decoration: _getOutterInputDecoration(), - isFocused: controller.focusNode.hasFocus, - isEmpty: _isEffectivelyEmpty(), - child: TextField( - focusNode: controller.focusNode, - controller: controller.nationalNumberController, - enabled: widget.enabled, - decoration: _getInnerInputDecoration(), - inputFormatters: widget.inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp( - '[${Patterns.plus}${Patterns.digits}${Patterns.punctuation}]')), - ], - autofillHints: widget.autofillHints, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - contextMenuBuilder: - widget.contextMenuBuilder ?? _defaultContextMenuBuilder, - showCursor: widget.showCursor, - onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, - onAppPrivateCommand: widget.onAppPrivateCommand, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - onTapOutside: widget.onTapOutside, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - mouseCursor: widget.mouseCursor, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - restorationId: widget.restorationId, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - ), - ), - ), - ); - } - - Widget _getCountryCodeChip() { - return Directionality( - textDirection: TextDirection.ltr, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: widget.enabled ? selectCountry : null, - // material here else the click pass through empty spaces - child: Material( - color: Colors.transparent, - child: Padding( - padding: !widget.showDialCode && !widget.showFlagInInput - ? EdgeInsets.zero - : const EdgeInsetsDirectional.fromSTEB(8, 0, 8, 0), - child: CountryCodeChip( - key: const ValueKey('country-code-chip'), - isoCode: controller.isoCode, - showFlag: widget.showFlagInInput, - showIsoCode: widget.showIsoCodeInInput, - showDialCode: widget.showDialCode, - textStyle: widget.countryCodeStyle ?? - widget.decoration.labelStyle ?? - TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.bodySmall?.color, - ), - flagSize: widget.flagSize, - enabled: widget.enabled, - ), - ), - ), - ), - ), - ); - } - - InputDecoration _getInnerInputDecoration() { - return InputDecoration.collapsed( - hintText: widget.decoration.hintText, - hintStyle: widget.decoration.hintStyle, - ).copyWith( - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - enabledBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ); - } - - InputDecoration _getOutterInputDecoration() { - final directionality = Directionality.of(context); - - return widget.decoration.copyWith( - hintText: null, - errorText: widget.errorText, - prefix: - directionality == TextDirection.ltr ? _getCountryCodeChip() : null, - suffix: - directionality == TextDirection.rtl ? _getCountryCodeChip() : null, - ); - } - - bool _isEffectivelyEmpty() { - if (widget.isCountryChipPersistent) return false; - final outterDecoration = _getOutterInputDecoration(); - // when there is not label and an hint text we need to have - // isEmpty false so the country code is displayed along the - // hint text to not have the hint text in the middle - if (outterDecoration.label == null && outterDecoration.hintText != null) { - return false; - } - return controller.nationalNumberController.text.isEmpty; - } -} diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart deleted file mode 100644 index 254ca9ea..00000000 --- a/lib/src/widgets/phone_form_field.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:phone_numbers_parser/phone_numbers_parser.dart'; - -import '../constants/patterns.dart'; -import '../controllers/phone_controller.dart'; -import '../controllers/phone_field_controller.dart'; -import '../validation/phone_validator.dart'; -import '../validation/validator_translator.dart'; -import 'country_selector/country_selector_navigator.dart'; -import 'phone_field.dart'; - -part 'phone_form_field_state.dart'; - -/// Phone input extending form field. -/// -/// ### controller: -/// {@template controller} -/// Use a [PhoneController] for PhoneFormField when you need to dynamically -/// change the value. -/// -/// Whenever the user modifies the phone field with an -/// associated [controller], the phone field updates -/// the display value and the controller notifies its listeners. -/// {@endtemplate} -/// -/// You can also use an [initialValue]: -/// {@template initialValue} -/// The initial value used. -/// -/// Only one of [initialValue] and [controller] can be specified. -/// If [controller] is specified the [initialValue] will be -/// the first value of the [PhoneController] -/// {@endtemplate} -/// -/// ### formatting: -/// {@template shouldFormat} -/// Specify whether the field will format the national number with [shouldFormat] = true (default) -/// eg: +33677784455 will be displayed as +33 6 77 78 44 55. -/// -/// The formats are localized for the country code. -/// eg: +1 677-784-455 & +33 6 77 78 44 55 -/// -/// -/// This does not affect the output value, only the display. -/// Therefor [onSizeFound] will still return a [PhoneNumber] -/// with nsn of 677784455. -/// {@endtemplate} -/// -/// ### phoneNumberType: -/// {@template phoneNumberType} -/// specify the type of phone number with [phoneNumberType]. -/// -/// accepted values are: -/// - null (can be mobile or fixedLine) -/// - mobile -/// - fixedLine -/// {@endtemplate} -/// -/// -/// ### Country picker: -/// -/// {@template selectorNavigator} -/// specify which type of country selector will be shown with [selectorNavigator]. -/// -/// Uses one of: -/// - const BottomSheetNavigator() -/// - const DraggableModalBottomSheetNavigator() -/// - const ModalBottomSheetNavigator() -/// - const DialogNavigator() -/// {@endtemplate} -/// -/// ### Country Code visibility: -/// -/// The country dial code will be visible when: -/// - the field is focussed. -/// - the field has a value for national number. -/// - the field has no label obstructing the view. -class PhoneFormField extends FormField { - /// {@macro controller} - final PhoneController? controller; - - /// {@macro shouldFormat} - final bool shouldFormat; - - /// callback called when the input value changes - final ValueChanged? onChanged; - - /// country that is displayed when there is no value - final IsoCode defaultCountry; - - /// the focusNode of the national number - final FocusNode? focusNode; - - /// show Dial Code or not - final bool showDialCode; - - /// show selected iso code or not - final bool showIsoCodeInInput; - - PhoneFormField({ - super.key, - this.controller, - this.shouldFormat = true, - this.onChanged, - this.focusNode, - bool showFlagInInput = true, - CountrySelectorNavigator countrySelectorNavigator = - const CountrySelectorNavigator.searchDelegate(), - Function(PhoneNumber?)? super.onSaved, - this.defaultCountry = IsoCode.US, - InputDecoration decoration = - const InputDecoration(border: UnderlineInputBorder()), - AutovalidateMode super.autovalidateMode = AutovalidateMode.onUserInteraction, - PhoneNumber? initialValue, - double flagSize = 16, - PhoneNumberInputValidator? validator, - bool isCountrySelectionEnabled = true, - bool isCountryChipPersistent = true, - // textfield inputs - TextInputType keyboardType = TextInputType.phone, - TextInputAction? textInputAction, - TextStyle? style, - TextStyle? countryCodeStyle, - StrutStyle? strutStyle, - TextAlign textAlign = TextAlign.start, - TextAlignVertical? textAlignVertical, - bool autofocus = false, - String obscuringCharacter = '*', - bool obscureText = false, - bool autocorrect = true, - SmartDashesType? smartDashesType, - SmartQuotesType? smartQuotesType, - bool enableSuggestions = true, - Widget Function(BuildContext, EditableTextState)? contextMenuBuilder, - bool? showCursor, - VoidCallback? onEditingComplete, - ValueChanged? onSubmitted, - AppPrivateCommandCallback? onAppPrivateCommand, - Function(PointerDownEvent)? onTapOutside, - List? inputFormatters, - super.enabled, - double cursorWidth = 2.0, - double? cursorHeight, - Radius? cursorRadius, - Color? cursorColor, - ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, - ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, - Brightness? keyboardAppearance, - EdgeInsets scrollPadding = const EdgeInsets.all(20.0), - bool enableInteractiveSelection = true, - TextSelectionControls? selectionControls, - MouseCursor? mouseCursor, - ScrollPhysics? scrollPhysics, - ScrollController? scrollController, - Iterable? autofillHints, - super.restorationId, - bool enableIMEPersonalizedLearning = true, - this.showDialCode = true, - this.showIsoCodeInInput = false, - }) : assert( - initialValue == null || controller == null, - 'One of initialValue or controller can be specified at a time', - ), - super( - initialValue: controller != null ? controller.value : initialValue, - validator: validator ?? PhoneValidator.valid(), - builder: (state) { - final field = state as PhoneFormFieldState; - return PhoneField( - controller: field._childController, - showFlagInInput: showFlagInInput, - showIsoCodeInInput: showIsoCodeInInput, - selectorNavigator: countrySelectorNavigator, - errorText: field.getErrorText(), - showDialCode: showDialCode, - flagSize: flagSize, - decoration: decoration, - enabled: enabled, - isCountrySelectionEnabled: isCountrySelectionEnabled, - isCountryChipPersistent: isCountryChipPersistent, - // textfield params - autofillHints: autofillHints, - keyboardType: keyboardType, - textInputAction: textInputAction, - style: style, - countryCodeStyle: countryCodeStyle, - strutStyle: strutStyle, - textAlign: textAlign, - textAlignVertical: textAlignVertical, - autofocus: autofocus, - obscuringCharacter: obscuringCharacter, - obscureText: obscureText, - autocorrect: autocorrect, - smartDashesType: smartDashesType, - smartQuotesType: smartQuotesType, - enableSuggestions: enableSuggestions, - contextMenuBuilder: contextMenuBuilder, - showCursor: showCursor, - onEditingComplete: onEditingComplete, - onSubmitted: onSubmitted, - onAppPrivateCommand: onAppPrivateCommand, - onTapOutside: onTapOutside, - cursorWidth: cursorWidth, - cursorHeight: cursorHeight, - cursorRadius: cursorRadius, - cursorColor: cursorColor, - selectionHeightStyle: selectionHeightStyle, - selectionWidthStyle: selectionWidthStyle, - keyboardAppearance: keyboardAppearance, - scrollPadding: scrollPadding, - enableInteractiveSelection: enableInteractiveSelection, - selectionControls: selectionControls, - mouseCursor: mouseCursor, - scrollController: scrollController, - scrollPhysics: scrollPhysics, - restorationId: restorationId, - enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, - inputFormatters: inputFormatters, - ); - }, - ); - - @override - PhoneFormFieldState createState() => PhoneFormFieldState(); -} diff --git a/lib/src/widgets/phone_form_field_state.dart b/lib/src/widgets/phone_form_field_state.dart deleted file mode 100644 index 7fada3e1..00000000 --- a/lib/src/widgets/phone_form_field_state.dart +++ /dev/null @@ -1,114 +0,0 @@ -part of 'phone_form_field.dart'; - -class PhoneFormFieldState extends FormFieldState { - late final PhoneController _controller; - late final PhoneFieldController _childController; - late final StreamSubscription _selectionSubscription; - - @override - PhoneFormField get widget => super.widget as PhoneFormField; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? PhoneController(value); - _childController = PhoneFieldController( - isoCode: _controller.value?.isoCode ?? widget.defaultCountry, - national: _getFormattedNsn(), - focusNode: widget.focusNode ?? FocusNode(), - ); - _controller.addListener(_onControllerChange); - _childController.addListener(() => _onChildControllerChange()); - // to expose text selection of national number - _selectionSubscription = _controller.selectionRequestStream - .listen((event) => _childController.selectNationalNumber()); - } - - @override - void dispose() { - super.dispose(); - _childController.dispose(); - _selectionSubscription.cancel(); - _controller.removeListener(_onControllerChange); - // dispose the controller only when it's initialised in this instance - // otherwise this should be done where instance is created - if (widget.controller == null) { - _controller.dispose(); - } - } - - @override - void reset() { - _controller.value = widget.initialValue; - super.reset(); - } - - /// when the controller changes this function will - /// update the childController so the [PhoneField] which - /// deals with the UI can display the correct value. - void _onControllerChange() { - final phone = _controller.value; - - widget.onChanged?.call(phone); - didChange(phone); - final formatted = _getFormattedNsn(); - if (_childController.national != formatted) { - _childController.national = formatted; - } - if (_childController.isoCode != phone?.isoCode) { - _childController.isoCode = phone?.isoCode ?? widget.defaultCountry; - } - } - - /// when the base controller changes (when the user manually input something) - /// then we need to update the local controller's value. - void _onChildControllerChange() { - if (_childController.national == _controller.value?.nsn && - _childController.isoCode == _controller.value?.isoCode) { - return; - } - - if (_childController.national == null) { - return _controller.value = null; - } - // we convert the multiple controllers from the child controller - // to a full blown PhoneNumber to access validation, formatting etc. - PhoneNumber phoneNumber; - // when the nsn input change we check if its not a whole number - // to allow for copy pasting and auto fill. If it is one then - // we parse it accordingly. - // we assume it's a whole phone number if it starts with + - final childNsn = _childController.national; - if (childNsn != null && childNsn.startsWith(RegExp('[${Patterns.plus}]'))) { - // if starts with + then we parse the whole number - // to figure out the country code - final international = childNsn; - try { - phoneNumber = PhoneNumber.parse(international); - } on PhoneNumberException { - return; - } - } else { - phoneNumber = PhoneNumber.parse( - childNsn ?? '', - destinationCountry: _childController.isoCode, - ); - } - _controller.value = phoneNumber; - } - - String? _getFormattedNsn() { - if (widget.shouldFormat) { - return _controller.value?.getFormattedNsn(); - } - return _controller.value?.nsn; - } - - /// gets the localized error text if any - String? getErrorText() { - if (errorText != null) { - return ValidatorTranslator.message(context, errorText!); - } - return null; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 7b8cd3e8..07c99166 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,15 +10,18 @@ environment: dependencies: flutter: sdk: flutter - flutter_localizations: # Add this line + flutter_localizations: sdk: flutter + intl: ">=0.18.1 <=1.0.0" - circle_flags: ^3.0.1 - phone_numbers_parser: ^8.1.0 - intl: ">=0.17.0 <=1.0.0" - diacritic: ^0.1.3 + circle_flags: ^4.0.0 + phone_numbers_parser: ^8.1.3 + diacritic: ^0.1.5 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 + +flutter: + generate: true diff --git a/test/_country_selector_navigator_test.dart b/test/_country_selector_navigator_test.dart index c1462e75..a2df3824 100644 --- a/test/_country_selector_navigator_test.dart +++ b/test/_country_selector_navigator_test.dart @@ -1,4 +1,3 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; @@ -18,7 +17,7 @@ void main() { testWidgets('should navigate to dialog', (tester) async { const nav = CountrySelectorNavigator.dialog(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(CountrySelector), findsOneWidget); @@ -26,7 +25,7 @@ void main() { testWidgets('should navigate to modal bottom sheet', (tester) async { const nav = CountrySelectorNavigator.modalBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -34,7 +33,7 @@ void main() { testWidgets('should navigate to bottom sheet', (tester) async { const nav = CountrySelectorNavigator.bottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); @@ -42,7 +41,7 @@ void main() { testWidgets('should navigate to draggable sheet', (tester) async { const nav = CountrySelectorNavigator.draggableBottomSheet(); - await tester.pumpWidget(getApp((ctx) => nav.navigate(ctx, FlagCache()))); + await tester.pumpWidget(getApp((ctx) => nav.show(ctx))); await tester.tap(find.byType(ElevatedButton)); await tester.pump(const Duration(seconds: 1)); expect(find.byType(CountrySelector), findsOneWidget); diff --git a/test/_country_selector_test.dart b/test/_country_selector_test.dart index 5d97bd7a..76b729f2 100644 --- a/test/_country_selector_test.dart +++ b/test/_country_selector_test.dart @@ -1,50 +1,15 @@ -import 'package:circle_flags/circle_flags.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/search_box.dart'; +import 'package:phone_form_field/src/country_selection/no_result_view.dart'; +import 'package:phone_form_field/src/country_selection/search_box.dart'; void main() { group('CountrySelector', () { - group('Without internationalization', () { - final app = MaterialApp( - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - flagCache: FlagCache(), - ), - ), - ); - - testWidgets('Should filter with text', (tester) async { - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'sp'); - await tester.pumpAndSettle(); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('ES'))); - // not the right language (we let english go through tho) - await tester.enterText(txtFound, 'Espagne'); - await tester.pumpAndSettle(); - expect(tiles, findsNothing); - await tester.pumpAndSettle(); - // country codes - await tester.enterText(txtFound, '33'); - await tester.pumpAndSettle(); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('FR'))); - }); - }); - - group('With internationalization', () { - final app = MaterialApp( - locale: const Locale('es', ''), + Widget buildSelector({List favorites = const []}) { + return MaterialApp( + locale: const Locale('en', ''), localizationsDelegates: const [ ...GlobalMaterialLocalizations.delegates, PhoneFieldLocalization.delegate, @@ -53,199 +18,100 @@ void main() { home: Scaffold( body: CountrySelector( onCountrySelected: (c) {}, - flagCache: FlagCache(), + favoriteCountries: favorites, ), ), ); - - testWidgets('Should filter with text', (tester) async { - await tester.pumpWidget(app); - await tester.pump(const Duration(seconds: 1)); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'esp'); - await tester.pump(const Duration(seconds: 1)); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('ES'))); - // not the right language (we let english go through tho) - await tester.enterText(txtFound, 'Espagne'); - await tester.pump(const Duration(seconds: 1)); - expect(tiles, findsNothing); - await tester.pump(const Duration(seconds: 1)); - // country codes - await tester.enterText(txtFound, '33'); - await tester.pump(const Duration(seconds: 1)); - expect(tiles, findsWidgets); - expect( - tester.widget(tiles.first).key, equals(const Key('FR'))); - }); + } + + testWidgets('Should filter with text', (tester) async { + await tester.pumpWidget(buildSelector()); + await tester.pump(const Duration(seconds: 1)); + final txtFound = find.byType(SearchBox); + expect(txtFound, findsOneWidget); + await tester.enterText(txtFound, 'esp'); + await tester.pump(const Duration(seconds: 1)); + final tiles = find.byType(ListTile); + expect(tiles, findsWidgets); + expect(tester.widget(tiles.first).key, equals(const Key('ES'))); + // not the right language (we let english go through tho) + await tester.enterText(txtFound, 'Espagne'); + await tester.pump(const Duration(seconds: 1)); + expect(tiles, findsNothing); + await tester.pump(const Duration(seconds: 1)); + // country codes + await tester.enterText(txtFound, '33'); + await tester.pump(const Duration(seconds: 1)); + expect(tiles, findsWidgets); + expect(tester.widget(tiles.first).key, equals(const Key('FR'))); }); - group('sorted countries with or without favorites', () { - Widget builder({ - List? favorites, - bool addFavoritesSeparator = false, - }) => - MaterialApp( - locale: const Locale('fr'), - localizationsDelegates: const [ - PhoneFieldLocalization.delegate, - ...GlobalMaterialLocalizations.delegates, - ], - supportedLocales: const [Locale('fr')], - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - addFavoritesSeparator: addFavoritesSeparator, - favoriteCountries: favorites ?? const [], - flagCache: FlagCache(), - ), - ), - ); - - testWidgets('should be properly sorted without favorites', - (tester) async { - await tester.pumpWidget(builder()); - await tester.pump(const Duration(seconds: 1)); - final allTiles = find.byType(ListTile); - expect(allTiles, findsWidgets); - // expect(tester.widget(allTiles.first).key, equals(Key('AF'))); - }); - - testWidgets('should be properly sorted with favorites', (tester) async { - await tester.pumpWidget(builder(favorites: [IsoCode.GU, IsoCode.GY])); - await tester.pump(const Duration(seconds: 1)); - final allTiles = find.byType(ListTile, skipOffstage: false); - expect(allTiles, findsWidgets); - expect(tester.widget(allTiles.at(0)).key, - equals(Key(IsoCode.GU.name))); - expect(tester.widget(allTiles.at(1)).key, - equals(Key(IsoCode.GY.name))); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'guy'); - await tester.pumpAndSettle(); - final filteredTiles = find.byType(ListTile); - expect(filteredTiles, findsWidgets); - expect(filteredTiles.evaluate().length, equals(2)); - }); - - testWidgets('should display/hide separator', (tester) async { - await tester.pumpWidget(builder( - favorites: [IsoCode.GU, IsoCode.GY], - addFavoritesSeparator: true, - )); - await tester.pump(const Duration(seconds: 1)); - final list = find.byType(ListView); - expect(list, findsOneWidget); - final allTiles = find.descendant( - of: list, - matching: find.byWidgetPredicate( - (Widget widget) => widget is ListTile || widget is Divider, - ), - ); + testWidgets('should show a divider between favorites and all countries', + (tester) async { + await tester.pumpWidget(buildSelector(favorites: const [IsoCode.BE])); + await tester.pump(const Duration(seconds: 1)); + final list = find.byType(ListView); + expect(list, findsOneWidget); + final allTiles = find.descendant( + of: list, + matching: find.byWidgetPredicate( + (Widget widget) => widget is ListTile || widget is Divider, + ), + ); - expect(allTiles, findsWidgets); - expect( - tester.widget(allTiles.at(2)), - isA(), - reason: 'separator should be visible after the favorites countries', - ); + expect(allTiles, findsWidgets); + expect( + tester.widget(allTiles.at(1)), + isA(), + reason: 'separator should be visible after the favorites countries', + ); + }); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'guy'); - await tester.pump(const Duration(seconds: 1)); - final tiles = find.byType(ListTile); - expect(tiles, findsWidgets); - expect( - tiles.evaluate().length, - equals(2), - reason: 'Separator should be hidden as all elements' - 'found are in favorites', - ); - }); + testWidgets('should hide favorites when search has started', + (tester) async { + await tester.pumpWidget(buildSelector(favorites: const [IsoCode.BE])); + await tester.pump(const Duration(seconds: 1)); + final searchBox = find.byType(SearchBox); + expect(searchBox, findsOneWidget); + await tester.enterText(searchBox, 'belg'); + await tester.pump(const Duration(seconds: 1)); + final tiles = find.byType(ListTile); + expect(tiles, findsOneWidget); }); - group('Empty search result', () { - Widget builder({ - String? noResultMessage, - }) => - MaterialApp( - locale: const Locale('fr'), - localizationsDelegates: const [ - PhoneFieldLocalization.delegate, - ...GlobalMaterialLocalizations.delegates, - ], - supportedLocales: const [Locale('fr')], - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - noResultMessage: noResultMessage, - flagCache: FlagCache(), - ), - ), - ); + testWidgets('should sort countries', (tester) async { + await tester + .pumpWidget(buildSelector(favorites: const [IsoCode.SE, IsoCode.SG])); + await tester.pump(const Duration(seconds: 1)); + final allTiles = find.byType(ListTile, skipOffstage: false); + expect(allTiles, findsWidgets); + expect(tester.widget(allTiles.at(0)).key, + equals(Key(IsoCode.SG.name))); + expect(tester.widget(allTiles.at(1)).key, + equals(Key(IsoCode.SE.name))); + }); - testWidgets('should display default untranslated no result message', - (tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: CountrySelector( - onCountrySelected: (c) {}, - flagCache: FlagCache(), - ), + testWidgets('should display no result when there is no result', + (tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: CountrySelector( + onCountrySelected: (c) {}, ), - )); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); - - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); - - final noResultWidget = find.text('No result found'); - expect(noResultWidget, findsOneWidget); - }); - - testWidgets('should display default translated no result message', - (tester) async { - await tester.pumpWidget(builder()); - - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); - - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); - - final noResultWidget = find.text('Aucun résultat'); - expect(noResultWidget, findsOneWidget); - }); - - testWidgets('should display custom no result message', (tester) async { - await tester.pumpWidget(builder(noResultMessage: 'Bad news !')); + ), + )); - final txtFound = find.byType(SearchBox); - expect(txtFound, findsOneWidget); - await tester.enterText(txtFound, 'fake search with no result'); - await tester.pumpAndSettle(); + final searchBox = find.byType(SearchBox); + expect(searchBox, findsOneWidget); + await tester.enterText(searchBox, 'fake search with no result'); + await tester.pumpAndSettle(); - // no listitem should be displayed when no result found - final allTiles = find.byType(ListTile); - expect(allTiles, findsNothing); + // no listitem should be displayed when no result found + final allTiles = find.byType(ListTile); + expect(allTiles, findsNothing); - final noResultWidget = find.text('Bad news !'); - expect(noResultWidget, findsOneWidget); - }); + final noResultWidget = find.byType(NoResultView); + expect(noResultWidget, findsOneWidget); }); }); } diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index 22420631..90413428 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/widgets/country_selector/country_list.dart'; +import 'package:phone_form_field/src/country_selection/country_list_view.dart'; void main() { group('PhoneFormField', () { final formKey = GlobalKey(); final phoneKey = GlobalKey>(); + Widget getWidget({ Function(PhoneNumber?)? onChanged, Function(PhoneNumber?)? onSaved, @@ -17,9 +18,7 @@ void main() { PhoneController? controller, bool showFlagInInput = true, bool showDialCode = true, - IsoCode defaultCountry = IsoCode.US, - bool shouldFormat = false, - PhoneNumberInputValidator? validator, + PhoneNumberInputValidator Function(BuildContext)? validatorBuilder, bool enabled = true, }) => MaterialApp( @@ -29,86 +28,81 @@ void main() { ], supportedLocales: const [Locale('en')], home: Scaffold( - body: Form( - key: formKey, - child: PhoneFormField( - key: phoneKey, - initialValue: initialValue, - onChanged: onChanged, - onSaved: onSaved, - onTapOutside: onTapOutside, - showFlagInInput: showFlagInInput, - showDialCode: showDialCode, - controller: controller, - defaultCountry: defaultCountry, - shouldFormat: shouldFormat, - validator: validator, - enabled: enabled, - ), - ), + body: Builder(builder: (context) { + return Form( + key: formKey, + child: PhoneFormField( + key: phoneKey, + initialValue: initialValue, + onChanged: onChanged, + onSaved: onSaved, + onTapOutside: onTapOutside, + showFlagInInput: showFlagInInput, + showDialCode: showDialCode, + controller: controller, + validator: validatorBuilder?.call(context), + enabled: enabled, + autovalidateMode: AutovalidateMode.onUserInteraction, + ), + ); + }), ), ); - group('display', () { - testWidgets('Should display input', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(TextField), findsOneWidget); - }); + testWidgets('Should display input', (tester) async { + await tester.pumpWidget( + getWidget(initialValue: PhoneNumber.parse('+33')), + ); + expect(find.byType(PhoneFormField), findsOneWidget); + }); - testWidgets('Should display country code', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(CountryCodeChip), findsWidgets); - }); + testWidgets('Should display country code', (tester) async { + await tester.pumpWidget(getWidget()); + expect(find.byType(CountryButton), findsWidgets); + }); - testWidgets('Should display flag', (tester) async { - await tester.pumpWidget(getWidget()); - expect(find.byType(CircleFlag), findsWidgets); - }); + testWidgets('Should display flag', (tester) async { + await tester.pumpWidget(getWidget()); + expect(find.byType(CircleFlag), findsWidgets); + }); - testWidgets( - 'disabled, tap on country chip - country list dialog is not shown', - (tester) async { - await tester.pumpWidget(getWidget(enabled: false)); - final countryChip = - tester.widget(find.byType(CountryCodeChip)); - expect(countryChip.enabled, false); + testWidgets( + 'Should not show country selection when disabled and country chip is tapped', + (tester) async { + await tester.pumpWidget(getWidget(enabled: false)); + final countryChip = + tester.widget(find.byType(CountryButton)); + expect(countryChip.enabled, false); - await tester.tap(find.byType(CountryCodeChip)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(CountryButton), warnIfMissed: false); + await tester.pumpAndSettle(); - expect(find.byType(CountryList), findsNothing); - }); + expect(find.byType(CountryListView), findsNothing); }); group('Country code', () { testWidgets('Should open dialog when country code is clicked', (tester) async { await tester.pumpWidget(getWidget()); - expect(find.byType(CountryList), findsNothing); + expect(find.byType(CountryListView), findsNothing); await tester.tap(find.byType(PhoneFormField)); await tester.pump(const Duration(seconds: 1)); - await tester.tap(find.byType(CountryCodeChip)); + await tester.tap(find.byType(CountryButton)); await tester.pumpAndSettle(); - expect(find.byType(CountryList), findsOneWidget); + expect(find.byType(CountryListView), findsOneWidget); }); - testWidgets('Should have a default country', (tester) async { - await tester.pumpWidget(getWidget(defaultCountry: IsoCode.FR)); - expect(find.text('+ 33'), findsWidgets); - }); - testWidgets('Should hide flag', (tester) async { await tester.pumpWidget(getWidget(showFlagInInput: false)); expect(find.byType(CircleFlag), findsNothing); }); - testWidgets('Should format when shouldFormat is true', (tester) async { + testWidgets('Should format phone number', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse( '', destinationCountry: IsoCode.FR, ); - await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: true)); + await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '677777777'); @@ -118,219 +112,313 @@ void main() { testWidgets('Should show dial code when showDialCode is true', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + PhoneNumber phoneNumber = PhoneNumber.parse('+33'); - await tester.pumpWidget(getWidget( + await tester.pumpWidget( + getWidget( initialValue: phoneNumber, showDialCode: true, - defaultCountry: IsoCode.FR)); + ), + ); await tester.pump(const Duration(seconds: 1)); expect(find.text('+ 33'), findsOneWidget); }); testWidgets('Should hide dial code when showDialCode is false', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + PhoneNumber phoneNumber = PhoneNumber.parse('+33'); - await tester.pumpWidget(getWidget( + await tester.pumpWidget( + getWidget( initialValue: phoneNumber, showDialCode: false, - defaultCountry: IsoCode.FR)); + ), + ); await tester.pump(const Duration(seconds: 1)); expect(find.text('+ 33'), findsNothing); }); }); - group('value changes', () { - testWidgets('Should display initial value', (tester) async { - await tester.pumpWidget(getWidget( - initialValue: PhoneNumber.parse('478787827', - destinationCountry: IsoCode.FR))); - expect(find.text('+ 33'), findsWidgets); - expect(find.text('478787827'), findsOneWidget); - }); + testWidgets('Should display initial value', (tester) async { + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33478787827'), + ), + ); + await tester.pumpAndSettle(); + expect(find.text('+ 33'), findsWidgets); + expect(find.text('4 78 78 78 27'), findsOneWidget); + }); - testWidgets('Should change value of controller', (tester) async { - final controller = PhoneController(null); - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); - final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, '123456789'); - expect( - newValue, - equals( - PhoneNumber.parse( - '123456789', - destinationCountry: IsoCode.US, - ), - ), - ); + testWidgets('Should change value of controller', (tester) async { + final controller = PhoneController( + initialValue: PhoneNumber.parse('+1'), + ); + PhoneNumber? newValue; + controller.addListener(() { + newValue = controller.value; }); + await tester.pumpWidget(getWidget(controller: controller)); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, '123456789'); + expect( + newValue, + equals(PhoneNumber.parse('+1 123456789')), + ); + }); - testWidgets('Should change value of input when controller changes', - (tester) async { - final controller = PhoneController(null); - // ignore: unused_local_variable - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); - controller.value = - PhoneNumber.parse('488997722', destinationCountry: IsoCode.FR); - await tester.pump(const Duration(seconds: 1)); - expect(find.text('+ 33'), findsWidgets); - expect(find.text('488997722'), findsOneWidget); - }); - testWidgets( - 'Should change value of country code chip when full number copy pasted', - (tester) async { - final controller = PhoneController(null); - // ignore: unused_local_variable - PhoneNumber? newValue; - controller.addListener(() { - newValue = controller.value; - }); - await tester.pumpWidget( - getWidget(controller: controller, defaultCountry: IsoCode.US)); - final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, '+33 0488 99 77 22'); - await tester.pump(); - expect(controller.value?.isoCode, equals(IsoCode.FR)); - expect(controller.value?.nsn, equals('488997722')); + testWidgets('Should change value of input when controller changes', + (tester) async { + final controller = PhoneController(); + await tester.pumpWidget(getWidget(controller: controller)); + controller.value = PhoneNumber.parse('+33488997722'); + + await tester.pumpAndSettle(); + + expect(find.text('+ 33'), findsWidgets); + expect(find.text(controller.value.formatNsn()), findsOneWidget); + }); + + testWidgets( + 'Should change value of country code chip when full number copy pasted', + (tester) async { + final controller = PhoneController(); + // ignore: unused_local_variable + PhoneNumber? newValue; + controller.addListener(() { + newValue = controller.value; }); + await tester.pumpWidget(getWidget(controller: controller)); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, '+33 0488 99 77 22'); + await tester.pump(); + expect(controller.value.isoCode, equals(IsoCode.FR)); + expect(controller.value.nsn, equals('488997722')); + }); - testWidgets('Should call onChange', (tester) async { - bool changed = false; - PhoneNumber? phoneNumber = - PhoneNumber.parse('', destinationCountry: IsoCode.FR); - void onChanged(PhoneNumber? p) { - changed = true; - phoneNumber = p; - } + testWidgets('Should call onChange', (tester) async { + bool changed = false; + PhoneNumber? phoneNumber = + PhoneNumber.parse('', destinationCountry: IsoCode.FR); + void onChanged(PhoneNumber? p) { + changed = true; + phoneNumber = p; + } + + await tester.pumpWidget( + getWidget( + initialValue: phoneNumber, + onChanged: onChanged, + ), + ); + final phoneField = find.byType(PhoneFormField); + await tester.tap(phoneField); + // non digits should not work + await tester.enterText(phoneField, 'aaa'); + await tester.pump(const Duration(seconds: 1)); + expect(changed, equals(false)); + await tester.enterText(phoneField, '123'); + await tester.pump(const Duration(seconds: 1)); + expect(changed, equals(true)); + expect( + phoneNumber, + equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR)), + ); + }); + group('validator', () { + testWidgets( + 'Should display invalid message when PhoneValidator.valid is used ' + 'and the phone number is invalid', (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget( getWidget( initialValue: phoneNumber, - onChanged: onChanged, + validatorBuilder: (context) => PhoneValidator.valid(context), ), ); final phoneField = find.byType(PhoneFormField); - await tester.tap(phoneField); - // non digits should not work - await tester.enterText(phoneField, 'aaa'); - await tester.pump(const Duration(seconds: 1)); - expect(changed, equals(false)); - await tester.enterText(phoneField, '123'); - await tester.pump(const Duration(seconds: 1)); - expect(changed, equals(true)); - expect(phoneNumber, - equals(PhoneNumber.parse('123', destinationCountry: IsoCode.FR))); - }); - }); - - group('validity', () { - testWidgets('Should tell when a phone number is not valid', - (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); - await tester.pumpWidget(getWidget(initialValue: phoneNumber)); - final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '9984'); - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(find.text('Invalid phone number'), findsOneWidget); + expect( + find.text(PhoneFieldLocalizationEn().invalidPhoneNumber), + findsOneWidget, + ); }); testWidgets( - 'Should tell when a phone number is not valid for a given phone number type', + 'Should display invalid mobile phone when PhoneValidator.validMobile' + ' is used and the phone number is not a mobile phone number', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.BE, - ); - // valid fixed line + PhoneNumber? phoneNumber = PhoneNumber.parse('+33'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validFixedLine(), + validatorBuilder: (context) => PhoneValidator.validMobile(context), )); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '77777777'); + await tester.enterText(phoneField, '6 99 99 99 99'); await tester.pumpAndSettle(); - expect(find.text('Invalid'), findsNothing); - // invalid mobile + expect( + find.text(PhoneFieldLocalizationEn().invalidMobilePhoneNumber), + findsNothing, + ); + await tester.enterText(phoneField, '777'); + await tester.pumpAndSettle(); + expect( + find.text(PhoneFieldLocalizationEn().invalidMobilePhoneNumber), + findsOneWidget, + ); + }); + + testWidgets( + 'Should display invalid fixed line phone when PhoneValidator.validFixedLine' + ' is used and the phone number is not a fixed line phone number', + (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget( initialValue: phoneNumber, - validator: PhoneValidator.validMobile( - errorText: 'Invalid phone number', - ), + validatorBuilder: (context) => PhoneValidator.validFixedLine(context), )); - final phoneField2 = find.byType(PhoneFormField); + final phoneField = find.byType(PhoneFormField); + await tester.enterText(phoneField, '67777777'); await tester.pumpAndSettle(); - await tester.enterText(phoneField2, '77777777'); + expect( + find.text(PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber), + findsNothing, + ); + await tester.enterText(phoneField, '777'); await tester.pumpAndSettle(); - expect(find.text('Invalid phone number'), findsOneWidget); + expect( + find.text(PhoneFieldLocalizationEn().invalidFixedLinePhoneNumber), + findsOneWidget, + ); + }); - // valid mobile + testWidgets( + 'should display error when PhoneValidator.required is used and the nsn is empty', + (WidgetTester tester) async { + final controller = + PhoneController(initialValue: PhoneNumber.parse('+32 444')); await tester.pumpWidget(getWidget( - initialValue: phoneNumber, - validator: PhoneValidator.validMobile( - errorText: 'Invalid phone number', - ), + controller: controller, + validatorBuilder: (context) => PhoneValidator.required(context), )); - final phoneField3 = find.byType(PhoneFormField); - await tester.enterText(phoneField3, '477668899'); + controller.changeNationalNumber(''); await tester.pumpAndSettle(); - expect(find.text('Invalid'), findsNothing); + + expect( + find.text(PhoneFieldLocalizationEn().requiredPhoneNumber), + findsOneWidget, + ); }); - }); - group('Format', () { - testWidgets('Should format when shouldFormat is true', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, + testWidgets( + 'should show error message when PhoneValidator.validCountry ' + 'is used and the current country is invalid', + (WidgetTester tester) async { + final controller = + PhoneController(initialValue: PhoneNumber.parse('+32 444')); + await tester.pumpWidget(getWidget( + controller: controller, + validatorBuilder: (context) => + PhoneValidator.validCountry(context, [IsoCode.FR, IsoCode.BE]), + )); + controller.changeCountry(IsoCode.US); + await tester.pumpAndSettle(); + expect( + find.text(PhoneFieldLocalizationEn().invalidCountry), + findsOneWidget, + ); + }, + ); + + testWidgets('should validate against all validators when compose is used', + (WidgetTester tester) async { + bool first = false; + bool second = false; + bool last = false; + + final validator = PhoneValidator.compose([ + (PhoneNumber? p) { + first = true; + return null; + }, + (PhoneNumber? p) { + second = true; + return null; + }, + (PhoneNumber? p) { + last = true; + return null; + }, + ]); + + await tester.pumpWidget( + getWidget( + initialValue: PhoneNumber.parse('+33'), + validatorBuilder: (context) => validator, + ), ); + final phoneField = find.byType(PhoneFormField); + await tester.enterText(phoneField, '9999'); + await tester.pumpAndSettle(); + expect(first, isTrue); + expect(second, isTrue); + expect(last, isTrue); + }); + testWidgets( + 'should stop and return first validator failure when compose is used', + (WidgetTester tester) async { + bool firstValidationDone = false; + bool lastValidationDone = false; + final validator = PhoneValidator.compose([ + (PhoneNumber? p) { + firstValidationDone = true; + return null; + }, + (PhoneNumber? p) { + return 'validation failed'; + }, + (PhoneNumber? p) { + lastValidationDone = true; + return null; + }, + ]); await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: true)); - await tester.pump(const Duration(seconds: 1)); + getWidget( + initialValue: PhoneNumber.parse('+33'), + validatorBuilder: (context) => validator, + ), + ); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '677777777'); - await tester.pump(const Duration(seconds: 1)); - expect(find.text('6 77 77 77 77'), findsOneWidget); + await tester.enterText(phoneField, '9999'); + await tester.pumpAndSettle(); + + expect(find.text('validation failed'), findsOneWidget); + expect(firstValidationDone, isTrue); + expect(lastValidationDone, isFalse); }); - testWidgets('Should not format when shouldFormat is false', - (tester) async { + }); + + group('Format', () { + testWidgets('Should format when shouldFormat is true', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse( '', destinationCountry: IsoCode.FR, ); - await tester.pumpWidget( - getWidget(initialValue: phoneNumber, shouldFormat: false)); + await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, '677777777'); await tester.pump(const Duration(seconds: 1)); - expect(find.text('677777777'), findsOneWidget); + expect(find.text('6 77 77 77 77'), findsOneWidget); }); }); @@ -351,18 +439,22 @@ void main() { onSaved: onSaved, )); final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '479281938'); + await tester.enterText(phoneField, '477889922'); await tester.pump(const Duration(seconds: 1)); formKey.currentState?.save(); await tester.pump(const Duration(seconds: 1)); expect(saved, isTrue); expect( - phoneNumber, - equals(PhoneNumber.parse( - '479281938', + phoneNumber, + equals( + PhoneNumber.parse( + '477 88 99 22', destinationCountry: IsoCode.FR, - ))); + ), + ), + ); }); + testWidgets('Should call onTapOutside', (tester) async { PhoneNumber? phoneNumber = PhoneNumber.parse( '', @@ -386,7 +478,7 @@ void main() { // Tap on the PhoneFormField to focus it final phoneField = find.byType(PhoneFormField); - await tester.enterText(phoneField, '479281938'); + await tester.enterText(phoneField, '488 22 33 44'); await tester.pump(const Duration(seconds: 1)); // Verify that the PhoneFormField has focus @@ -432,20 +524,18 @@ void main() { ); }); - testWidgets('Should reset', (tester) async { - PhoneNumber? phoneNumber = PhoneNumber.parse( - '', - destinationCountry: IsoCode.FR, - ); + testWidgets('Should reset with form state', (tester) async { + PhoneNumber? phoneNumber = PhoneNumber.parse('+32'); await tester.pumpWidget(getWidget(initialValue: phoneNumber)); await tester.pump(const Duration(seconds: 1)); - const national = '123456'; + const national = '477 88 99 22'; final phoneField = find.byType(PhoneFormField); await tester.enterText(phoneField, national); + await tester.pumpAndSettle(); expect(find.text(national), findsOneWidget); formKey.currentState?.reset(); - await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); expect(find.text(national), findsNothing); }); }); diff --git a/test/phone_validator_test.dart b/test/phone_validator_test.dart deleted file mode 100644 index c246e9e5..00000000 --- a/test/phone_validator_test.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:phone_form_field/phone_form_field.dart'; - -void main() async { - group('PhoneValidator.compose', () { - testWidgets('compose should test each validator', - (WidgetTester tester) async { - bool first = false; - bool second = false; - bool last = false; - - final validator = PhoneValidator.compose([ - (PhoneNumber? p) { - first = true; - return null; - }, - (PhoneNumber? p) { - second = true; - return null; - }, - (PhoneNumber? p) { - last = true; - return null; - }, - ]); - - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), isNull); - expect(first, isTrue); - expect(second, isTrue); - expect(last, isTrue); - }); - - testWidgets('compose should stop and return first validator failure', - (WidgetTester tester) async { - bool firstValidationDone = false; - bool lastValidationDone = false; - final validator = PhoneValidator.compose([ - (PhoneNumber? p) { - firstValidationDone = true; - return null; - }, - (PhoneNumber? p) { - return 'validation failed'; - }, - (PhoneNumber? p) { - lastValidationDone = true; - return null; - }, - ]); - expect(validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('validation failed')); - expect(firstValidationDone, isTrue); - expect(lastValidationDone, isFalse); - }); - }); - - group('PhoneValidator.required', () { - testWidgets('should be required value', (WidgetTester tester) async { - final validator = PhoneValidator.required(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.US, nsn: '')), - equals('requiredPhoneNumber'), - ); - - final validatorWithText = PhoneValidator.required( - errorText: 'custom message', - ); - expect( - validatorWithText(const PhoneNumber(isoCode: IsoCode.US, nsn: '')), - equals('custom message'), - ); - }); - }); - - group('PhoneValidator.invalid', () { - testWidgets('should be invalid', (WidgetTester tester) async { - final validator = PhoneValidator.valid(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '123')), - equals('invalidPhoneNumber'), - ); - - final validatorWithText = PhoneValidator.valid( - errorText: 'custom message', - ); - expect( - validatorWithText(const PhoneNumber(isoCode: IsoCode.FR, nsn: '123')), - equals('custom message'), - ); - }); - - testWidgets('should (not) be invalid when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.valid(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.valid(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidPhoneNumber'), - ); - }); - }); - - group('PhoneValidator.type', () { - testWidgets('should be invalid mobile type', (WidgetTester tester) async { - final validator = PhoneValidator.validMobile(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '412345678')), - equals('invalidMobilePhoneNumber'), - ); - - final validatorWithText = PhoneValidator.validMobile( - errorText: 'custom type message', - ); - expect( - validatorWithText( - const PhoneNumber(isoCode: IsoCode.FR, nsn: '412345678')), - equals('custom type message'), - ); - }); - - testWidgets('should (not) be invalid mobile type when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validMobile(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.validMobile(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidMobilePhoneNumber'), - ); - }); - - testWidgets('should be invalid fixed line type', - (WidgetTester tester) async { - final validator = PhoneValidator.validFixedLine(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '612345678')), - equals('invalidFixedLinePhoneNumber'), - ); - - final validatorWithText = PhoneValidator.validFixedLine( - errorText: 'custom fixed type message', - ); - expect( - validatorWithText( - const PhoneNumber(isoCode: IsoCode.FR, nsn: '612345678')), - equals('custom fixed type message'), - ); - }); - - testWidgets( - 'should (not) be invalid fixed line type when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validFixedLine(); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), isNull); - - final validatorNotEmpty = - PhoneValidator.validFixedLine(allowEmpty: false); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidFixedLinePhoneNumber'), - ); - }); - }); - - group('PhoneValidator.country', () { - testWidgets('should be invalid country', (WidgetTester tester) async { - final validator = PhoneValidator.validCountry([IsoCode.FR, IsoCode.BE]); - expect( - validator(const PhoneNumber(isoCode: IsoCode.US, nsn: '112')), - equals('invalidCountry'), - ); - }); - - testWidgets('should (not) be invalid country when (no) value entered', - (WidgetTester tester) async { - final validator = PhoneValidator.validCountry([IsoCode.US, IsoCode.BE]); - expect( - validator(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - isNull, - ); - - final validatorNotEmpty = PhoneValidator.validCountry( - [IsoCode.US, IsoCode.BE], - allowEmpty: false, - ); - expect( - validatorNotEmpty(const PhoneNumber(isoCode: IsoCode.FR, nsn: '')), - equals('invalidCountry'), - ); - }); - }); -}