Merge remote-tracking branch 'origin/auth-login-and-2fa-login' into auth-recover-password

# Conflicts:
#	modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart
#	modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart
#	modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart
#	modules/auth/lib/src/core/domain/repositories/auth_repository.dart
#	modules/auth/lib/src/features/recover_password/presentation/new_password_screen.dart
#	modules/auth/lib/src/features/recover_password/presentation/restore_password_screen.dart
#	packages/design_system/lib/src/inputs/textfields.dart
#	packages/sf_localizations/assets/l10n/de.json
#	packages/sf_localizations/assets/l10n/en.json
#	packages/sf_localizations/assets/l10n/es.json
#	packages/sf_localizations/assets/l10n/fr.json
#	packages/sf_localizations/assets/l10n/it.json
#	packages/sf_localizations/assets/l10n/pt.json
#	packages/sf_localizations/lib/src/generated/i18n.dart
This commit is contained in:
2025-12-19 12:13:41 +01:00
32 changed files with 1585 additions and 815 deletions

View File

@@ -3,6 +3,10 @@ abstract class AuthRemoteDatasource {
Future<void> verifyPhoneCode({required String phone, required String code});
Future<String> login({required String email, required String password});
Future<void> twoFALogin({required String token, required String code});
Future<String> requestPasswordReset({String? phone, String? email});
Future<void> recoverPassword({required newPassword, required token});

View File

@@ -54,6 +54,35 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
return Exception(message);
}
@override
Future<String> login({
required String email,
required String password,
}) async {
try {
final response = await _repository.post<Map<String, dynamic>>(
'/auth/login',
body: <String, dynamic>{'email': email, 'password': password},
);
final token = response.data!['token'];
return token;
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error in login');
}
}
@override
Future<void> twoFALogin({required String token, required String code}) async {
try {
await _repository.post<Map<String, dynamic>>(
'/auth/login',
body: <String, dynamic>{'token': token, 'password': code},
);
} on DioException catch (error) {
throw _mapDioError(error, defaultMessage: 'Error in login');
}
}
@override
Future<String> requestPasswordReset({
String? phone,

View File

@@ -16,6 +16,16 @@ class AuthRepositoryImpl implements AuthRepository {
return _remote.verifyPhoneCode(phone: phone, code: code);
}
@override
Future<String> login({required String email, required String password}) {
return _remote.login(email: email, password: password);
}
@override
Future<void> twoFALogin({required String token, required String code}) {
return _remote.twoFALogin(token: token, code: code);
}
@override
Future<String> requestPasswordReset({String? phone, String? email}) {
return _remote.requestPasswordReset(phone: phone, email: email);

View File

@@ -3,6 +3,10 @@ abstract class AuthRepository {
Future<void> verifyPhoneCode({required String phone, required String code});
Future<String> login({required String email, required String password});
Future<void> twoFALogin({required String token, required String code});
Future<String> requestPasswordReset({String phone, String email});
Future<void> recoverPassword({required String newPassword, required String token});

View File

@@ -17,16 +17,14 @@ class CreateProfileScreen extends ConsumerWidget {
Text(
"Comienza con un peque; luego podrás agregar más",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0),
),
CustomTextField(
label: "Nombre",
hint: "Nombre",
),
CustomTextField(
label: "Apellidos",
hint: "Apellidos",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
CustomTextField(label: "Nombre", hint: "Nombre"),
CustomTextField(label: "Apellidos", hint: "Apellidos"),
Column(
spacing: 8,
children: [
@@ -42,21 +40,21 @@ class CreateProfileScreen extends ConsumerWidget {
children: [
Expanded(
child: CustomTextField(
numeric: true,
keyboardType: TextInputType.number,
hint: "DD",
length: 2,
),
),
Expanded(
child: CustomTextField(
numeric: true,
keyboardType: TextInputType.number,
hint: "MM",
length: 2,
),
),
Expanded(
child: CustomTextField(
numeric: true,
keyboardType: TextInputType.number,
hint: "AAAA",
length: 4,
),
@@ -77,7 +75,7 @@ class CreateProfileScreen extends ConsumerWidget {
size: 18,
weight: FontWeight.w500,
),
)
),
],
);
}

View File

@@ -67,7 +67,7 @@ class RequestLinkPhoneScreen extends ConsumerWidget {
child: CustomTextField(
controller: viewModel.phoneNumberController,
hint: context.translate(I18n.phoneNumber),
numeric: true,
keyboardType: TextInputType.number,
),
),
],

View File

@@ -0,0 +1,3 @@
abstract class LoginUseCase {
Future<void> login({required String email, required String password});
}

View File

@@ -0,0 +1,13 @@
import 'package:auth/src/core/domain/repositories/auth_repository.dart';
import 'package:auth/src/features/login/domain/login_use_case.dart';
class LoginUseCaseImpl implements LoginUseCase {
LoginUseCaseImpl(this._repository);
final AuthRepository _repository;
@override
Future<void> login({required String email, required String password}) {
return _repository.login(email: email, password: password);
}
}

View File

@@ -1,172 +1,165 @@
import 'package:auth/src/features/login/presentation/loading_google_screen.dart';
import 'package:auth/src/features/sign_up/signup_screen.dart';
import 'package:auth/src/features/login/presentation/state/login_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 LoginScreen extends ConsumerStatefulWidget {
class LoginScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const LoginScreen({super.key, required this.navigationContract});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
bool passwordVisible = false;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final vm = ref.read(loginViewModelProvider.notifier);
final state = ref.watch(loginViewModelProvider);
bool passwordVisible = true;
final content = [
Column(
spacing: 8,
children: [
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
size: 50,
),
Text(
// context.translate(I18n.example)
"¡Te damos la bienvenida!",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
],
),
Column(
spacing: 32,
children: [
Column(
spacing: 24,
children: [
CustomTextField(
hint: "Nombre de usuario",
label: "Nombre de usuario",
),
Column(
spacing: 12,
children: [
CustomTextField(
showPassword: passwordVisible,
label: "Contraseña",
hint: "********",
),
Align(
alignment: Alignment.topLeft,
child: CustomTextButton(
text: "¿Has olvidado la contraseña?",
onPressed: () => widget.navigationContract.pushTo(
AppRoutes.recoverPassword,
),
size: 16,
),
),
],
),
],
),
PrimaryButton(
onPressed: () =>
widget.navigationContract.goTo(AppRoutes.dashboardHome),
text: "Iniciar sesión",
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
Container(
padding: EdgeInsets.only(top: 24),
child: Column(
spacing: 24,
children: [
Stack(
children: [
Divider(endIndent: 74, indent: 74),
Align(
alignment: Alignment.center,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
child: Text("o continúa con"),
),
),
],
),
Row(
spacing: 20,
children: [
Spacer(),
SecondaryButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LoadingGoogleScreen(),
),
),
radius: 16,
padding: 44,
text: "Google",
label: "Google",
),
SecondaryButton(
onPressed: () => {},
radius: 16,
padding: 44,
icon: Icons.apple,
label: "Apple",
),
Spacer(),
],
),
],
),
),
Column(
spacing: 8,
children: [
Text(
"¿No tienes cuenta?",
style: TextStyle(fontSize: 18, letterSpacing: 0),
),
TextButton(
onPressed: () =>
widget.navigationContract.goTo(AppRoutes.signup),
child: Text(
"Crear una ahora",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
],
),
];
Future<void> onSignIn() async {
FocusScope.of(context).unfocus();
final login = await vm.login();
if (login) navigationContract.goTo(AppRoutes.dashboardHome);
}
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Expanded(
child: Center(
child: Container(
margin: EdgeInsets.all(30),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) {
return content[index];
},
separatorBuilder: (BuildContext context, int index) {
return Divider(color: Colors.transparent, height: 48);
},
itemCount: content.length,
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
size: 54,
),
),
Text(
context.translate(I18n.welcome),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 48),
CustomTextField(
hint: context.translate(I18n.username),
label: context.translate(I18n.username),
controller: vm.emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 24),
CustomTextField(
showPassword: state.passwordVisible,
label: context.translate(I18n.password),
hint: "********",
controller: vm.passwordController,
textInputAction: TextInputAction.done,
onSubmitted: (_) => onSignIn(),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.topLeft,
child: CustomTextButton(
text: context.translate(I18n.forgotPassword),
onPressed: state.isLoading
? () {}
: () =>
navigationContract.pushTo(AppRoutes.recoverPassword),
size: 16,
),
),
const SizedBox(height: 30),
PrimaryButton(
onPressed: state.isLoading ? () {} : onSignIn,
text: context.translate(I18n.signIn),
color: theme.getColorFor(ThemeCode.buttonPrimary),
leading: state.isLoading
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: null,
),
const SizedBox(height: 30),
Stack(
children: [
const Divider(endIndent: 74, indent: 74),
Align(
alignment: Alignment.center,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
child: Text(context.translate(I18n.orContinueWith)),
),
),
],
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SecondaryButton(
onPressed: state.isLoading
? () {}
: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const LoadingGoogleScreen(),
),
),
radius: 16,
padding: 44,
text: context.translate(I18n.google),
label: 'Google',
),
const SizedBox(width: 16),
SecondaryButton(
onPressed: state.isLoading ? () {} : () {},
radius: 16,
padding: 44,
icon: Icons.apple,
label: 'Apple',
),
],
),
const SizedBox(height: 30),
Text(
context.translate(I18n.dontHaveAccount),
style: const TextStyle(fontSize: 18, letterSpacing: 0),
),
TextButton(
onPressed: state.isLoading
? null
: () => navigationContract.goTo(AppRoutes.signup),
child: Text(
context.translate(I18n.createOneNow),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
),
);

View File

@@ -0,0 +1,9 @@
import 'package:auth/src/core/providers/auth_repository_provider.dart';
import 'package:auth/src/features/login/domain/login_use_case.dart';
import 'package:auth/src/features/login/domain/login_use_case_impl.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final loginUseCaseProvider = Provider.autoDispose<LoginUseCase>((ref) {
final authRepository = ref.read(authRepositoryProvider);
return LoginUseCaseImpl(authRepository);
});

View File

@@ -0,0 +1,79 @@
import 'package:auth/src/features/login/domain/login_use_case.dart';
import 'package:auth/src/features/login/presentation/providers/login_provider.dart';
import 'package:auth/src/features/login/presentation/state/login_view_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final loginViewModelProvider =
NotifierProvider.autoDispose<LoginViewModel, LoginViewState>(
LoginViewModel.new,
);
class LoginViewModel extends Notifier<LoginViewState> {
late final LoginUseCase _loginUseCase;
late final TextEditingController emailController;
late final TextEditingController passwordController;
@override
LoginViewState build() {
_loginUseCase = ref.read(loginUseCaseProvider);
emailController = TextEditingController();
passwordController = TextEditingController();
emailController.addListener(_onEmailChanged);
passwordController.addListener(_onPasswordChanged);
ref.onDispose(disposeControllers);
return const LoginViewState();
}
void _onEmailChanged() {
if (emailController.text != state.email) {
state = state.copyWith(email: emailController.text, errorMessage: '');
}
}
void _onPasswordChanged() {
if (passwordController.text != state.password) {
state = state.copyWith(
password: passwordController.text,
errorMessage: '',
);
}
}
void togglePasswordVisible() {
state = state.copyWith(passwordVisible: !state.passwordVisible);
}
Future<bool> login() async {
final email = state.email.trim();
final password = state.password.trim();
if (email.isEmpty) {
state = state.copyWith(errorMessage: 'errorMessageIsEmpty');
return false;
}
state = state.copyWith(isLoading: true, errorMessage: '');
try {
await _loginUseCase.login(email: email, password: password);
if (!ref.mounted) return false;
state = state.copyWith(isLoading: false);
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(isLoading: false, errorMessage: e.toString());
return false;
}
}
void disposeControllers() {
emailController.removeListener(_onEmailChanged);
passwordController.removeListener(_onPasswordChanged);
emailController.dispose();
passwordController.dispose();
}
}

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_view_state.freezed.dart';
@freezed
abstract class LoginViewState with _$LoginViewState {
const factory LoginViewState({
@Default('') String email,
@Default('') String password,
@Default(false) bool passwordVisible,
@Default('') String errorMessage,
@Default(false) bool isLoading,
}) = _LoginViewState;
}

View File

@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'login_view_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LoginViewState {
String get email; String get password; bool get passwordVisible; String get errorMessage; bool get isLoading;
/// Create a copy of LoginViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LoginViewStateCopyWith<LoginViewState> get copyWith => _$LoginViewStateCopyWithImpl<LoginViewState>(this as LoginViewState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoginViewState&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading));
}
@override
int get hashCode => Object.hash(runtimeType,email,password,passwordVisible,errorMessage,isLoading);
@override
String toString() {
return 'LoginViewState(email: $email, password: $password, passwordVisible: $passwordVisible, errorMessage: $errorMessage, isLoading: $isLoading)';
}
}
/// @nodoc
abstract mixin class $LoginViewStateCopyWith<$Res> {
factory $LoginViewStateCopyWith(LoginViewState value, $Res Function(LoginViewState) _then) = _$LoginViewStateCopyWithImpl;
@useResult
$Res call({
String email, String password, bool passwordVisible, String errorMessage, bool isLoading
});
}
/// @nodoc
class _$LoginViewStateCopyWithImpl<$Res>
implements $LoginViewStateCopyWith<$Res> {
_$LoginViewStateCopyWithImpl(this._self, this._then);
final LoginViewState _self;
final $Res Function(LoginViewState) _then;
/// Create a copy of LoginViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? email = null,Object? password = null,Object? passwordVisible = null,Object? errorMessage = null,Object? isLoading = null,}) {
return _then(_self.copyWith(
email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,passwordVisible: null == passwordVisible ? _self.passwordVisible : passwordVisible // ignore: cast_nullable_to_non_nullable
as bool,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,
));
}
}
/// Adds pattern-matching-related methods to [LoginViewState].
extension LoginViewStatePatterns on LoginViewState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LoginViewState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LoginViewState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LoginViewState value) $default,){
final _that = this;
switch (_that) {
case _LoginViewState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LoginViewState value)? $default,){
final _that = this;
switch (_that) {
case _LoginViewState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String email, String password, bool passwordVisible, String errorMessage, bool isLoading)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LoginViewState() when $default != null:
return $default(_that.email,_that.password,_that.passwordVisible,_that.errorMessage,_that.isLoading);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String email, String password, bool passwordVisible, String errorMessage, bool isLoading) $default,) {final _that = this;
switch (_that) {
case _LoginViewState():
return $default(_that.email,_that.password,_that.passwordVisible,_that.errorMessage,_that.isLoading);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String email, String password, bool passwordVisible, String errorMessage, bool isLoading)? $default,) {final _that = this;
switch (_that) {
case _LoginViewState() when $default != null:
return $default(_that.email,_that.password,_that.passwordVisible,_that.errorMessage,_that.isLoading);case _:
return null;
}
}
}
/// @nodoc
class _LoginViewState implements LoginViewState {
const _LoginViewState({this.email = '', this.password = '', this.passwordVisible = false, this.errorMessage = '', this.isLoading = false});
@override@JsonKey() final String email;
@override@JsonKey() final String password;
@override@JsonKey() final bool passwordVisible;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool isLoading;
/// Create a copy of LoginViewState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LoginViewStateCopyWith<_LoginViewState> get copyWith => __$LoginViewStateCopyWithImpl<_LoginViewState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoginViewState&&(identical(other.email, email) || other.email == email)&&(identical(other.password, password) || other.password == password)&&(identical(other.passwordVisible, passwordVisible) || other.passwordVisible == passwordVisible)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading));
}
@override
int get hashCode => Object.hash(runtimeType,email,password,passwordVisible,errorMessage,isLoading);
@override
String toString() {
return 'LoginViewState(email: $email, password: $password, passwordVisible: $passwordVisible, errorMessage: $errorMessage, isLoading: $isLoading)';
}
}
/// @nodoc
abstract mixin class _$LoginViewStateCopyWith<$Res> implements $LoginViewStateCopyWith<$Res> {
factory _$LoginViewStateCopyWith(_LoginViewState value, $Res Function(_LoginViewState) _then) = __$LoginViewStateCopyWithImpl;
@override @useResult
$Res call({
String email, String password, bool passwordVisible, String errorMessage, bool isLoading
});
}
/// @nodoc
class __$LoginViewStateCopyWithImpl<$Res>
implements _$LoginViewStateCopyWith<$Res> {
__$LoginViewStateCopyWithImpl(this._self, this._then);
final _LoginViewState _self;
final $Res Function(_LoginViewState) _then;
/// Create a copy of LoginViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? email = null,Object? password = null,Object? passwordVisible = null,Object? errorMessage = null,Object? isLoading = null,}) {
return _then(_LoginViewState(
email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable
as String,password: null == password ? _self.password : password // ignore: cast_nullable_to_non_nullable
as String,passwordVisible: null == passwordVisible ? _self.passwordVisible : passwordVisible // ignore: cast_nullable_to_non_nullable
as bool,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,
));
}
}
// dart format on

View File

@@ -1,4 +1,4 @@
import 'package:design_system/design_system.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -6,12 +6,11 @@ class SignupAddressScreen extends ConsumerStatefulWidget {
const SignupAddressScreen({super.key});
@override
ConsumerState<SignupAddressScreen> createState() => SignupAddressScreenState();
ConsumerState<SignupAddressScreen> createState() =>
SignupAddressScreenState();
}
class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
class SignupAddressScreenState extends ConsumerState<SignupAddressScreen> {
late String country;
late int relation;
@@ -29,30 +28,39 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
Column(
spacing: 8,
children: [
Align(alignment: Alignment.bottomLeft, child: Text(
"Fecha de nacimiento",
style: TextStyle(fontSize: 14, letterSpacing: 0),
)),
Align(
alignment: Alignment.bottomLeft,
child: Text(
"Fecha de nacimiento",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
Row(
spacing: 8,
children: [
Expanded(child: CustomTextField(
spacing: 8,
children: [
Expanded(
child: CustomTextField(
//label: "Fecha de nacimiento",
hint: "DD",
length: 2,
numeric: true,
)),
Expanded(child: CustomTextField(
keyboardType: TextInputType.number,
),
),
Expanded(
child: CustomTextField(
hint: "MM",
length: 2,
numeric: true,
)),
Expanded(child: CustomTextField(
keyboardType: TextInputType.number,
),
),
Expanded(
child: CustomTextField(
hint: "AAAA",
length: 4,
numeric: true,
)),
]
keyboardType: TextInputType.number,
),
),
],
),
],
),
@@ -69,13 +77,16 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
CustomDropdown(
items: [Text("Padre"), Text("Madre"), Text("Tutor")],
hint: "¿Qué familiar eres?",
onChanged: (value)=>setState(() {
onChanged: (value) => setState(() {
relation = value;
})
}),
),
],
),
CustomTextField(label: "Dirección completa", hint: "Calle Gran Vía 30 6º, 28013"),
CustomTextField(
label: "Dirección completa",
hint: "Calle Gran Vía 30 6º, 28013",
),
CustomTextField(label: "Ciudad", hint: "Ciudad"),
Column(
spacing: 8,
@@ -90,9 +101,9 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
CustomDropdown(
items: [Text("España"), Text("Francia"), Text("Portugal")],
hint: "País",
onChanged: (value)=>setState(() {
onChanged: (value) => setState(() {
country = value;
})
}),
),
],
),

View File

@@ -1,7 +1,7 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
class SignupPersonalScreen extends StatelessWidget{
class SignupPersonalScreen extends StatelessWidget {
const SignupPersonalScreen({super.key});
@override
@@ -21,16 +21,20 @@ class SignupPersonalScreen extends StatelessWidget{
onChanged: (value)=> {},
width: 80,
),*/
Expanded(child: CustomTextField(
label: "Teléfono móvil",
hint: "123456789",
numeric: true
))
]
Expanded(
child: CustomTextField(
label: "Teléfono móvil",
hint: "123456789",
keyboardType: TextInputType.number,
),
),
],
),
CustomTextField(
label: "Correo electrónico",
hint: "Correo electrónico",
),
CustomTextField(label: "Correo electrónico", hint: "Correo electrónico"),
],
);
}
}
}

View File

@@ -32,9 +32,9 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
child: Column(
children: [
PrimaryButton(
onPressed: ()=>{},
onPressed: () => {},
text: "Añadir dinero",
color: theme.getColorFor(ThemeCode.buttonPrimary)
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
onPressed: () => Navigator.pop(context),
@@ -58,7 +58,7 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
CustomTextField(
numeric: true,
keyboardType: TextInputType.number,
label: "Cantidad",
hint: "0€",
),
@@ -135,7 +135,8 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
CustomTextField(
lines: 3,
length: 150,
label: "Escribir mensaje a ${widget.kid.name} del motivo del ingreso",
label:
"Escribir mensaje a ${widget.kid.name} del motivo del ingreso",
hint: "Escribe tu mensaje",
),
const Align(
@@ -191,4 +192,4 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
],
);
}
}
}

View File

@@ -5,14 +5,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:home/src/presentation/wallet_management_layout.dart';
import 'package:sf_shared/sf_shared.dart';
class ExtractScreen extends ConsumerWidget{
class ExtractScreen extends ConsumerWidget {
final Kid kid;
@override
ExtractScreen({
super.key,
required this.kid,
});
ExtractScreen({super.key, required this.kid});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -20,92 +17,125 @@ class ExtractScreen extends ConsumerWidget{
return WalletManagementLayout(
kid: kid,
children: [Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary)
children: [
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: Column(
spacing: 24,
children: [
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
"Retirar dinero de la cuenta",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
"Este dato aparecerá en el reloj del peque",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
],
),
CustomTextField(
label: "Selecciona la cantidad de dinero",
hint: "2€",
keyboardType: TextInputType.number,
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
"Este es el mensaje fijado por defecto:",
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
"\"Hemos quitado el dinero del reloj, ya no puedes pagar con él\"",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
],
),
Column(
spacing: 8,
children: [
Align(
alignment: Alignment.topLeft,
child: Text(
"Escribir mensaje a ${kid.name} del motivo de la retirada de su dinero",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
),
CustomTextField(
hint: "Escribe tu mensaje",
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text(
"Máximo 150 caracteres",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
],
),
],
),
],
),
),
child: Column(
spacing: 24,
children: [
Column(
spacing: 8,
children: [
Align(alignment: Alignment.topLeft, child: Text(
"Retirar dinero de la cuenta",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0)
)),
Align(alignment: Alignment.topLeft, child: Text(
"Este dato aparecerá en el reloj del peque",
style: TextStyle(fontSize: 14, letterSpacing: 0)
))
],
),
CustomTextField(
label: "Selecciona la cantidad de dinero",
hint: "2€",
numeric: true,
),
Column(
spacing: 8,
children: [
Align(alignment: Alignment.topLeft, child: Text(
"Este es el mensaje fijado por defecto:",
style: TextStyle(fontSize: 16, letterSpacing: 0)
)),
Align(alignment: Alignment.topLeft, child: Text(
"\"Hemos quitado el dinero del reloj, ya no puedes pagar con él\"",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, letterSpacing: 0)
))
],
),
Column(
spacing: 8,
children: [
Align(alignment: Alignment.topLeft, child: Text(
"Escribir mensaje a ${kid.name} del motivo de la retirada de su dinero",
style: TextStyle(fontSize: 14, letterSpacing: 0)
)),
CustomTextField(
hint: "Escribe tu mensaje",
lines: 4,
length: 150,
),
Row(
spacing: 4,
children: [
Icon(Icons.info_outline, size: 16),
Text("Máximo 150 caracteres", style: TextStyle(fontSize: 14, letterSpacing: 0))
],
)
],
)
],
),
)],
],
footer: Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.only(topRight: Radius.circular(24), topLeft: Radius.circular(24))
borderRadius: BorderRadius.only(
topRight: Radius.circular(24),
topLeft: Radius.circular(24),
),
),
child: Column(
spacing: 16,
children: [
PrimaryButton(
onPressed: ()=>{Navigator.pop(context)},
onPressed: () => {Navigator.pop(context)},
text: "Enviar mensaje y bloquear",
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
style: ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.all(0))),
onPressed: ()=>Navigator.pop(context),
child: Text("Cancelar", style: TextStyle(fontSize: 18))
)
style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () => Navigator.pop(context),
child: Text("Cancelar", style: TextStyle(fontSize: 18)),
),
],
),
)
),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,8 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
@override
void initState() {
super.initState();
dailyLimits = [ //dey, week, month, year
dailyLimits = [
//dey, week, month, year
{"title": "Diario L-V", "limit": "5", "edit": false},
{"title": "Fines de semana", "limit": "8", "edit": false},
{"title": "Semanal", "limit": "30", "edit": false},
@@ -41,12 +42,7 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
"end": "21:00",
"edit": false,
},
{
"title": "Vacaciones",
"start": "09:00",
"end": "22:00",
"edit": false
},
{"title": "Vacaciones", "start": "09:00", "end": "22:00", "edit": false},
];
conditions = [
{"title": "Alimentación", "limit": "10", "active": true, "edit": false},
@@ -73,7 +69,10 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
footer: Container(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24))
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
padding: EdgeInsets.all(24),
child: Column(
@@ -81,19 +80,19 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
PrimaryButton(
onPressed: () => {},
text: "Guardar límites",
color: theme.getColorFor(ThemeCode.buttonPrimary)
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(
onPressed: ()=>Navigator.pop(context),
onPressed: () => Navigator.pop(context),
child: Text(
"Cancelar",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: theme.getColorFor(ThemeCode.textPrimary)
)
)
)
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
),
],
),
),
@@ -112,7 +111,7 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
child: Text(
"Pon límite de gastos",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
)
),
),
Text("Libertad para ellos, tranquilidad para ti"),
...List<Widget>.generate(dailyLimits.length, (int index) {
@@ -135,9 +134,7 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
),
],
),
if (dailyLimits[index]["edit"]) CustomTextField(
hint: "5€",
),
if (dailyLimits[index]["edit"]) CustomTextField(hint: "5€"),
],
);
}),
@@ -170,16 +167,15 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
TextButton(
onPressed: () => {
setState(() {
timeLimits[index]["edit"] = !timeLimits[index]["edit"];
timeLimits[index]["edit"] =
!timeLimits[index]["edit"];
}),
},
child: Text("Editar"),
),
],
),
if (timeLimits[index]["edit"]) CustomTextField(
hint: "5€",
),
if (timeLimits[index]["edit"]) CustomTextField(hint: "5€"),
],
);
}),
@@ -197,67 +193,89 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
children: [
Text(
"Condiciones",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Column(
spacing: 8,
children: List<Widget>.generate(conditions.length, (int index)=>
Column(
children: List<Widget>.generate(
conditions.length,
(int index) => Column(
spacing: 8,
children: [
Row(children: [
Expanded(child: CheckboxListTile(
value: conditions[index]["active"],
onChanged: (_)=>setState(() {
conditions[index]["active"] = !conditions[index]["active"];
}),
title: Text(
"${conditions[index]["title"]}: ${conditions[index]["limit"]}€/sem",
style: TextStyle(fontSize: 16, letterSpacing: 0),
Row(
children: [
Expanded(
child: CheckboxListTile(
value: conditions[index]["active"],
onChanged: (_) => setState(() {
conditions[index]["active"] =
!conditions[index]["active"];
}),
title: Text(
"${conditions[index]["title"]}: ${conditions[index]["limit"]}€/sem",
style: TextStyle(
fontSize: 16,
letterSpacing: 0,
),
),
checkboxScaleFactor: 2,
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(
ThemeCode.buttonPrimary,
),
contentPadding: EdgeInsets.zero,
),
),
checkboxScaleFactor: 2,
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
contentPadding: EdgeInsets.zero,
)),
TextButton(
onPressed: ()=>setState(() {
conditions[index]["edit"] = ! conditions[index]["edit"];
}),
child: Text(
"Editar",
style: TextStyle(fontSize: 16, letterSpacing: 0),
)
)
]),
if (conditions[index]["edit"]) CustomTextField(
hint: "5€",
numeric: true,
)
]
)
TextButton(
onPressed: () => setState(() {
conditions[index]["edit"] =
!conditions[index]["edit"];
}),
child: Text(
"Editar",
style: TextStyle(fontSize: 16, letterSpacing: 0),
),
),
],
),
if (conditions[index]["edit"])
CustomTextField(
hint: "5€",
keyboardType: TextInputType.number,
),
],
),
),
)
),
],
)
),
),
Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24))
borderRadius: BorderRadius.all(Radius.circular(24)),
),
child: Column(
spacing: 24,
children: [
Text(
"Comercios bloqueados",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Column(
spacing: 8,
children: List<Widget>.generate(blocks.length, (int index) =>
CheckboxListTile(
children: List<Widget>.generate(
blocks.length,
(int index) => CheckboxListTile(
value: blocks[index]["active"],
onChanged: (_) => setState(() {
blocks[index]["active"] = !blocks[index]["active"];
@@ -270,13 +288,13 @@ class LimitsScreenState extends ConsumerState<LimitsScreen> {
controlAffinity: ListTileControlAffinity.leading,
activeColor: theme.getColorFor(ThemeCode.buttonPrimary),
contentPadding: EdgeInsets.zero,
)
)
)
),
),
),
],
),
)
),
],
);
}
}
}

View File

@@ -43,7 +43,7 @@ class _WageScreenState extends ConsumerState<WageScreen> {
PrimaryButton(
onPressed: () => {},
text: "Activar paga automática",
color: theme.getColorFor(ThemeCode.buttonPrimary)
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
TextButton(onPressed: () {}, child: const Text('Cancelar')),
],
@@ -59,14 +59,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column(
spacing: 10,
children: [
Align(alignment: Alignment.topLeft,
Align(
alignment: Alignment.topLeft,
child: const Text(
"Paga automática",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
),
CustomTextField(
numeric: true,
keyboardType: TextInputType.number,
label: "Cantidad",
hint: "0€",
),
@@ -84,13 +85,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column(
spacing: 10,
children: [
Align(alignment: Alignment.topLeft,
Align(
alignment: Alignment.topLeft,
child: const Text(
"Frecuencia",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
)
),
),
Align(alignment: Alignment.topLeft,
Align(
alignment: Alignment.topLeft,
child: const Text("Cuándo se envía el dinero"),
),
CheckboxListTile(
@@ -139,22 +142,30 @@ class _WageScreenState extends ConsumerState<WageScreen> {
Text("Sábado"),
Text("Domingo"),
],
values: ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"],
onChanged: (value)=> {},
values: [
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado",
"Domingo",
],
onChanged: (value) => {},
hint: "Día de la semana",
),
CustomDropdown(
hint: "Hora del día",
items: List<Widget>.generate(24,(int index){
items: List<Widget>.generate(24, (int index) {
return Text("$index:00");
}),
onChanged: (value)=> {},
onChanged: (value) => {},
),
CustomTextField(
lines: 3,
length: 150,
label:
"Escribir mensaje a ${widget.kid.name} del motivo del ingreso",
"Escribir mensaje a ${widget.kid.name} del motivo del ingreso",
hint: "Escribe tu mensaje",
),
const Align(
@@ -174,13 +185,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column(
spacing: 10,
children: [
const Align(alignment: Alignment.topLeft,
const Align(
alignment: Alignment.topLeft,
child: Text(
"Condiciones",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
),
const Align(alignment: Alignment.topLeft,
const Align(
alignment: Alignment.topLeft,
child: Text("Este dato aparecerá en el reloj del peque"),
),
CheckboxListTile(