added country_code_picker package to link phone feature

This commit is contained in:
AlcalaJulian
2025-12-04 17:32:42 +01:00
parent 6b3776f618
commit ad10ad3b59
10 changed files with 221 additions and 88 deletions

View File

@@ -168,6 +168,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
country_code_picker:
dependency: transitive
description:
name: country_code_picker
sha256: f0411f4833b6f98e8b7215f4fa3813bcc88e50f13925f70a170dbd36e3e447f5
url: "https://pub.dev"
source: hosted
version: "3.4.1"
coverage:
dependency: transitive
description:
@@ -214,6 +222,14 @@ packages:
relative: true
source: path
version: "0.0.1"
diacritic:
dependency: transitive
description:
name: diacritic
sha256: "12981945ec38931748836cd76f2b38773118d0baef3c68404bdfde9566147876"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
dio:
dependency: transitive
description:
@@ -432,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.2"
intl_phone_field_v2:
dependency: transitive
description:
name: intl_phone_field_v2
sha256: b1e5077e31cc8705639a69b2e0410a8ecc858c3e518726d99b378b6c35adfefb
url: "https://pub.dev"
source: hosted
version: "4.0.5"
io:
dependency: transitive
description:

View File

@@ -1,4 +1,5 @@
import 'package:auth/src/features/link_phone/presentation/link_phone_view_model.dart';
import 'package:design_system/src/dropdowns/country_prefix_picker.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -10,13 +11,11 @@ class LinkPhoneScreen extends ConsumerWidget {
const LinkPhoneScreen({super.key, required this.navigationContract});
void _onCountryChanged(int? value) {}
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewModel = ref.watch(linkPhoneViewModelProvider.notifier);
final viewModel = ref.read(linkPhoneViewModelProvider.notifier);
final viewState = ref.watch(linkPhoneViewModelProvider);
return Scaffold(
@@ -56,19 +55,17 @@ class LinkPhoneScreen extends ConsumerWidget {
Row(
spacing: 10,
children: [
CustomDropdown(
value: 0,
items: const [
Icon(Icons.outlined_flag),
Icon(Icons.outlined_flag),
Icon(Icons.outlined_flag),
],
onChanged: _onCountryChanged,
width: 80,
CountryPrefixPicker(
initialCountryCode: viewState.dialCode,
onChanged: (country) {
viewModel.updateDialCode(
country.dialCode ?? viewState.dialCode,
);
},
),
Expanded(
child: CustomTextField(
// controller: viewModel.phoneNumberController,
controller: viewModel.phoneNumberController,
hint: context.translate(I18n.phoneNumber),
numeric: true,
),
@@ -80,11 +77,10 @@ class LinkPhoneScreen extends ConsumerWidget {
const SizedBox(height: 16),
if (viewState.errorMessage?.isNotEmpty ?? false) ...[
if (viewState.errorMessage.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
// context.translate(viewState.errorMessage
'',
viewState.errorMessage,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
@@ -99,7 +95,7 @@ class LinkPhoneScreen extends ConsumerWidget {
onPressed: () async {
await viewModel.requestCode();
final updatedState = ref.read(linkPhoneViewModelProvider);
if (updatedState.errorMessage!.isEmpty) {
if (updatedState.errorMessage.isEmpty) {
navigationContract.pushTo(AppRoutes.phoneCode);
}
},

View File

@@ -17,30 +17,42 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
@override
LinkPhoneViewState build() {
_linkPhoneUseCase = ref.read(linkPhoneUseCaseProvider);
phoneNumberController = TextEditingController();
phoneNumberController.addListener(_onPhoneNumberChanged);
ref.onDispose(disposeControllers);
return const LinkPhoneViewState();
}
void _onPhoneNumberChanged() {
final raw = phoneNumberController.text;
state = state.copyWith(phoneNumber: raw, errorMessage: '');
}
Future<void> requestCode() async {
final phone = phoneNumberController.text.trim();
void updateDialCode(String dialCode) {
state = state.copyWith(dialCode: dialCode, errorMessage: '');
}
if (phone.isEmpty) {
Future<void> requestCode() async {
final trimmedNumber = state.phoneNumber.trim();
if (trimmedNumber.isEmpty) {
state = state.copyWith(errorMessage: 'El teléfono no puede estar vacío');
return;
}
state = state.copyWith(isLoading: true, errorMessage: '');
final fullPhone = '${state.dialCode}$trimmedNumber';
state = state.copyWith(
isLoading: true,
errorMessage: '',
codeRequested: false,
);
try {
await _linkPhoneUseCase.requestCode(phone: phone);
await _linkPhoneUseCase.requestCode(phone: fullPhone);
if (!ref.mounted) return;
state = state.copyWith(
@@ -50,7 +62,12 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: e.toString());
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
codeRequested: false,
);
}
}

View File

@@ -6,7 +6,8 @@ part 'link_phone_view_state.freezed.dart';
abstract class LinkPhoneViewState with _$LinkPhoneViewState {
const factory LinkPhoneViewState({
@Default('') String phoneNumber,
String? errorMessage,
@Default('+34') String dialCode,
@Default('') String errorMessage,
@Default(false) bool isLoading,
@Default(false) bool codeRequested,
}) = _LinkPhoneViewState;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LinkPhoneViewState {
String get phoneNumber; String? get errorMessage; bool get isLoading; bool get codeRequested;
String get phoneNumber; String get dialCode; String get errorMessage; bool get isLoading; bool get codeRequested;
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LinkPhoneViewStateCopyWith<LinkPhoneViewState> get copyWith => _$LinkPhoneViewS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LinkPhoneViewStateCopyWith<$Res> {
factory $LinkPhoneViewStateCopyWith(LinkPhoneViewState value, $Res Function(LinkPhoneViewState) _then) = _$LinkPhoneViewStateCopyWithImpl;
@useResult
$Res call({
String phoneNumber, String? errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
});
@@ -62,11 +62,12 @@ class _$LinkPhoneViewStateCopyWithImpl<$Res>
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? phoneNumber = null,Object? errorMessage = freezed,Object? isLoading = null,Object? codeRequested = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,}) {
return _then(_self.copyWith(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,codeRequested: null == codeRequested ? _self.codeRequested : codeRequested // ignore: cast_nullable_to_non_nullable
as bool,
));
@@ -153,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String phoneNumber, String? errorMessage, bool isLoading, bool codeRequested)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return orElse();
}
@@ -174,10 +175,10 @@ return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeR
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String phoneNumber, String? errorMessage, bool isLoading, bool codeRequested) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested) $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState():
return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
throw StateError('Unexpected subclass');
}
@@ -194,10 +195,10 @@ return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeR
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String phoneNumber, String? errorMessage, bool isLoading, bool codeRequested)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested)? $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return null;
}
@@ -209,11 +210,12 @@ return $default(_that.phoneNumber,_that.errorMessage,_that.isLoading,_that.codeR
class _LinkPhoneViewState implements LinkPhoneViewState {
const _LinkPhoneViewState({this.phoneNumber = '', this.errorMessage, this.isLoading = false, this.codeRequested = false});
const _LinkPhoneViewState({this.phoneNumber = '', this.dialCode = '+34', this.errorMessage = '', this.isLoading = false, this.codeRequested = false});
@override@JsonKey() final String phoneNumber;
@override final String? errorMessage;
@override@JsonKey() final String dialCode;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool codeRequested;
@@ -227,16 +229,16 @@ _$LinkPhoneViewStateCopyWith<_LinkPhoneViewState> get copyWith => __$LinkPhoneVi
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LinkPhoneViewState&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.dialCode, dialCode) || other.dialCode == dialCode)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.codeRequested, codeRequested) || other.codeRequested == codeRequested));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
}
@@ -247,7 +249,7 @@ abstract mixin class _$LinkPhoneViewStateCopyWith<$Res> implements $LinkPhoneVie
factory _$LinkPhoneViewStateCopyWith(_LinkPhoneViewState value, $Res Function(_LinkPhoneViewState) _then) = __$LinkPhoneViewStateCopyWithImpl;
@override @useResult
$Res call({
String phoneNumber, String? errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
});
@@ -264,11 +266,12 @@ class __$LinkPhoneViewStateCopyWithImpl<$Res>
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? phoneNumber = null,Object? errorMessage = freezed,Object? isLoading = null,Object? codeRequested = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? phoneNumber = null,Object? dialCode = null,Object? errorMessage = null,Object? isLoading = null,Object? codeRequested = null,}) {
return _then(_LinkPhoneViewState(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
as String,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as String,dialCode: null == dialCode ? _self.dialCode : dialCode // ignore: cast_nullable_to_non_nullable
as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,codeRequested: null == codeRequested ? _self.codeRequested : codeRequested // ignore: cast_nullable_to_non_nullable
as bool,
));

View File

@@ -32,6 +32,7 @@ dependencies:
freezed_annotation: ^3.1.0
freezed: ^3.2.3
dio: ^5.9.0
country_code_picker: ^3.4.1
dev_dependencies:
flutter_test:

View File

@@ -9,4 +9,5 @@ export 'src/snackbars/snackbar.dart';
export 'src/buttons/primary_button.dart';
export 'src/buttons/secondary_button.dart';
export 'src/buttons/custom_text_button.dart';
export 'src/dropdowns/dropdown.dart';
export 'src/dropdowns/dropdown.dart';
export 'src/dropdowns/country_prefix_picker.dart';

View File

@@ -0,0 +1,79 @@
import 'package:country_code_picker/country_code_picker.dart';
import 'package:flutter/material.dart';
class CountryPrefixPicker extends StatelessWidget {
const CountryPrefixPicker({
super.key,
required this.onChanged,
this.initialCountryCode = '+34',
this.radius = 12,
this.width = 90,
this.height = 55,
this.borderColor = const Color(0xFF4B4B4B),
this.backgroundColor = Colors.white,
});
final ValueChanged<CountryCode> onChanged;
final String initialCountryCode;
final double radius;
final double width;
final double height;
final Color borderColor;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: CountryCodePicker(
onChanged: onChanged,
initialSelection: initialCountryCode,
showFlag: false,
showDropDownButton: false,
hideMainText: true,
padding: EdgeInsets.zero,
builder: (CountryCode? country) {
if (country == null) {
return const SizedBox.shrink();
}
return InputDecorator(
decoration: InputDecoration(
isDense: false,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
filled: true,
fillColor: backgroundColor,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(radius)),
borderSide: BorderSide(color: borderColor),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(radius)),
borderSide: BorderSide(color: borderColor),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (country.flagUri != null)
Image.asset(
country.flagUri!,
package: 'country_code_picker',
width: 24,
height: 24,
fit: BoxFit.cover,
),
const Icon(Icons.arrow_drop_down, size: 24),
],
),
);
},
),
);
}
}

View File

@@ -1,18 +1,17 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CustomTextField extends StatefulWidget{
bool? showPassword;
class CustomTextField extends StatefulWidget {
final bool? showPassword;
final bool numeric;
final String hint;
final String label;
final int? lines;
final ValueChanged<String>? onChanged;
final int? length;
final TextEditingController? controller;
CustomTextField({
const CustomTextField({
super.key,
this.showPassword,
this.numeric = false,
@@ -21,63 +20,74 @@ class CustomTextField extends StatefulWidget{
this.lines,
this.length,
this.onChanged,
this.controller,
});
@override
State<CustomTextField> createState() => CustomTextFieldState();
}
class CustomTextFieldState extends State<CustomTextField>{
class CustomTextFieldState extends State<CustomTextField> {
late bool _showPassword;
@override
void initState() {
super.initState();
_showPassword = widget.showPassword ?? true;
}
@override
Widget build(BuildContext context) {
return Column(
spacing: 8,
children: [
?widget.label == '' ? null : Align(
alignment: Alignment.bottomLeft,
child: Text(
widget.label,
style: TextStyle(fontSize: 14, letterSpacing: 0),
)
),
if (widget.label.isNotEmpty)
Align(
alignment: Alignment.bottomLeft,
child: Text(
widget.label,
style: const TextStyle(fontSize: 14, letterSpacing: 0),
),
),
TextFormField(
keyboardType: widget.numeric? TextInputType.number : TextInputType.text,
obscureText: !(widget.showPassword ?? true),
enableSuggestions: widget.showPassword ?? true,
autocorrect: !(widget.showPassword ?? false),
style: TextStyle(color: Color(0xFF4B4B4B)),
inputFormatters: widget.numeric? [
FilteringTextInputFormatter.digitsOnly
] : [],
controller: widget.controller,
keyboardType: widget.numeric
? TextInputType.number
: TextInputType.text,
obscureText: !_showPassword,
enableSuggestions: _showPassword,
autocorrect: !_showPassword,
style: const TextStyle(color: Color(0xFF4B4B4B)),
inputFormatters: widget.numeric
? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly]
: const <TextInputFormatter>[],
decoration: InputDecoration(
counterText: "",
hintText: widget.hint,
//labelText: widget.label,
//floatingLabelBehavior: FloatingLabelBehavior.always,
border: OutlineInputBorder(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: Color(0xFF4B4B4B)),
gapPadding: 16
gapPadding: 16,
),
suffixIcon: widget.showPassword!=null ? IconButton(
icon: Icon(widget.showPassword!
? Icons.visibility_off
: Icons.visibility),
onPressed: () {
setState(() {
widget.showPassword = !widget.showPassword!;
});
},
) : null,
suffixIcon: widget.showPassword != null
? IconButton(
icon: Icon(
_showPassword ? Icons.visibility_off : Icons.visibility,
),
onPressed: () {
setState(() {
_showPassword = !_showPassword;
});
},
)
: null,
),
minLines: widget.lines ?? 1,
maxLines: widget.lines ?? 1,
maxLength: widget.length,
onChanged: widget.onChanged ?? (_)=>{},
)
onChanged: widget.onChanged,
),
],
);
}
}
}

View File

@@ -16,6 +16,7 @@ dependencies:
path: ../utils
flutter_riverpod: ^3.0.3
get_it: ^9.0.5
country_code_picker: ^3.4.1
fonts:
path: ../../packages/fonts