added country_code_picker package to link phone feature #8

Merged
Raul merged 2 commits from feature/auth-link-phone into develop 2025-12-04 17:22:11 +00:00
10 changed files with 174 additions and 49 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

@@ -9,6 +9,7 @@ class CustomTextField extends StatefulWidget {
final int? lines;
final ValueChanged<String>? onChanged;
final int? length;
final TextEditingController? controller;
const CustomTextField({
super.key,
@@ -19,6 +20,7 @@ class CustomTextField extends StatefulWidget {
this.lines,
this.length,
this.onChanged,
this.controller,
});
@override

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