25-7-28-2
This commit is contained in:
7574
lib/controller/local/phone_intel/countries.dart
Normal file
7574
lib/controller/local/phone_intel/countries.dart
Normal file
File diff suppressed because it is too large
Load Diff
168
lib/controller/local/phone_intel/country_picker_dialog.dart
Normal file
168
lib/controller/local/phone_intel/country_picker_dialog.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sefer_driver/controller/local/phone_intel/helpers.dart';
|
||||
|
||||
import 'countries.dart';
|
||||
|
||||
class PickerDialogStyle {
|
||||
final Color? backgroundColor;
|
||||
|
||||
final TextStyle? countryCodeStyle;
|
||||
|
||||
final TextStyle? countryNameStyle;
|
||||
|
||||
final Widget? listTileDivider;
|
||||
|
||||
final EdgeInsets? listTilePadding;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final Color? searchFieldCursorColor;
|
||||
|
||||
final InputDecoration? searchFieldInputDecoration;
|
||||
|
||||
final EdgeInsets? searchFieldPadding;
|
||||
|
||||
final double? width;
|
||||
|
||||
PickerDialogStyle({
|
||||
this.backgroundColor,
|
||||
this.countryCodeStyle,
|
||||
this.countryNameStyle,
|
||||
this.listTileDivider,
|
||||
this.listTilePadding,
|
||||
this.padding,
|
||||
this.searchFieldCursorColor,
|
||||
this.searchFieldInputDecoration,
|
||||
this.searchFieldPadding,
|
||||
this.width,
|
||||
});
|
||||
}
|
||||
|
||||
class CountryPickerDialog extends StatefulWidget {
|
||||
final List<Country> countryList;
|
||||
final Country selectedCountry;
|
||||
final ValueChanged<Country> onCountryChanged;
|
||||
final String searchText;
|
||||
final List<Country> filteredCountries;
|
||||
final PickerDialogStyle? style;
|
||||
final String languageCode;
|
||||
|
||||
const CountryPickerDialog({
|
||||
Key? key,
|
||||
required this.searchText,
|
||||
required this.languageCode,
|
||||
required this.countryList,
|
||||
required this.onCountryChanged,
|
||||
required this.selectedCountry,
|
||||
required this.filteredCountries,
|
||||
this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CountryPickerDialog> createState() => _CountryPickerDialogState();
|
||||
}
|
||||
|
||||
class _CountryPickerDialogState extends State<CountryPickerDialog> {
|
||||
late List<Country> _filteredCountries;
|
||||
late Country _selectedCountry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selectedCountry = widget.selectedCountry;
|
||||
_filteredCountries = widget.filteredCountries.toList()
|
||||
..sort(
|
||||
(a, b) => a
|
||||
.localizedName(widget.languageCode)
|
||||
.compareTo(b.localizedName(widget.languageCode)),
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaWidth = MediaQuery.of(context).size.width;
|
||||
final width = widget.style?.width ?? mediaWidth;
|
||||
const defaultHorizontalPadding = 40.0;
|
||||
const defaultVerticalPadding = 24.0;
|
||||
return Dialog(
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
vertical: defaultVerticalPadding,
|
||||
horizontal: mediaWidth > (width + defaultHorizontalPadding * 2)
|
||||
? (mediaWidth - width) / 2
|
||||
: defaultHorizontalPadding),
|
||||
backgroundColor: widget.style?.backgroundColor,
|
||||
child: Container(
|
||||
padding: widget.style?.padding ?? const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding:
|
||||
widget.style?.searchFieldPadding ?? const EdgeInsets.all(0),
|
||||
child: TextField(
|
||||
cursorColor: widget.style?.searchFieldCursorColor,
|
||||
decoration: widget.style?.searchFieldInputDecoration ??
|
||||
InputDecoration(
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
labelText: widget.searchText,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_filteredCountries = widget.countryList.stringSearch(value)
|
||||
..sort(
|
||||
(a, b) => a
|
||||
.localizedName(widget.languageCode)
|
||||
.compareTo(b.localizedName(widget.languageCode)),
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredCountries.length,
|
||||
itemBuilder: (ctx, index) => Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: kIsWeb
|
||||
? Image.asset(
|
||||
'assets/flags/${_filteredCountries[index].code.toLowerCase()}.png',
|
||||
package: 'intl_phone_field',
|
||||
width: 32,
|
||||
)
|
||||
: Text(
|
||||
_filteredCountries[index].flag,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
contentPadding: widget.style?.listTilePadding,
|
||||
title: Text(
|
||||
_filteredCountries[index]
|
||||
.localizedName(widget.languageCode),
|
||||
style: widget.style?.countryNameStyle ??
|
||||
const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
trailing: Text(
|
||||
'+${_filteredCountries[index].dialCode}',
|
||||
style: widget.style?.countryCodeStyle ??
|
||||
const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
onTap: () {
|
||||
_selectedCountry = _filteredCountries[index];
|
||||
widget.onCountryChanged(_selectedCountry);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
widget.style?.listTileDivider ??
|
||||
const Divider(thickness: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
lib/controller/local/phone_intel/helpers.dart
Normal file
31
lib/controller/local/phone_intel/helpers.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'countries.dart';
|
||||
|
||||
bool isNumeric(String s) =>
|
||||
s.isNotEmpty && int.tryParse(s.replaceAll("+", "")) != null;
|
||||
|
||||
String removeDiacritics(String str) {
|
||||
var withDia =
|
||||
'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž';
|
||||
var withoutDia =
|
||||
'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz';
|
||||
|
||||
for (int i = 0; i < withDia.length; i++) {
|
||||
str = str.replaceAll(withDia[i], withoutDia[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
extension CountryExtensions on List<Country> {
|
||||
List<Country> stringSearch(String search) {
|
||||
search = removeDiacritics(search.toLowerCase());
|
||||
return where(
|
||||
(country) => isNumeric(search) || search.startsWith("+")
|
||||
? country.dialCode.contains(search)
|
||||
: removeDiacritics(country.name.replaceAll("+", "").toLowerCase())
|
||||
.contains(search) ||
|
||||
country.nameTranslations.values.any((element) =>
|
||||
removeDiacritics(element.toLowerCase()).contains(search)),
|
||||
).toList();
|
||||
}
|
||||
}
|
||||
521
lib/controller/local/phone_intel/intl_phone_field.dart
Normal file
521
lib/controller/local/phone_intel/intl_phone_field.dart
Normal file
@@ -0,0 +1,521 @@
|
||||
library intl_phone_field;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import './countries.dart';
|
||||
import './phone_number.dart';
|
||||
import 'country_picker_dialog.dart';
|
||||
import 'helpers.dart';
|
||||
|
||||
class IntlPhoneField extends StatefulWidget {
|
||||
/// The TextFormField key.
|
||||
final GlobalKey<FormFieldState>? formFieldKey;
|
||||
|
||||
/// Whether to hide the text being edited (e.g., for passwords).
|
||||
final bool obscureText;
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// How the text should be aligned vertically.
|
||||
final TextAlignVertical? textAlignVertical;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
final FormFieldSetter<PhoneNumber>? onSaved;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onChanged}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [inputFormatters], which are called before [onChanged]
|
||||
/// runs and can validate and change ("format") the input value.
|
||||
/// * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
|
||||
/// which are more specialized input change notifications.
|
||||
final ValueChanged<PhoneNumber>? onChanged;
|
||||
|
||||
final ValueChanged<Country>? onCountryChanged;
|
||||
|
||||
/// An optional method that validates an input. Returns an error string to display if the input is invalid, or null otherwise.
|
||||
///
|
||||
/// A [PhoneNumber] is passed to the validator as argument.
|
||||
/// The validator can handle asynchronous validation when declared as a [Future].
|
||||
/// Or run synchronously when declared as a [Function].
|
||||
///
|
||||
/// By default, the validator checks whether the input number length is between selected country's phone numbers min and max length.
|
||||
/// If `disableLengthCheck` is not set to `true`, your validator returned value will be overwritten by the default validator.
|
||||
/// But, if `disableLengthCheck` is set to `true`, your validator will have to check phone number length itself.
|
||||
final FutureOr<String?> Function(PhoneNumber?)? validator;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.keyboardType}
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [TextEditingController].
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// Defines the keyboard focus for this widget.
|
||||
///
|
||||
/// The [focusNode] is a long-lived object that's typically managed by a
|
||||
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
||||
///
|
||||
/// To give the keyboard focus to this widget, provide a [focusNode] and then
|
||||
/// use the current [FocusScope] to request the focus:
|
||||
///
|
||||
/// ```dart
|
||||
/// FocusScope.of(context).requestFocus(myFocusNode);
|
||||
/// ```
|
||||
///
|
||||
/// This happens automatically when the widget is tapped.
|
||||
///
|
||||
/// To be notified when the widget gains or loses the focus, add a listener
|
||||
/// to the [focusNode]:
|
||||
///
|
||||
/// ```dart
|
||||
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
|
||||
/// ```
|
||||
///
|
||||
/// If null, this widget will create its own [FocusNode].
|
||||
///
|
||||
/// ## Keyboard
|
||||
///
|
||||
/// Requesting the focus will typically cause the keyboard to be shown
|
||||
/// if it's not showing already.
|
||||
///
|
||||
/// On Android, the user can hide the keyboard - without changing the focus -
|
||||
/// with the system back button. They can restore the keyboard's visibility
|
||||
/// by tapping on a text field. The user might hide the keyboard and
|
||||
/// switch to a physical keyboard, or they might just need to get it
|
||||
/// out of the way for a moment, to expose something it's
|
||||
/// obscuring. In this case requesting the focus again will not
|
||||
/// cause the focus to change, and will not make the keyboard visible.
|
||||
///
|
||||
/// This widget builds an [EditableText] and will ensure that the keyboard is
|
||||
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onSubmitted}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [EditableText.onSubmitted] for an example of how to handle moving to
|
||||
/// the next/previous field when using [TextInputAction.next] and
|
||||
/// [TextInputAction.previous] for [textInputAction].
|
||||
final void Function(String)? onSubmitted;
|
||||
|
||||
/// If false the widget is "disabled": it ignores taps, the [TextFormField]'s
|
||||
/// [decoration] is rendered in grey,
|
||||
/// [decoration]'s [InputDecoration.counterText] is set to `""`,
|
||||
/// and the drop down icon is hidden no matter [showDropdownIcon] value.
|
||||
///
|
||||
/// If non-null this property overrides the [decoration]'s
|
||||
/// [Decoration.enabled] property.
|
||||
final bool enabled;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
/// This setting is only honored on iOS devices.
|
||||
///
|
||||
/// If unset, defaults to the brightness of [ThemeData.brightness].
|
||||
final Brightness? keyboardAppearance;
|
||||
|
||||
/// Initial Value for the field.
|
||||
/// This property can be used to pre-fill the field.
|
||||
final String? initialValue;
|
||||
|
||||
final String languageCode;
|
||||
|
||||
/// 2 letter ISO Code or country dial code.
|
||||
///
|
||||
/// ```dart
|
||||
/// initialCountryCode: 'IN', // India
|
||||
/// initialCountryCode: '+225', // Côte d'Ivoire
|
||||
/// ```
|
||||
final String? initialCountryCode;
|
||||
|
||||
/// List of Country to display see countries.dart for format
|
||||
final List<Country>? countries;
|
||||
|
||||
/// The decoration to show around the text field.
|
||||
///
|
||||
/// By default, draws a horizontal line under the text field but can be
|
||||
/// configured to show an icon, label, hint text, and error text.
|
||||
///
|
||||
/// Specify null to remove the decoration entirely (including the
|
||||
/// extra padding introduced by the decoration to save space for the labels).
|
||||
final InputDecoration decoration;
|
||||
|
||||
/// The style to use for the text being edited.
|
||||
///
|
||||
/// This text style is also used as the base style for the [decoration].
|
||||
///
|
||||
/// If null, defaults to the `subtitle1` text style from the current [Theme].
|
||||
final TextStyle? style;
|
||||
|
||||
/// Disable view Min/Max Length check
|
||||
final bool disableLengthCheck;
|
||||
|
||||
/// Won't work if [enabled] is set to `false`.
|
||||
final bool showDropdownIcon;
|
||||
|
||||
final BoxDecoration dropdownDecoration;
|
||||
|
||||
/// The style use for the country dial code.
|
||||
final TextStyle? dropdownTextStyle;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.inputFormatters}
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
/// The text that describes the search input field.
|
||||
///
|
||||
/// When the input field is empty and unfocused, the label is displayed on top of the input field (i.e., at the same location on the screen where text may be entered in the input field).
|
||||
/// When the input field receives focus (or if the field is non-empty), the label moves above (i.e., vertically adjacent to) the input field.
|
||||
final String searchText;
|
||||
|
||||
/// Position of an icon [leading, trailing]
|
||||
final IconPosition dropdownIconPosition;
|
||||
|
||||
/// Icon of the drop down button.
|
||||
///
|
||||
/// Default is [Icon(Icons.arrow_drop_down)]
|
||||
final Icon dropdownIcon;
|
||||
|
||||
/// Whether this text field should focus itself if nothing else is already focused.
|
||||
final bool autofocus;
|
||||
|
||||
/// Autovalidate mode for text form field.
|
||||
///
|
||||
/// If [AutovalidateMode.onUserInteraction], this FormField will only auto-validate after its content changes.
|
||||
/// If [AutovalidateMode.always], it will auto-validate even without user interaction.
|
||||
/// If [AutovalidateMode.disabled], auto-validation will be disabled.
|
||||
///
|
||||
/// Defaults to [AutovalidateMode.onUserInteraction].
|
||||
final AutovalidateMode? autovalidateMode;
|
||||
|
||||
/// Whether to show or hide country flag.
|
||||
///
|
||||
/// Default value is `true`.
|
||||
final bool showCountryFlag;
|
||||
|
||||
/// Message to be displayed on autoValidate error
|
||||
///
|
||||
/// Default value is `Invalid Mobile Number`.
|
||||
final String? invalidNumberMessage;
|
||||
|
||||
/// The color of the cursor.
|
||||
final Color? cursorColor;
|
||||
|
||||
/// How tall the cursor will be.
|
||||
final double? cursorHeight;
|
||||
|
||||
/// How rounded the corners of the cursor should be.
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// How thick the cursor will be.
|
||||
final double cursorWidth;
|
||||
|
||||
/// Whether to show cursor.
|
||||
final bool? showCursor;
|
||||
|
||||
/// The padding of the Flags Button.
|
||||
///
|
||||
/// The amount of insets that are applied to the Flags Button.
|
||||
///
|
||||
/// If unset, defaults to [EdgeInsets.zero].
|
||||
final EdgeInsetsGeometry flagsButtonPadding;
|
||||
|
||||
/// The type of action button to use for the keyboard.
|
||||
final TextInputAction? textInputAction;
|
||||
|
||||
/// Optional set of styles to allow for customizing the country search
|
||||
/// & pick dialog
|
||||
final PickerDialogStyle? pickerDialogStyle;
|
||||
|
||||
/// The margin of the country selector button.
|
||||
///
|
||||
/// The amount of space to surround the country selector button.
|
||||
///
|
||||
/// If unset, defaults to [EdgeInsets.zero].
|
||||
final EdgeInsets flagsButtonMargin;
|
||||
|
||||
/// Enable the autofill hint for phone number.
|
||||
final bool disableAutoFillHints;
|
||||
|
||||
/// If null, default magnification configuration will be used.
|
||||
final TextMagnifierConfiguration? magnifierConfiguration;
|
||||
|
||||
const IntlPhoneField({
|
||||
Key? key,
|
||||
this.formFieldKey,
|
||||
this.initialCountryCode,
|
||||
this.languageCode = 'en',
|
||||
this.disableAutoFillHints = false,
|
||||
this.obscureText = false,
|
||||
this.textAlign = TextAlign.left,
|
||||
this.textAlignVertical,
|
||||
this.onTap,
|
||||
this.readOnly = false,
|
||||
this.initialValue,
|
||||
this.keyboardType = TextInputType.phone,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.decoration = const InputDecoration(),
|
||||
this.style,
|
||||
this.dropdownTextStyle,
|
||||
this.onSubmitted,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.countries,
|
||||
this.onCountryChanged,
|
||||
this.onSaved,
|
||||
this.showDropdownIcon = true,
|
||||
this.dropdownDecoration = const BoxDecoration(),
|
||||
this.inputFormatters,
|
||||
this.enabled = true,
|
||||
this.keyboardAppearance,
|
||||
@Deprecated('Use searchFieldInputDecoration of PickerDialogStyle instead')
|
||||
this.searchText = 'Search country',
|
||||
this.dropdownIconPosition = IconPosition.leading,
|
||||
this.dropdownIcon = const Icon(Icons.arrow_drop_down),
|
||||
this.autofocus = false,
|
||||
this.textInputAction,
|
||||
this.autovalidateMode = AutovalidateMode.onUserInteraction,
|
||||
this.showCountryFlag = true,
|
||||
this.cursorColor,
|
||||
this.disableLengthCheck = false,
|
||||
this.flagsButtonPadding = EdgeInsets.zero,
|
||||
this.invalidNumberMessage = 'Invalid Mobile Number',
|
||||
this.cursorHeight,
|
||||
this.cursorRadius = Radius.zero,
|
||||
this.cursorWidth = 2.0,
|
||||
this.showCursor = true,
|
||||
this.pickerDialogStyle,
|
||||
this.flagsButtonMargin = EdgeInsets.zero,
|
||||
this.magnifierConfiguration,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<IntlPhoneField> createState() => _IntlPhoneFieldState();
|
||||
}
|
||||
|
||||
class _IntlPhoneFieldState extends State<IntlPhoneField> {
|
||||
late List<Country> _countryList;
|
||||
late Country _selectedCountry;
|
||||
late List<Country> filteredCountries;
|
||||
late String number;
|
||||
|
||||
String? validatorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_countryList = widget.countries ?? countries;
|
||||
filteredCountries = _countryList;
|
||||
number = widget.initialValue ?? '';
|
||||
if (widget.initialCountryCode == null && number.startsWith('+')) {
|
||||
number = number.substring(1);
|
||||
// parse initial value
|
||||
_selectedCountry = countries.firstWhere(
|
||||
(country) => number.startsWith(country.fullCountryCode),
|
||||
orElse: () => _countryList.first);
|
||||
|
||||
// remove country code from the initial number value
|
||||
number = number.replaceFirst(
|
||||
RegExp("^${_selectedCountry.fullCountryCode}"), "");
|
||||
} else {
|
||||
_selectedCountry = _countryList.firstWhere(
|
||||
(item) => item.code == (widget.initialCountryCode ?? 'US'),
|
||||
orElse: () => _countryList.first);
|
||||
|
||||
// remove country code from the initial number value
|
||||
if (number.startsWith('+')) {
|
||||
number = number.replaceFirst(
|
||||
RegExp("^\\+${_selectedCountry.fullCountryCode}"), "");
|
||||
} else {
|
||||
number = number.replaceFirst(
|
||||
RegExp("^${_selectedCountry.fullCountryCode}"), "");
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.autovalidateMode == AutovalidateMode.always) {
|
||||
final initialPhoneNumber = PhoneNumber(
|
||||
countryISOCode: _selectedCountry.code,
|
||||
countryCode: '+${_selectedCountry.dialCode}',
|
||||
number: widget.initialValue ?? '',
|
||||
);
|
||||
|
||||
final value = widget.validator?.call(initialPhoneNumber);
|
||||
|
||||
if (value is String) {
|
||||
validatorMessage = value;
|
||||
} else {
|
||||
(value as Future).then((msg) {
|
||||
validatorMessage = msg;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeCountry() async {
|
||||
filteredCountries = _countryList;
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (ctx, setState) => CountryPickerDialog(
|
||||
languageCode: widget.languageCode.toLowerCase(),
|
||||
style: widget.pickerDialogStyle,
|
||||
filteredCountries: filteredCountries,
|
||||
searchText: widget.searchText,
|
||||
countryList: _countryList,
|
||||
selectedCountry: _selectedCountry,
|
||||
onCountryChanged: (Country country) {
|
||||
_selectedCountry = country;
|
||||
widget.onCountryChanged?.call(country);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
key: widget.formFieldKey,
|
||||
initialValue: (widget.controller == null) ? number : null,
|
||||
autofillHints: widget.disableAutoFillHints
|
||||
? null
|
||||
: [AutofillHints.telephoneNumberNational],
|
||||
readOnly: widget.readOnly,
|
||||
obscureText: widget.obscureText,
|
||||
textAlign: widget.textAlign,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
cursorColor: widget.cursorColor,
|
||||
onTap: widget.onTap,
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
showCursor: widget.showCursor,
|
||||
onFieldSubmitted: widget.onSubmitted,
|
||||
magnifierConfiguration: widget.magnifierConfiguration,
|
||||
decoration: widget.decoration.copyWith(
|
||||
prefixIcon: _buildFlagsButton(),
|
||||
counterText: !widget.enabled ? '' : null,
|
||||
),
|
||||
style: widget.style,
|
||||
onSaved: (value) {
|
||||
widget.onSaved?.call(
|
||||
PhoneNumber(
|
||||
countryISOCode: _selectedCountry.code,
|
||||
countryCode:
|
||||
'+${_selectedCountry.dialCode}${_selectedCountry.regionCode}',
|
||||
number: value!,
|
||||
),
|
||||
);
|
||||
},
|
||||
onChanged: (value) async {
|
||||
final phoneNumber = PhoneNumber(
|
||||
countryISOCode: _selectedCountry.code,
|
||||
countryCode: '+${_selectedCountry.fullCountryCode}',
|
||||
number: value,
|
||||
);
|
||||
|
||||
if (widget.autovalidateMode != AutovalidateMode.disabled) {
|
||||
validatorMessage = await widget.validator?.call(phoneNumber);
|
||||
}
|
||||
|
||||
widget.onChanged?.call(phoneNumber);
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || !isNumeric(value)) return validatorMessage;
|
||||
if (!widget.disableLengthCheck) {
|
||||
return value.length >= _selectedCountry.minLength &&
|
||||
value.length <= _selectedCountry.maxLength
|
||||
? null
|
||||
: widget.invalidNumberMessage;
|
||||
}
|
||||
|
||||
return validatorMessage;
|
||||
},
|
||||
maxLength: widget.disableLengthCheck ? null : _selectedCountry.maxLength,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
enabled: widget.enabled,
|
||||
keyboardAppearance: widget.keyboardAppearance,
|
||||
autofocus: widget.autofocus,
|
||||
textInputAction: widget.textInputAction,
|
||||
autovalidateMode: widget.autovalidateMode,
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildFlagsButton() {
|
||||
return Container(
|
||||
margin: widget.flagsButtonMargin,
|
||||
child: DecoratedBox(
|
||||
decoration: widget.dropdownDecoration,
|
||||
child: InkWell(
|
||||
borderRadius: widget.dropdownDecoration.borderRadius as BorderRadius?,
|
||||
onTap: widget.enabled ? _changeCountry : null,
|
||||
child: Padding(
|
||||
padding: widget.flagsButtonPadding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
if (widget.enabled &&
|
||||
widget.showDropdownIcon &&
|
||||
widget.dropdownIconPosition == IconPosition.leading) ...[
|
||||
widget.dropdownIcon,
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (widget.showCountryFlag) ...[
|
||||
kIsWeb
|
||||
? Image.asset(
|
||||
'assets/flags/${_selectedCountry.code.toLowerCase()}.png',
|
||||
package: 'intl_phone_field',
|
||||
width: 32,
|
||||
)
|
||||
: Text(
|
||||
_selectedCountry.flag,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
FittedBox(
|
||||
child: Text(
|
||||
'+${_selectedCountry.dialCode}',
|
||||
style: widget.dropdownTextStyle,
|
||||
),
|
||||
),
|
||||
if (widget.enabled &&
|
||||
widget.showDropdownIcon &&
|
||||
widget.dropdownIconPosition == IconPosition.trailing) ...[
|
||||
const SizedBox(width: 4),
|
||||
widget.dropdownIcon,
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum IconPosition {
|
||||
leading,
|
||||
trailing,
|
||||
}
|
||||
79
lib/controller/local/phone_intel/phone_number.dart
Normal file
79
lib/controller/local/phone_intel/phone_number.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'countries.dart';
|
||||
|
||||
class NumberTooLongException implements Exception {}
|
||||
|
||||
class NumberTooShortException implements Exception {}
|
||||
|
||||
class InvalidCharactersException implements Exception {}
|
||||
|
||||
class PhoneNumber {
|
||||
String countryISOCode;
|
||||
String countryCode;
|
||||
String number;
|
||||
|
||||
PhoneNumber({
|
||||
required this.countryISOCode,
|
||||
required this.countryCode,
|
||||
required this.number,
|
||||
});
|
||||
|
||||
factory PhoneNumber.fromCompleteNumber({required String completeNumber}) {
|
||||
if (completeNumber == "") {
|
||||
return PhoneNumber(countryISOCode: "", countryCode: "", number: "");
|
||||
}
|
||||
|
||||
try {
|
||||
Country country = getCountry(completeNumber);
|
||||
String number;
|
||||
if (completeNumber.startsWith('+')) {
|
||||
number = completeNumber.substring(1 + country.dialCode.length + country.regionCode.length);
|
||||
} else {
|
||||
number = completeNumber.substring(country.dialCode.length + country.regionCode.length);
|
||||
}
|
||||
return PhoneNumber(
|
||||
countryISOCode: country.code, countryCode: country.dialCode + country.regionCode, number: number);
|
||||
} on InvalidCharactersException {
|
||||
rethrow;
|
||||
// ignore: unused_catch_clause
|
||||
} on Exception catch (e) {
|
||||
return PhoneNumber(countryISOCode: "", countryCode: "", number: "");
|
||||
}
|
||||
}
|
||||
|
||||
bool isValidNumber() {
|
||||
Country country = getCountry(completeNumber);
|
||||
if (number.length < country.minLength) {
|
||||
throw NumberTooShortException();
|
||||
}
|
||||
|
||||
if (number.length > country.maxLength) {
|
||||
throw NumberTooLongException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String get completeNumber {
|
||||
return countryCode + number;
|
||||
}
|
||||
|
||||
static Country getCountry(String phoneNumber) {
|
||||
if (phoneNumber == "") {
|
||||
throw NumberTooShortException();
|
||||
}
|
||||
|
||||
final validPhoneNumber = RegExp(r'^[+0-9]*[0-9]*$');
|
||||
|
||||
if (!validPhoneNumber.hasMatch(phoneNumber)) {
|
||||
throw InvalidCharactersException();
|
||||
}
|
||||
|
||||
if (phoneNumber.startsWith('+')) {
|
||||
return countries
|
||||
.firstWhere((country) => phoneNumber.substring(1).startsWith(country.dialCode + country.regionCode));
|
||||
}
|
||||
return countries.firstWhere((country) => phoneNumber.startsWith(country.dialCode + country.regionCode));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'PhoneNumber(countryISOCode: $countryISOCode, countryCode: $countryCode, number: $number)';
|
||||
}
|
||||
Reference in New Issue
Block a user