added linkPhone use cases, auth repository providers and data folder

This commit is contained in:
AlcalaJulian
2025-12-09 10:08:36 +01:00
parent 760e94ffe9
commit 0f30c7f422
34 changed files with 470 additions and 183 deletions

View File

@@ -1,7 +1,7 @@
export 'src/features/device_sign_up/link_watch/create_profile_screen.dart';
export 'src/features/onboarding/onboarding_builder.dart';
export 'src/features/link_phone/link_phone_builder.dart';
export 'src/features/login/phone_code_builder.dart';
export 'src/features/link_phone/presentation/request_phone/request_link_phone_builder.dart';
export 'src/features/link_phone/presentation/verify_code/verify_link_phone_code_builder.dart';
export 'src/features/login/login_builder.dart';
export 'src/features/recover_password/recover_password_builder.dart';
export 'src/features/device_sign_up/device_signup_builder.dart';

View File

@@ -16,7 +16,10 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
body: <String, dynamic>{'phone': phone},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error al solicitar el código');
throw _mapDioError(
error,
defaultMessage: error.response?.data ?? 'Error to request phone code',
);
}
}
@@ -31,7 +34,7 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
body: <String, dynamic>{'phone': phone, 'code': code},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error al verificar el código');
throw _mapDioError(error, defaultMessage: 'Error in verification code');
}
}

View File

@@ -7,12 +7,14 @@ class LinkPhoneUseCaseImpl implements LinkPhoneUseCase {
final AuthRepository _repository;
@override
Future<void> requestCode({required String phone}) {
return _repository.requestPhoneCode(phone: phone);
Future<void> requestCode({required String phone}) async {
// return _repository.requestPhoneCode(phone: phone);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
@override
Future<void> verifyCode({required String phone, required String code}) {
return _repository.verifyPhoneCode(phone: phone, code: code);
Future<void> verifyCode({required String phone, required String code}) async {
// return _repository.verifyPhoneCode(phone: phone, code: code);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}

View File

@@ -1,18 +1,18 @@
import 'package:auth/src/features/link_phone/presentation/link_phone_screen.dart';
import 'package:auth/src/features/link_phone/presentation/request_phone/request_link_phone_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class LinkPhoneBuilder {
const LinkPhoneBuilder();
class RequestLinkPhoneBuilder {
const RequestLinkPhoneBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: LinkPhoneScreen(navigationContract: navigationContract),
child: RequestLinkPhoneScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -1,15 +1,14 @@
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:auth/src/features/link_phone/presentation/state/link_phone_view_model.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
class LinkPhoneScreen extends ConsumerWidget {
class RequestLinkPhoneScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const LinkPhoneScreen({super.key, required this.navigationContract});
const RequestLinkPhoneScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -56,6 +55,7 @@ class LinkPhoneScreen extends ConsumerWidget {
spacing: 10,
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
initialCountryCode: viewState.dialCode,
onChanged: (country) {
viewModel.updateDialCode(

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:auth/src/features/link_phone/domain/use_cases/link_phone_use_case.dart';
import 'package:auth/src/features/link_phone/presentation/link_phone_view_state.dart';
import 'package:auth/src/features/link_phone/presentation/state/link_phone_view_state.dart';
final linkPhoneViewModelProvider =
NotifierProvider.autoDispose<LinkPhoneViewModel, LinkPhoneViewState>(
@@ -13,6 +13,7 @@ final linkPhoneViewModelProvider =
class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
late final LinkPhoneUseCase _linkPhoneUseCase;
late final TextEditingController phoneNumberController;
late final TextEditingController codeController;
@override
LinkPhoneViewState build() {
@@ -21,6 +22,8 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
phoneNumberController = TextEditingController();
phoneNumberController.addListener(_onPhoneNumberChanged);
codeController = TextEditingController();
ref.onDispose(disposeControllers);
return const LinkPhoneViewState();
@@ -28,18 +31,34 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
void _onPhoneNumberChanged() {
final raw = phoneNumberController.text;
state = state.copyWith(phoneNumber: raw, errorMessage: '');
state = state.copyWith(
phoneNumber: raw,
errorMessage: '',
codeVerified: false,
);
}
void updateDialCode(String dialCode) {
state = state.copyWith(dialCode: dialCode, errorMessage: '');
state = state.copyWith(
dialCode: dialCode,
errorMessage: '',
codeVerified: false,
);
}
void updateCode(String code) {
codeController.text = code;
state = state.copyWith(errorMessage: '', codeVerified: false);
}
Future<void> requestCode() async {
final trimmedNumber = state.phoneNumber.trim();
if (trimmedNumber.isEmpty) {
state = state.copyWith(errorMessage: 'El teléfono no puede estar vacío');
state = state.copyWith(
errorMessage: 'errorMessagePhoneIsEmpty',
codeVerified: false,
);
return;
}
@@ -49,6 +68,7 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
isLoading: true,
errorMessage: '',
codeRequested: false,
codeVerified: false,
);
try {
@@ -67,6 +87,55 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
isLoading: false,
errorMessage: e.toString(),
codeRequested: false,
codeVerified: false,
);
}
}
Future<void> verifyCode() async {
final dialCode = state.dialCode;
final phoneNumber = state.phoneNumber.trim();
final code = codeController.text.trim();
final fullPhone = '$dialCode$phoneNumber';
if (phoneNumber.isEmpty) {
state = state.copyWith(
errorMessage: 'errorMessagePhoneIsEmpty',
codeVerified: false,
);
return;
}
if (code.isEmpty) {
state = state.copyWith(
errorMessage: 'errorMessageCodeIsEmpty',
codeVerified: false,
);
return;
}
state = state.copyWith(
isLoading: true,
errorMessage: '',
codeVerified: false,
);
try {
await _linkPhoneUseCase.verifyCode(phone: fullPhone, code: code);
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: '',
codeVerified: true,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
codeVerified: false,
);
}
}
@@ -74,5 +143,6 @@ class LinkPhoneViewModel extends Notifier<LinkPhoneViewState> {
void disposeControllers() {
phoneNumberController.removeListener(_onPhoneNumberChanged);
phoneNumberController.dispose();
codeController.dispose();
}
}

View File

@@ -10,5 +10,6 @@ abstract class LinkPhoneViewState with _$LinkPhoneViewState {
@Default('') String errorMessage,
@Default(false) bool isLoading,
@Default(false) bool codeRequested,
@Default(false) bool codeVerified,
}) = _LinkPhoneViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LinkPhoneViewState {
String get phoneNumber; String get dialCode; String get errorMessage; bool get isLoading; bool get codeRequested;
String get phoneNumber; String get dialCode; String get errorMessage; bool get isLoading; bool get codeRequested; bool get codeVerified;
/// 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.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));
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)&&(identical(other.codeVerified, codeVerified) || other.codeVerified == codeVerified));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested,codeVerified);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested, codeVerified: $codeVerified)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LinkPhoneViewStateCopyWith<$Res> {
factory $LinkPhoneViewStateCopyWith(LinkPhoneViewState value, $Res Function(LinkPhoneViewState) _then) = _$LinkPhoneViewStateCopyWithImpl;
@useResult
$Res call({
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified
});
@@ -62,13 +62,14 @@ 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? dialCode = null,Object? errorMessage = null,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,Object? codeVerified = null,}) {
return _then(_self.copyWith(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // 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,codeVerified: null == codeVerified ? _self.codeVerified : codeVerified // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@@ -154,10 +155,10 @@ return $default(_that);case _:
/// }
/// ```
@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;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
return orElse();
}
@@ -175,10 +176,10 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String phoneNumber, String dialCode, 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, bool codeVerified) $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState():
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +196,10 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String phoneNumber, String dialCode, 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, bool codeVerified)? $default,) {final _that = this;
switch (_that) {
case _LinkPhoneViewState() when $default != null:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested);case _:
return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoading,_that.codeRequested,_that.codeVerified);case _:
return null;
}
@@ -210,7 +211,7 @@ return $default(_that.phoneNumber,_that.dialCode,_that.errorMessage,_that.isLoad
class _LinkPhoneViewState implements LinkPhoneViewState {
const _LinkPhoneViewState({this.phoneNumber = '', this.dialCode = '+34', this.errorMessage = '', this.isLoading = false, this.codeRequested = false});
const _LinkPhoneViewState({this.phoneNumber = '', this.dialCode = '+34', this.errorMessage = '', this.isLoading = false, this.codeRequested = false, this.codeVerified = false});
@override@JsonKey() final String phoneNumber;
@@ -218,6 +219,7 @@ class _LinkPhoneViewState implements LinkPhoneViewState {
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool codeRequested;
@override@JsonKey() final bool codeVerified;
/// Create a copy of LinkPhoneViewState
/// with the given fields replaced by the non-null parameter values.
@@ -229,16 +231,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.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));
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)&&(identical(other.codeVerified, codeVerified) || other.codeVerified == codeVerified));
}
@override
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested);
int get hashCode => Object.hash(runtimeType,phoneNumber,dialCode,errorMessage,isLoading,codeRequested,codeVerified);
@override
String toString() {
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested)';
return 'LinkPhoneViewState(phoneNumber: $phoneNumber, dialCode: $dialCode, errorMessage: $errorMessage, isLoading: $isLoading, codeRequested: $codeRequested, codeVerified: $codeVerified)';
}
@@ -249,7 +251,7 @@ abstract mixin class _$LinkPhoneViewStateCopyWith<$Res> implements $LinkPhoneVie
factory _$LinkPhoneViewStateCopyWith(_LinkPhoneViewState value, $Res Function(_LinkPhoneViewState) _then) = __$LinkPhoneViewStateCopyWithImpl;
@override @useResult
$Res call({
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested
String phoneNumber, String dialCode, String errorMessage, bool isLoading, bool codeRequested, bool codeVerified
});
@@ -266,13 +268,14 @@ 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? dialCode = null,Object? errorMessage = null,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,Object? codeVerified = null,}) {
return _then(_LinkPhoneViewState(
phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // 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,codeVerified: null == codeVerified ? _self.codeVerified : codeVerified // ignore: cast_nullable_to_non_nullable
as bool,
));
}

View File

@@ -1,18 +1,18 @@
import 'package:auth/src/features/login/presentation/phone_code_screen.dart';
import 'package:auth/src/features/link_phone/presentation/verify_code/verify_link_phone_code_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class PhoneCodeBuilder {
const PhoneCodeBuilder();
class VerifyLinkPhoneCodeBuilder {
const VerifyLinkPhoneCodeBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: PhoneCodeScreen(navigationContract: navigationContract),
child: VerifyLinkPhoneCodeScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:auth/src/features/link_phone/presentation/state/link_phone_view_model.dart';
import 'package:auth/src/features/link_phone/presentation/widgets/link_phone_code_input.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
class VerifyLinkPhoneCodeScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const VerifyLinkPhoneCodeScreen({
super.key,
required this.navigationContract,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final viewModel = ref.read(linkPhoneViewModelProvider.notifier);
final viewState = ref.watch(linkPhoneViewModelProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 48,
children: [
Column(
children: [
Text(
context.translate(I18n.connect),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
const SizedBox(height: 24),
Text.rich(
TextSpan(
text: context.translate(I18n.verificationCodeSentTo),
children: [
TextSpan(
text: '${viewState.dialCode}${viewState.phoneNumber}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
Text(
context.translate(I18n.enterCodeHere),
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
LinkPhoneCodeInput(
length: 6,
onCodeChanged: viewModel.updateCode,
),
if (viewState.errorMessage.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
viewState.errorMessage,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 12,
),
),
],
],
),
Column(
children: [
PrimaryButton(
onPressed: () async {
await viewModel.verifyCode();
final updatedState = ref.read(linkPhoneViewModelProvider);
if (updatedState.codeVerified) {
navigationContract.pushTo(AppRoutes.login);
}
},
text: context.translate(I18n.enter),
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
const SizedBox(height: 24),
Text(
context.translate(I18n.didNotReceiveIt),
style: TextStyle(
fontSize: 18,
letterSpacing: 0,
height: 1.5,
),
),
const SizedBox(height: 8),
CustomTextButton(
onPressed: () => navigationContract.goBack(),
text: context.translate(I18n.tryAgain),
size: 18,
weight: FontWeight.w500,
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LinkPhoneCodeInput extends StatefulWidget {
const LinkPhoneCodeInput({
super.key,
this.length = 6,
required this.onCodeChanged,
});
final int length;
final ValueChanged<String> onCodeChanged;
@override
State<LinkPhoneCodeInput> createState() => _LinkPhoneCodeInputState();
}
class _LinkPhoneCodeInputState extends State<LinkPhoneCodeInput> {
late final List<TextEditingController> _controllers;
late final List<FocusNode> _focusNodes;
@override
void initState() {
super.initState();
_controllers = List<TextEditingController>.generate(
widget.length,
(_) => TextEditingController(),
);
_focusNodes = List<FocusNode>.generate(widget.length, (_) => FocusNode());
}
@override
void dispose() {
for (final controller in _controllers) {
controller.dispose();
}
for (final node in _focusNodes) {
node.dispose();
}
super.dispose();
}
void _onDigitChanged(int index, String value) {
if (value.length > 1) {
final single = value.characters.last;
_controllers[index].text = single;
_controllers[index].selection = TextSelection.fromPosition(
TextPosition(offset: single.length),
);
}
if (value.isNotEmpty && index < widget.length - 1) {
_focusNodes[index + 1].requestFocus();
} else if (value.isEmpty && index > 0) {
_focusNodes[index - 1].requestFocus();
}
final code = _controllers.map((c) => c.text).join();
widget.onCodeChanged(code);
}
@override
Widget build(BuildContext context) {
return Row(
spacing: 8,
children: List<Widget>.generate(widget.length, (int i) {
return Expanded(
child: TextField(
controller: _controllers[i],
focusNode: _focusNodes[i],
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: '0',
counterText: '',
border: OutlineInputBorder(),
),
maxLength: 1,
onChanged: (value) => _onDigitChanged(i, value),
),
);
}),
);
}
}

View File

@@ -1,119 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PhoneCodeScreen extends ConsumerWidget {
final NavigationContract navigationContract;
PhoneCodeScreen({super.key, required this.navigationContract});
final focusNodes = List<FocusNode>.generate(6, (int i) {
return FocusNode();
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Expanded(
child: Center(
child: Column(
spacing: 48,
children: [
Spacer(flex: 8),
Column(
spacing: 24,
children: [
Text(
"Conéctate",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Text.rich(
TextSpan(
text: "Hemos enviado el código al ",
children: [
TextSpan(
// text: widget.phone,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text("Introduce el código aquí"),
Row(
spacing: 8,
children: List<Widget>.generate(6, (int i) {
return Expanded(
child: TextField(
focusNode: focusNodes[i],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: "0",
counterText: "",
border: OutlineInputBorder(),
),
maxLength: 1,
onChanged: (String value) => {
value != ""
? focusNodes[i + 1].requestFocus()
: focusNodes[i - 1].requestFocus(),
},
),
);
}),
),
],
),
Column(
spacing: 24,
children: [
PrimaryButton(
onPressed: () => {
navigationContract.pushTo(AppRoutes.login),
},
text: "Entrar",
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
Column(
spacing: 8,
children: [
Text(
"¿No lo has recibido?",
style: TextStyle(
fontSize: 18,
letterSpacing: 0,
height: 1.5,
),
),
CustomTextButton(
onPressed: () => {},
text: "Volver a intentarlo",
size: 18,
weight: FontWeight.w500,
),
],
),
],
),
Spacer(flex: 10),
],
),
),
),
),
);
}
}