import 'package:active_ecommerce_seller_app/custom/buttons.dart'; import 'package:active_ecommerce_seller_app/my_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl_phone_number_input/src/models/country_list.dart'; import 'package:intl_phone_number_input/src/models/country_model.dart'; import 'package:intl_phone_number_input/src/providers/country_provider.dart'; import 'package:intl_phone_number_input/src/utils/formatter/as_you_type_formatter.dart'; import 'package:intl_phone_number_input/src/utils/phone_number.dart'; import 'package:intl_phone_number_input/src/utils/phone_number/phone_number_util.dart'; import 'package:intl_phone_number_input/src/utils/selector_config.dart'; import 'package:intl_phone_number_input/src/utils/test/test_helper.dart'; import 'package:intl_phone_number_input/src/utils/util.dart'; import 'package:intl_phone_number_input/src/utils/widget_view.dart'; import 'package:intl_phone_number_input/src/utils/selector_config.dart'; import 'package:intl_phone_number_input/src/widgets/countries_search_list_widget.dart'; import 'package:intl_phone_number_input/src/widgets/input_widget.dart'; import 'package:intl_phone_number_input/src/widgets/item.dart'; /// Enum for [CustomSelectorButton] types. /// /// Available type includes: /// * [PhoneInputSelectorType.DROPDOWN] /// * [PhoneInputSelectorType.BOTTOM_SHEET] /// * [PhoneInputSelectorType.DIALOG] /// A [TextFormField] for [CustomInternationalPhoneNumberInput]. /// /// [initialValue] accepts a [PhoneNumber] this is used to set initial values /// for phone the input field and the selector button /// /// [selectorButtonOnErrorPadding] is a double which is used to align the selector /// button with the input field when an error occurs /// /// [locale] accepts a country locale which will be used to translation, if the /// translation exist /// /// [countries] accepts list of string on Country isoCode, if specified filters /// available countries to match the [countries] specified. class CustomInternationalPhoneNumberInput extends StatefulWidget { final SelectorConfig selectorConfig; final ValueChanged onInputChanged; final ValueChanged? onInputValidated; final VoidCallback? onSubmit; final ValueChanged? onFieldSubmitted; final String Function(String?)? validator; final ValueChanged? onSaved; final TextEditingController? textFieldController; final TextInputType keyboardType; final TextInputAction? keyboardAction; final PhoneNumber? initialValue; final String hintText; final String errorMessage; final double selectorButtonOnErrorPadding; final double spaceBetweenSelectorAndTextField; final int maxLength; final bool isEnabled; final bool formatInput; final bool autoFocus; final bool autoFocusSearch; final AutovalidateMode autoValidateMode; final bool ignoreBlank; final bool countrySelectorScrollControlled; final String? locale; final TextStyle? textStyle; final TextStyle? selectorTextStyle; final InputBorder? inputBorder; final InputDecoration? inputDecoration; final InputDecoration? searchBoxDecoration; final Color? cursorColor; final TextAlign textAlign; final TextAlignVertical textAlignVertical; final EdgeInsets scrollPadding; final FocusNode? focusNode; final Iterable? autofillHints; final List? countries; CustomInternationalPhoneNumberInput( {Key? key, this.selectorConfig = const SelectorConfig(), required this.onInputChanged, this.onInputValidated, this.onSubmit, this.onFieldSubmitted, this.validator, this.onSaved, this.textFieldController, this.keyboardAction, this.keyboardType = TextInputType.phone, this.initialValue, this.hintText = 'Phone number', this.errorMessage = 'Invalid phone number', this.selectorButtonOnErrorPadding = 24, this.spaceBetweenSelectorAndTextField = 12, this.maxLength = 15, this.isEnabled = true, this.formatInput = true, this.autoFocus = false, this.autoFocusSearch = false, this.autoValidateMode = AutovalidateMode.disabled, this.ignoreBlank = false, this.countrySelectorScrollControlled = true, this.locale, this.textStyle, this.selectorTextStyle, this.inputBorder, this.inputDecoration, this.searchBoxDecoration, this.textAlign = TextAlign.start, this.textAlignVertical = TextAlignVertical.center, this.scrollPadding = const EdgeInsets.all(20.0), this.focusNode, this.cursorColor, this.autofillHints, this.countries}) : super(key: key); @override State createState() => _InputWidgetState(); } class _InputWidgetState extends State { TextEditingController? controller; double selectorButtonBottomPadding = 0; Country? country; List countries = []; bool isNotValid = true; @override void initState() { super.initState(); loadCountries(); controller = widget.textFieldController ?? TextEditingController(); initialiseWidget(); } @override void setState(fn) { if (this.mounted) { super.setState(fn); } } @override Widget build(BuildContext context) { return _InputWidgetView( state: this, ); } @override void didUpdateWidget(CustomInternationalPhoneNumberInput oldWidget) { if (oldWidget?.initialValue?.hash != widget?.initialValue?.hash) { loadCountries(); initialiseWidget(); } else { loadCountries(previouslySelectedCountry: country); } super.didUpdateWidget(oldWidget); } /// [initialiseWidget] sets initial values of the widget void initialiseWidget() async { if (widget.initialValue != null) { if (widget.initialValue!.phoneNumber != null && widget.initialValue!.phoneNumber!.isNotEmpty && (await PhoneNumberUtil.isValidNumber( phoneNumber: widget.initialValue!.phoneNumber!, isoCode: widget.initialValue!.isoCode!))!) { controller!.text = await PhoneNumber.getParsableNumber(widget.initialValue!); phoneNumberControllerListener(); } } } /// loads countries from [Countries.countryList] and selected Country void loadCountries({Country? previouslySelectedCountry}) { if (this.mounted) { List countries = CountryProvider.getCountriesData(countries: widget.countries!.cast()); final CountryComparator? countryComparator = widget.selectorConfig?.countryComparator; if (countryComparator != null) { countries.sort(countryComparator); } Country country = previouslySelectedCountry ?? Utils.getInitialSelectedCountry( countries, widget.initialValue?.isoCode ?? '', ); setState(() { this.countries = countries; this.country = country; }); } } /// Listener that validates changes from the widget, returns a bool to /// the `ValueCallback` [widget.onInputValidated] void phoneNumberControllerListener() { if (this.mounted) { String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), ''); getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code) .then((phoneNumber) { if (phoneNumber == null) { String phoneNumber = '${this.country?.dialCode}$parsedPhoneNumberString'; if (widget.onInputChanged != null) { widget.onInputChanged(PhoneNumber( phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode)); } if (widget.onInputValidated != null) { widget.onInputValidated!(false); } this.isNotValid = true; } else { if (widget.onInputChanged != null) { widget.onInputChanged(PhoneNumber( phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode)); } if (widget.onInputValidated != null) { widget.onInputValidated!(true); } this.isNotValid = false; } }); } } /// Returns a formatted String of [phoneNumber] with [isoCode], returns `null` /// if [phoneNumber] is not valid or if an [Exception] is caught. Future getParsedPhoneNumber( String phoneNumber, String? isoCode) async { if (phoneNumber.isNotEmpty && isoCode != null) { try { bool isValidPhoneNumber = (await PhoneNumberUtil.isValidNumber( phoneNumber: phoneNumber, isoCode: isoCode))!; if (isValidPhoneNumber) { return await PhoneNumberUtil.normalizePhoneNumber( phoneNumber: phoneNumber, isoCode: isoCode); } } on Exception { return null; } } return null; } /// Creates or Select [InputDecoration] InputDecoration getInputDecoration(InputDecoration? decoration) { InputDecoration value = decoration ?? InputDecoration( border: widget.inputBorder ?? UnderlineInputBorder(), hintText: widget.hintText, ); if (widget.selectorConfig.setSelectorButtonAsPrefixIcon) { return value.copyWith( prefixIcon: CustomSelectorButton( country: country, countries: countries, onCountryChanged: onCountryChanged, selectorConfig: widget.selectorConfig, selectorTextStyle: widget.selectorTextStyle, searchBoxDecoration: widget.searchBoxDecoration, locale: locale, isEnabled: widget.isEnabled, autoFocusSearchField: widget.autoFocusSearch, isScrollControlled: widget.countrySelectorScrollControlled, )); } return value; } /// Validate the phone number when a change occurs void onChanged(String value) { phoneNumberControllerListener(); } /// Validate and returns a validation error when [FormState] validate is called. /// /// Also updates [selectorButtonBottomPadding] String? validator(String? value) { bool isValid = this.isNotValid && (value!.isNotEmpty || widget.ignoreBlank == false); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (isValid && widget.errorMessage != null) { setState(() { this.selectorButtonBottomPadding = widget.selectorButtonOnErrorPadding ?? 24; }); } else { setState(() { this.selectorButtonBottomPadding = 0; }); } }); return isValid ? widget.errorMessage : null; } /// Changes Selector Button Country and Validate Change. void onCountryChanged(Country? country) { setState(() { this.country = country; }); phoneNumberControllerListener(); } void _phoneNumberSaved() { if (this.mounted) { String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), ''); getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code) .then( (phoneNumber) => widget.onSaved?.call( PhoneNumber( phoneNumber: phoneNumber, isoCode: this.country?.alpha2Code, dialCode: this.country?.dialCode), ), ); } } /// Saved the phone number when form is saved void onSaved(String? value) { _phoneNumberSaved(); } /// Corrects duplicate locale String? get locale { if (widget.locale == null) return null; if (widget.locale!.toLowerCase() == 'nb' || widget.locale!.toLowerCase() == 'nn') { return 'no'; } return widget.locale; } } class _InputWidgetView extends WidgetView { final _InputWidgetState state; _InputWidgetView({Key? key, required this.state}) : super(key: key, state: state); @override Widget build(BuildContext context) { final countryCode = state?.country?.alpha2Code ?? ''; final dialCode = state?.country?.dialCode ?? ''; return Container( child: Row( textDirection: TextDirection.ltr, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ CustomSelectorButton( country: state.country, countries: state.countries, onCountryChanged: state.onCountryChanged, selectorConfig: widget.selectorConfig, selectorTextStyle: widget.selectorTextStyle, searchBoxDecoration: widget.searchBoxDecoration, locale: state.locale, isEnabled: widget.isEnabled, autoFocusSearchField: widget.autoFocusSearch, isScrollControlled: widget.countrySelectorScrollControlled, ), SizedBox( height: state.selectorButtonBottomPadding, ), ], ), Flexible( child: TextFormField( key: Key(TestHelper.TextInputKeyValue), textDirection: TextDirection.ltr, controller: state.controller, focusNode: widget.focusNode, enabled: widget.isEnabled, autofocus: widget.autoFocus, keyboardType: widget.keyboardType, textInputAction: widget.keyboardAction, style: widget.textStyle, decoration: state.getInputDecoration(widget.inputDecoration), textAlign: widget.textAlign, textAlignVertical: widget.textAlignVertical, onEditingComplete: widget.onSubmit, onFieldSubmitted: widget.onFieldSubmitted, autovalidateMode: widget.autoValidateMode, autofillHints: widget.autofillHints, validator: widget.validator ?? state.validator, onSaved: state.onSaved, scrollPadding: widget.scrollPadding, inputFormatters: [ LengthLimitingTextInputFormatter(widget.maxLength), widget.formatInput ? AsYouTypeFormatter( isoCode: countryCode, dialCode: dialCode, onInputFormatted: (TextEditingValue value) { state.controller!.value = value; }, ) : FilteringTextInputFormatter.digitsOnly, ], onChanged: state.onChanged, ), ) ], ), ); } } /// [CustomSelectorButton] class CustomSelectorButton extends StatelessWidget { final List countries; final Country? country; final SelectorConfig selectorConfig; final TextStyle? selectorTextStyle; final InputDecoration? searchBoxDecoration; final bool autoFocusSearchField; final String? locale; final bool isEnabled; final bool isScrollControlled; final ValueChanged onCountryChanged; const CustomSelectorButton({ Key? key, required this.countries, required this.country, required this.selectorConfig, required this.selectorTextStyle, required this.searchBoxDecoration, required this.autoFocusSearchField, required this.locale, required this.onCountryChanged, required this.isEnabled, required this.isScrollControlled, }) : super(key: key); @override Widget build(BuildContext context) { return selectorConfig.selectorType == PhoneInputSelectorType.DROPDOWN ? countries.isNotEmpty && countries.length > 1 ? DropdownButtonHideUnderline( child: DropdownButton( key: Key(TestHelper.DropdownButtonKeyValue), hint: Item( country: country, showFlag: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, textStyle: selectorTextStyle, ), value: country, items: mapCountryToDropdownItem(countries), onChanged: isEnabled ? onCountryChanged : null, ), ) : Item( country: country, showFlag: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, textStyle: selectorTextStyle, ) : Container( height: 36, decoration: BoxDecoration( color: Colors.white, border: Border.all( color: MyTheme.textfield_grey, width: .5 ), borderRadius: BorderRadius.only( topLeft: Radius.circular(5.0), bottomLeft: Radius.circular(5.0))), child: Buttons( key: Key(TestHelper.DropdownButtonKeyValue), padding: EdgeInsets.zero, color: MyTheme.grey_153, shape: RoundedRectangleBorder( borderRadius: const BorderRadius.only( topLeft: const Radius.circular(5.0), bottomLeft: const Radius.circular(5.0), )), onPressed: countries.isNotEmpty && countries.length > 1 && isEnabled ? () async { Country? selected; if (selectorConfig.selectorType == PhoneInputSelectorType.BOTTOM_SHEET) { selected = await showCountrySelectorBottomSheet( context, countries); } else { selected = await showCountrySelectorDialog(context, countries); } if (selected != null) { onCountryChanged(selected); } } : null, child: Padding( padding: const EdgeInsets.only(right: 8.0), child: Item( country: country, showFlag: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, textStyle: TextStyle( color: MyTheme.textfield_grey) //selectorTextStyle, ), ), ), ); } /// Converts the list [countries] to `DropdownMenuItem` List> mapCountryToDropdownItem( List countries) { return countries.map((country) { return DropdownMenuItem( value: country, child: Item( key: Key(TestHelper.countryItemKeyValue(country.alpha2Code)), country: country, showFlag: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, textStyle: selectorTextStyle, withCountryNames: false, ), ); }).toList(); } /// shows a Dialog with list [countries] if the [PhoneInputSelectorType.DIALOG] is selected Future showCountrySelectorDialog( BuildContext context, List countries) { return showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) => AlertDialog( content: Container( width: double.maxFinite, child: CountrySearchListWidget( countries, locale, searchBoxDecoration: searchBoxDecoration, showFlags: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, autoFocus: autoFocusSearchField, ), ), ), ); } /// shows a Dialog with list [countries] if the [PhoneInputSelectorType.BOTTOM_SHEET] is selected Future showCountrySelectorBottomSheet( BuildContext context, List countries) { return showModalBottomSheet( context: context, clipBehavior: Clip.hardEdge, isScrollControlled: isScrollControlled ?? true, backgroundColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12))), builder: (BuildContext context) { return AnimatedPadding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), duration: const Duration(milliseconds: 100), child: Stack(children: [ GestureDetector( onTap: () => Navigator.pop(context), ), DraggableScrollableSheet( builder: (BuildContext context, ScrollController controller) { return Container( decoration: ShapeDecoration( color: Theme.of(context).canvasColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), ), child: CountrySearchListWidget( countries, locale, searchBoxDecoration: searchBoxDecoration, scrollController: controller, showFlags: selectorConfig.showFlags, useEmoji: selectorConfig.useEmoji, autoFocus: autoFocusSearchField, ), ); }, ), ]), ); }, ); } }