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<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<String> requestPasswordReset({String? phone, String? email});
Future<void> recoverPassword({required newPassword, required token}); Future<void> recoverPassword({required newPassword, required token});

View File

@@ -54,6 +54,35 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
return Exception(message); 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 @override
Future<String> requestPasswordReset({ Future<String> requestPasswordReset({
String? phone, String? phone,

View File

@@ -16,6 +16,16 @@ class AuthRepositoryImpl implements AuthRepository {
return _remote.verifyPhoneCode(phone: phone, code: code); 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 @override
Future<String> requestPasswordReset({String? phone, String? email}) { Future<String> requestPasswordReset({String? phone, String? email}) {
return _remote.requestPasswordReset(phone: phone, email: 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<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<String> requestPasswordReset({String phone, String email});
Future<void> recoverPassword({required String newPassword, required String token}); Future<void> recoverPassword({required String newPassword, required String token});

View File

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

View File

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

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

@@ -6,12 +6,11 @@ class SignupAddressScreen extends ConsumerStatefulWidget {
const SignupAddressScreen({super.key}); const SignupAddressScreen({super.key});
@override @override
ConsumerState<SignupAddressScreen> createState() => SignupAddressScreenState(); ConsumerState<SignupAddressScreen> createState() =>
SignupAddressScreenState();
} }
class SignupAddressScreenState extends ConsumerState<SignupAddressScreen> { class SignupAddressScreenState extends ConsumerState<SignupAddressScreen> {
late String country; late String country;
late int relation; late int relation;
@@ -29,30 +28,39 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
Column( Column(
spacing: 8, spacing: 8,
children: [ children: [
Align(alignment: Alignment.bottomLeft, child: Text( Align(
alignment: Alignment.bottomLeft,
child: Text(
"Fecha de nacimiento", "Fecha de nacimiento",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
)), ),
),
Row( Row(
spacing: 8, spacing: 8,
children: [ children: [
Expanded(child: CustomTextField( Expanded(
child: CustomTextField(
//label: "Fecha de nacimiento", //label: "Fecha de nacimiento",
hint: "DD", hint: "DD",
length: 2, length: 2,
numeric: true, keyboardType: TextInputType.number,
)), ),
Expanded(child: CustomTextField( ),
Expanded(
child: CustomTextField(
hint: "MM", hint: "MM",
length: 2, length: 2,
numeric: true, keyboardType: TextInputType.number,
)), ),
Expanded(child: CustomTextField( ),
Expanded(
child: CustomTextField(
hint: "AAAA", hint: "AAAA",
length: 4, length: 4,
numeric: true, keyboardType: TextInputType.number,
)), ),
] ),
],
), ),
], ],
), ),
@@ -71,11 +79,14 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
hint: "¿Qué familiar eres?", hint: "¿Qué familiar eres?",
onChanged: (value) => setState(() { onChanged: (value) => setState(() {
relation = value; 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"), CustomTextField(label: "Ciudad", hint: "Ciudad"),
Column( Column(
spacing: 8, spacing: 8,
@@ -92,7 +103,7 @@ class SignupAddressScreenState extends ConsumerState<SignupAddressScreen>{
hint: "País", hint: "País",
onChanged: (value) => setState(() { onChanged: (value) => setState(() {
country = value; country = value;
}) }),
), ),
], ],
), ),

View File

@@ -21,16 +21,20 @@ class SignupPersonalScreen extends StatelessWidget{
onChanged: (value)=> {}, onChanged: (value)=> {},
width: 80, width: 80,
),*/ ),*/
Expanded(child: CustomTextField( Expanded(
child: CustomTextField(
label: "Teléfono móvil", label: "Teléfono móvil",
hint: "123456789", hint: "123456789",
numeric: true keyboardType: TextInputType.number,
)) ),
] ),
],
),
CustomTextField(
label: "Correo electrónico",
hint: "Correo electrónico",
), ),
CustomTextField(label: "Correo electrónico", hint: "Correo electrónico"),
], ],
); );
} }
} }

View File

@@ -34,7 +34,7 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
PrimaryButton( PrimaryButton(
onPressed: () => {}, onPressed: () => {},
text: "Añadir dinero", text: "Añadir dinero",
color: theme.getColorFor(ThemeCode.buttonPrimary) color: theme.getColorFor(ThemeCode.buttonPrimary),
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
@@ -58,7 +58,7 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
), ),
CustomTextField( CustomTextField(
numeric: true, keyboardType: TextInputType.number,
label: "Cantidad", label: "Cantidad",
hint: "0€", hint: "0€",
), ),
@@ -135,7 +135,8 @@ class _DepositScreenState extends ConsumerState<DepositScreen> {
CustomTextField( CustomTextField(
lines: 3, lines: 3,
length: 150, 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", hint: "Escribe tu mensaje",
), ),
const Align( const Align(

View File

@@ -9,10 +9,7 @@ class ExtractScreen extends ConsumerWidget{
final Kid kid; final Kid kid;
@override @override
ExtractScreen({ ExtractScreen({super.key, required this.kid});
super.key,
required this.kid,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -20,11 +17,12 @@ class ExtractScreen extends ConsumerWidget{
return WalletManagementLayout( return WalletManagementLayout(
kid: kid, kid: kid,
children: [Container( children: [
Container(
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary) color: theme.getColorFor(ThemeCode.backgroundPrimary),
), ),
child: Column( child: Column(
spacing: 24, spacing: 24,
@@ -32,41 +30,64 @@ class ExtractScreen extends ConsumerWidget{
Column( Column(
spacing: 8, spacing: 8,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Retirar dinero de la cuenta", "Retirar dinero de la cuenta",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0) style: TextStyle(
)), fontSize: 20,
Align(alignment: Alignment.topLeft, child: Text( fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: Text(
"Este dato aparecerá en el reloj del peque", "Este dato aparecerá en el reloj del peque",
style: TextStyle(fontSize: 14, letterSpacing: 0) style: TextStyle(fontSize: 14, letterSpacing: 0),
)) ),
),
], ],
), ),
CustomTextField( CustomTextField(
label: "Selecciona la cantidad de dinero", label: "Selecciona la cantidad de dinero",
hint: "2€", hint: "2€",
numeric: true, keyboardType: TextInputType.number,
), ),
Column( Column(
spacing: 8, spacing: 8,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Este es el mensaje fijado por defecto:", "Este es el mensaje fijado por defecto:",
style: TextStyle(fontSize: 16, letterSpacing: 0) style: TextStyle(fontSize: 16, letterSpacing: 0),
)), ),
Align(alignment: Alignment.topLeft, child: Text( ),
Align(
alignment: Alignment.topLeft,
child: Text(
"\"Hemos quitado el dinero del reloj, ya no puedes pagar con él\"", "\"Hemos quitado el dinero del reloj, ya no puedes pagar con él\"",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, letterSpacing: 0) style: TextStyle(
)) fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
], ],
), ),
Column( Column(
spacing: 8, spacing: 8,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Escribir mensaje a ${kid.name} del motivo de la retirada de su dinero", "Escribir mensaje a ${kid.name} del motivo de la retirada de su dinero",
style: TextStyle(fontSize: 14, letterSpacing: 0) style: TextStyle(fontSize: 14, letterSpacing: 0),
)), ),
),
CustomTextField( CustomTextField(
hint: "Escribe tu mensaje", hint: "Escribe tu mensaje",
lines: 4, lines: 4,
@@ -76,19 +97,26 @@ class ExtractScreen extends ConsumerWidget{
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.info_outline, size: 16), Icon(Icons.info_outline, size: 16),
Text("Máximo 150 caracteres", style: TextStyle(fontSize: 14, letterSpacing: 0)) Text(
], "Máximo 150 caracteres",
) style: TextStyle(fontSize: 14, letterSpacing: 0),
], ),
)
], ],
), ),
)], ],
),
],
),
),
],
footer: Container( footer: Container(
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary), 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( child: Column(
spacing: 16, spacing: 16,
@@ -99,13 +127,15 @@ class ExtractScreen extends ConsumerWidget{
color: theme.getColorFor(ThemeCode.buttonPrimary), color: theme.getColorFor(ThemeCode.buttonPrimary),
), ),
TextButton( TextButton(
style: ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.all(0))), style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.all(0)),
),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text("Cancelar", style: TextStyle(fontSize: 18)) child: Text("Cancelar", style: TextStyle(fontSize: 18)),
) ),
], ],
), ),
) ),
); );
} }
} }

View File

@@ -7,12 +7,18 @@ import 'package:sf_shared/sf_shared.dart';
class GoalsScreen extends ConsumerStatefulWidget { class GoalsScreen extends ConsumerStatefulWidget {
final Kid kid; final Kid kid;
List<Task> tasks = [Task(rewardAmount: 10, subtasks: [ List<Task> tasks = [
Task(
rewardAmount: 10,
subtasks: [
Subtask(name: "Hacer la cama", completed: false), Subtask(name: "Hacer la cama", completed: false),
Subtask(name: "Terminar los deberes", completed: true), Subtask(name: "Terminar los deberes", completed: true),
Subtask(name: "Recoger la mesa", completed: true), Subtask(name: "Recoger la mesa", completed: true),
Subtask(name: "Lavarse los dientes", completed: false) Subtask(name: "Lavarse los dientes", completed: false),
]), Task(rewardAmount: 10)]; ],
),
Task(rewardAmount: 10),
];
final List<SavingsGoal> savingsGoals; final List<SavingsGoal> savingsGoals;
@override @override
@@ -21,8 +27,8 @@ class GoalsScreen extends ConsumerStatefulWidget{
required this.kid, required this.kid,
this.savingsGoals = const [ this.savingsGoals = const [
SavingsGoal(name: "Cumpleaños de Emma", goal: 24, saved: 12.09), SavingsGoal(name: "Cumpleaños de Emma", goal: 24, saved: 12.09),
SavingsGoal(name: "Protecciones nuevas de patines", goal: 10, saved: 0) SavingsGoal(name: "Protecciones nuevas de patines", goal: 10, saved: 0),
] ],
}); });
@override @override
@@ -49,7 +55,7 @@ class GoalsScreenState extends ConsumerState<GoalsScreen>{
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary), color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)) borderRadius: BorderRadius.all(Radius.circular(24)),
), ),
child: Column( child: Column(
spacing: 24, spacing: 24,
@@ -57,10 +63,20 @@ class GoalsScreenState extends ConsumerState<GoalsScreen>{
Row( Row(
spacing: 8, spacing: 8,
children: [ children: [
Text("Metas", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0)), Text(
"Metas",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
Icon(Icons.workspace_premium), Icon(Icons.workspace_premium),
Spacer(), Spacer(),
Text("Sólo con Plan Completo", style: TextStyle(fontSize: 14, letterSpacing: 0)) Text(
"Sólo con Plan Completo",
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
], ],
), ),
Text( Text(
@@ -73,7 +89,7 @@ class GoalsScreenState extends ConsumerState<GoalsScreen>{
SavingsBlock(fullPlan: fullPlan, savings: widget.savingsGoals), SavingsBlock(fullPlan: fullPlan, savings: widget.savingsGoals),
TasksBlock(fullPlan: fullPlan, tasks: widget.tasks), TasksBlock(fullPlan: fullPlan, tasks: widget.tasks),
], ],
footer: Container() footer: Container(),
); );
} }
} }
@@ -86,7 +102,7 @@ class SavingsBlock extends ConsumerStatefulWidget {
const SavingsBlock({ const SavingsBlock({
super.key, super.key,
required this.fullPlan, required this.fullPlan,
required this.savings required this.savings,
}); });
@override @override
@@ -115,26 +131,40 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary), color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
border: BoxBorder.all(color: theme.getColorFor(ThemeCode.textPrimary)) border: BoxBorder.all(color: theme.getColorFor(ThemeCode.textPrimary)),
), ),
child: Stack( child: Stack(
children: [ children: [
Align(alignment: Alignment.topRight, child: SvgPicture.asset("assets/images/ui/ahorros.svg")), Align(
alignment: Alignment.topRight,
child: SvgPicture.asset("assets/images/ui/ahorros.svg"),
),
Column( Column(
spacing: 24, spacing: 24,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Ahorros", "Ahorros",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0), style: TextStyle(
)), fontSize: 20,
Align(alignment: Alignment.topLeft, child: SizedBox( fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200, width: 200,
child: Text( child: Text(
"Enséñale a ahorrar y refuerza la importancia de no malgastar su dinero", "Enséñale a ahorrar y refuerza la importancia de no malgastar su dinero",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
), ),
)), ),
if (fullPlan) Column( ),
if (fullPlan)
Column(
spacing: 24, spacing: 24,
children: [ children: [
Align( Align(
@@ -154,43 +184,52 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: TextButton( child: TextButton(
onPressed: () => {}, onPressed: () => {},
style: ButtonStyle(padding: WidgetStatePropertyAll(EdgeInsets.zero)), style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.zero),
),
child: Text( child: Text(
"Ver estado de ahorros", "Ver estado de ahorros",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, letterSpacing: 0) style: TextStyle(
) fontSize: 14,
) fontWeight: FontWeight.w500,
) letterSpacing: 0,
),
),
),
),
], ],
), ),
if (!fullPlan) TextButton( if (!fullPlan)
TextButton(
onPressed: () => {}, onPressed: () => {},
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.workspace_premium, size: 16), Icon(Icons.workspace_premium, size: 16),
Text("Suscribirme al Plan Completo") Text("Suscribirme al Plan Completo"),
], ],
) ),
) ),
], ],
) ),
], ],
) ),
); );
var editBlock = ({create, index}) => Column( var editBlock = ({create, index}) => Column(
spacing: 24, spacing: 24,
children: [ children: [
CustomTextField( CustomTextField(
hint: create? "Ponle un título para reconocerlo" : widget.savings[index].name, hint: create
? "Ponle un título para reconocerlo"
: widget.savings[index].name,
label: "Motivo del ahorro", label: "Motivo del ahorro",
lines: create ? 2 : 1, lines: create ? 2 : 1,
), ),
CustomTextField( CustomTextField(
hint: "30€", hint: "30€",
label: "Seleciona la cantidad a ahorrar", label: "Seleciona la cantidad a ahorrar",
numeric: true, keyboardType: TextInputType.number,
), ),
CheckboxListTile( CheckboxListTile(
value: false, value: false,
@@ -201,13 +240,17 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text( title: Text(
"Ahorro automático desde su paga", "Ahorro automático desde su paga",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, letterSpacing: 0), style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
), ),
), ),
CustomTextField( CustomTextField(
hint: "2€", hint: "2€",
label: "Selecciona la cantidad de dinero", label: "Selecciona la cantidad de dinero",
numeric: true, keyboardType: TextInputType.number,
), ),
CheckboxListTile( CheckboxListTile(
value: false, value: false,
@@ -230,8 +273,12 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
), ),
Text( Text(
"\"¡Genial, has conseguido ahorrar lo que querías!\"", "\"¡Genial, has conseguido ahorrar lo que querías!\"",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, letterSpacing: 0), style: TextStyle(
) fontSize: 18,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
], ],
), ),
Column( Column(
@@ -246,13 +293,13 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
Row( Row(
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.info_outline, size: 16,), Icon(Icons.info_outline, size: 16),
Text( Text(
"Máximo 150 caracteres", "Máximo 150 caracteres",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
) ),
], ],
) ),
], ],
), ),
Column( Column(
@@ -261,8 +308,7 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
PrimaryButton( PrimaryButton(
onPressed: () => {}, onPressed: () => {},
text: "Guardar cambios", text: "Guardar cambios",
color: theme.getColorFor( color: theme.getColorFor(ThemeCode.buttonPrimary),
ThemeCode.buttonPrimary)
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -278,13 +324,15 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
}, },
child: Text( child: Text(
"Cancelar", "Cancelar",
style: TextStyle(fontSize: 16, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
) ),
) ),
),
], ],
) ),
], ],
); );
@@ -295,15 +343,22 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundSecondary), color: theme.getColorFor(ThemeCode.backgroundSecondary),
border: Border.all(color: theme.getColorFor(ThemeCode.textPrimary)), border: Border.all(color: theme.getColorFor(ThemeCode.textPrimary)),
borderRadius: BorderRadius.all(Radius.circular(24)) borderRadius: BorderRadius.all(Radius.circular(24)),
), ),
child: Column( child: Column(
spacing: 24, spacing: 24,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Ahorros", "Ahorros",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0), style: TextStyle(
)), fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Text( Text(
"Enséñale a ahorrar y refuerza la importancia de no malgastar su dinero", "Enséñale a ahorrar y refuerza la importancia de no malgastar su dinero",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
@@ -312,10 +367,10 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary) color: theme.getColorFor(ThemeCode.backgroundPrimary),
),
child: editBlock(create: true, index: null),
), ),
child: editBlock(create: true, index: null)
)
], ],
), ),
); );
@@ -331,19 +386,24 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
child: Column( child: Column(
spacing: 24, spacing: 24,
children: [ children: [
Align(alignment: Alignment.topLeft, Align(
alignment: Alignment.topLeft,
child: Text( child: Text(
"Ahorros", "Ahorros",
style: TextStyle(fontSize: 20, style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
)
), ),
...List<Widget>.generate(widget.savings.length, (int index) => ),
Container( ),
...List<Widget>.generate(
widget.savings.length,
(int index) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: BoxBorder.all( border: BoxBorder.all(
color: theme.getColorFor(ThemeCode.textPrimary)), color: theme.getColorFor(ThemeCode.textPrimary),
),
color: theme.getColorFor(ThemeCode.backgroundPrimary), color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
), ),
@@ -354,7 +414,8 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
Row( Row(
spacing: 16, spacing: 16,
children: [ children: [
Expanded(child: Column( Expanded(
child: Column(
spacing: 8, spacing: 8,
children: [ children: [
Align( Align(
@@ -363,47 +424,61 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
"Ahorro para", "Ahorro para",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
fontSize: 16, letterSpacing: 0), fontSize: 16,
letterSpacing: 0,
),
), ),
), ),
Text( Text(
widget.savings[index].name, widget.savings[index].name,
style: TextStyle(fontSize: 16, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
) ),
),
], ],
)), ),
),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor( color: theme.getColorFor(
ThemeCode.backgroundTertiary), ThemeCode.backgroundTertiary,
),
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(16)), Radius.circular(16),
),
), ),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 8, vertical: 4), horizontal: 8,
vertical: 4,
),
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [
Icon(Icons.account_balance, size: 24, Icon(
Icons.account_balance,
size: 24,
color: theme.getColorFor( color: theme.getColorFor(
ThemeCode.buttonPrimary)), ThemeCode.buttonPrimary,
),
),
MoneyText( MoneyText(
text: "${widget.savings[index] text: "${widget.savings[index].goal}",
.goal}",
size: 30, size: 30,
secondarySize: 14, secondarySize: 14,
color: theme.getColorFor( color: theme.getColorFor(
ThemeCode.buttonPrimary) ThemeCode.buttonPrimary,
) ),
),
], ],
), ),
)
]
), ),
if (index == 0)Column( ],
),
if (index == 0)
Column(
spacing: 8, spacing: 8,
children: [ children: [
ProgressBar( ProgressBar(
@@ -413,42 +488,52 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
textSize: 40, textSize: 40,
textSecondarySize: 24, textSecondarySize: 24,
backgroundColor: theme.getColorFor( backgroundColor: theme.getColorFor(
ThemeCode.backgroundTertiary), ThemeCode.backgroundTertiary,
),
foregroundColor: theme.getColorFor( foregroundColor: theme.getColorFor(
ThemeCode.buttonPrimary), ThemeCode.buttonPrimary,
),
textColor: theme.getColorFor( textColor: theme.getColorFor(
ThemeCode.textSecondary) ThemeCode.textSecondary,
),
), ),
Center(child: Text("Ahorrado")), Center(child: Text("Ahorrado")),
if (!showAdd[index]) TextButton( if (!showAdd[index])
TextButton(
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStatePropertyAll( padding: WidgetStatePropertyAll(
EdgeInsets.zero)), EdgeInsets.zero,
onPressed: () => ),
setState(() { ),
onPressed: () => setState(() {
showAdd[index] = true; showAdd[index] = true;
}), }),
child: Text( child: Text(
"+ Añadir dinero extra a este ahorro", "+ Añadir dinero extra a este ahorro",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
fontSize: 16, letterSpacing: 0), fontSize: 16,
) letterSpacing: 0,
), ),
if (showAdd[index]) CustomTextField( ),
),
if (showAdd[index])
CustomTextField(
hint: "5€", hint: "5€",
numeric: true, keyboardType: TextInputType.number,
) ),
], ],
), ),
if (!showEdit[index]) Row( if (!showEdit[index])
Row(
children: [ children: [
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStatePropertyAll( padding: WidgetStatePropertyAll(
EdgeInsets.zero)), EdgeInsets.zero,
onPressed: () => ),
setState(() { ),
onPressed: () => setState(() {
showEdit[index] = true; showEdit[index] = true;
}), }),
child: Row( child: Row(
@@ -457,44 +542,51 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
Icon(Icons.edit_outlined, size: 24), Icon(Icons.edit_outlined, size: 24),
Text( Text(
"Editar", "Editar",
style: TextStyle(fontSize: 16, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
),
), ),
], ],
) ),
), ),
Spacer(), Spacer(),
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStatePropertyAll( padding: WidgetStatePropertyAll(
EdgeInsets.zero)), EdgeInsets.zero,
),
),
onPressed: () => {}, onPressed: () => {},
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.delete_outline_outlined, Icon(
size: 24), Icons.delete_outline_outlined,
size: 24,
),
Text( Text(
"Eliminar", "Eliminar",
style: TextStyle(fontSize: 16, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
) ),
],
)
), ),
], ],
), ),
if(showEdit[index]) editBlock( ),
create: false, index: index) ],
),
if (showEdit[index])
editBlock(create: false, index: index),
], ],
), ),
), ),
), ),
TextButton( TextButton(
onPressed: () => onPressed: () => setState(() {
setState(() {
showNew = true; showNew = true;
}), }),
child: Row( child: Row(
@@ -504,13 +596,15 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
Icon(Icons.account_balance, size: 24), Icon(Icons.account_balance, size: 24),
Text( Text(
"Crear otro ahorro", "Crear otro ahorro",
style: TextStyle(fontSize: 18, style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
), ),
Spacer() ),
Spacer(),
], ],
) ),
), ),
], ],
), ),
@@ -526,23 +620,17 @@ class SavingsBlockState extends ConsumerState<SavingsBlock>{
} }
class TasksBlock extends ConsumerStatefulWidget { class TasksBlock extends ConsumerStatefulWidget {
final bool fullPlan; final bool fullPlan;
final List<Task> tasks; final List<Task> tasks;
@override @override
const TasksBlock({ const TasksBlock({super.key, required this.fullPlan, required this.tasks});
super.key,
required this.fullPlan,
required this.tasks
});
@override @override
ConsumerState<ConsumerStatefulWidget> createState() => TasksBlockState(); ConsumerState<ConsumerStatefulWidget> createState() => TasksBlockState();
} }
class TasksBlockState extends ConsumerState<TasksBlock> { class TasksBlockState extends ConsumerState<TasksBlock> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
@@ -552,26 +640,40 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundPrimary), color: theme.getColorFor(ThemeCode.backgroundPrimary),
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
border: BoxBorder.all(color: theme.getColorFor(ThemeCode.textPrimary)) border: BoxBorder.all(color: theme.getColorFor(ThemeCode.textPrimary)),
), ),
child: Stack( child: Stack(
children: [ children: [
Align(alignment: Alignment.topRight, child: SvgPicture.asset("assets/images/ui/tareas.svg")), Align(
alignment: Alignment.topRight,
child: SvgPicture.asset("assets/images/ui/tareas.svg"),
),
Column( Column(
spacing: 24, spacing: 24,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Tareas", "Tareas",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500, letterSpacing: 0), style: TextStyle(
)), fontSize: 20,
Align(alignment: Alignment.topLeft, child: SizedBox( fontWeight: FontWeight.w500,
letterSpacing: 0,
),
),
),
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200, width: 200,
child: Text( child: Text(
"Refuerza su sentido de la responsabilidad con pequeñas tareas que cumplir", "Refuerza su sentido de la responsabilidad con pequeñas tareas que cumplir",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
), ),
)), ),
if (fullPlan) Align( ),
if (fullPlan)
Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: SecondaryButton( child: SecondaryButton(
onPressed: () => {}, onPressed: () => {},
@@ -579,22 +681,23 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
size: 14, size: 14,
width: 190, width: 190,
height: 44, height: 44,
)
), ),
if (!fullPlan) TextButton( ),
if (!fullPlan)
TextButton(
onPressed: () => {}, onPressed: () => {},
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.workspace_premium, size: 16), Icon(Icons.workspace_premium, size: 16),
Text("Suscribirme al Plan Completo") Text("Suscribirme al Plan Completo"),
], ],
) ),
) ),
], ],
) ),
], ],
) ),
); );
if (widget.fullPlan) { if (widget.fullPlan) {
@@ -604,8 +707,9 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
border: BoxBorder.all( border: BoxBorder.all(
color: theme.getColorFor(ThemeCode.textPrimary)), color: theme.getColorFor(ThemeCode.textPrimary),
color: theme.getColorFor(ThemeCode.backgroundSecondary) ),
color: theme.getColorFor(ThemeCode.backgroundSecondary),
), ),
child: Column( child: Column(
spacing: 24, spacing: 24,
@@ -613,113 +717,136 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
Column( Column(
spacing: 8, spacing: 8,
children: [ children: [
Align(alignment: Alignment.topLeft, child: Text( Align(
alignment: Alignment.topLeft,
child: Text(
"Tareas", "Tareas",
style: TextStyle(fontSize: 20, style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
)), ),
),
),
Text( Text(
"Refuerza su sentido de la responsabilidad con pequeñas tareas que cumplir", "Refuerza su sentido de la responsabilidad con pequeñas tareas que cumplir",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
) ),
], ],
), ),
...List<Widget>.generate(widget.tasks.length, (int i) => ...List<Widget>.generate(
Container( widget.tasks.length,
(int i) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: BoxBorder.fromLTRB(top: BorderSide( border: BoxBorder.fromLTRB(
top: BorderSide(
color: theme.getColorFor(ThemeCode.buttonPrimary), color: theme.getColorFor(ThemeCode.buttonPrimary),
width: 8)), width: 8,
),
),
borderRadius: BorderRadius.all(Radius.circular(24)), borderRadius: BorderRadius.all(Radius.circular(24)),
color: theme.getColorFor(ThemeCode.backgroundPrimary) color: theme.getColorFor(ThemeCode.backgroundPrimary),
), ),
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
child: Column( child: Column(
spacing: 16, spacing: 16,
children: [ children: [
Row(children: [ Row(
children: [
Text( Text(
"Lista de tareas", "Lista de tareas",
style: TextStyle(fontSize: 20, style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
),
), ),
Spacer(), Spacer(),
Text( Text(
"10 oct - 17 oct", "10 oct - 17 oct",
style: TextStyle(fontSize: 14, letterSpacing: 0), style: TextStyle(fontSize: 14, letterSpacing: 0),
) ),
]), ],
),
...List<CheckboxListTile>.generate( ...List<CheckboxListTile>.generate(
widget.tasks[i].subtasks.length, (int j) => widget.tasks[i].subtasks.length,
CheckboxListTile( (int j) => CheckboxListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
checkboxScaleFactor: 2, checkboxScaleFactor: 2,
value: widget.tasks[i].subtasks[j].completed, value: widget.tasks[i].subtasks[j].completed,
title: Text(widget.tasks[i].subtasks[j].name), title: Text(widget.tasks[i].subtasks[j].name),
controlAffinity: ListTileControlAffinity controlAffinity: ListTileControlAffinity.leading,
.leading,
activeColor: theme.getColorFor( activeColor: theme.getColorFor(
ThemeCode.buttonPrimary), ThemeCode.buttonPrimary,
onChanged: (_) => ),
setState(() { onChanged: (_) => setState(() {
widget.tasks[i].subtasks[j] = Subtask( widget.tasks[i].subtasks[j] = Subtask(
name: widget.tasks[i].subtasks[j] name: widget.tasks[i].subtasks[j].name,
.name, completed: !widget.tasks[i].subtasks[j].completed,
completed: !widget.tasks[i] );
.subtasks[j].completed);
}), }),
) ),
), ),
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets padding: WidgetStatePropertyAll(EdgeInsets.zero),
.zero)), ),
onPressed: () => {}, onPressed: () => {},
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: Text(
"+ Añadir tarea", "+ Añadir tarea",
style: TextStyle(fontSize: 18, style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
) ),
) ),
),
), ),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode color: theme.getColorFor(
.backgroundTertiary), ThemeCode.backgroundTertiary,
borderRadius: BorderRadius.all(Radius.circular( ),
24)) borderRadius: BorderRadius.all(Radius.circular(24)),
), ),
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 16), horizontal: 20,
vertical: 16,
),
child: Row( child: Row(
spacing: 16, spacing: 16,
children: [ children: [
Expanded(child: Text( Expanded(
child: Text(
"Recompensa por cumplimiento", "Recompensa por cumplimiento",
style: TextStyle( style: TextStyle(
fontSize: 16, letterSpacing: 0), fontSize: 16,
)), letterSpacing: 0,
),
),
),
Row( Row(
spacing: 8, spacing: 8,
children: [ children: [
Icon(Icons.emoji_events_outlined, size: 16, Icon(
Icons.emoji_events_outlined,
size: 16,
color: theme.getColorFor( color: theme.getColorFor(
ThemeCode.buttonPrimary)), ThemeCode.buttonPrimary,
),
),
MoneyText( MoneyText(
text: "${widget.tasks[i] text: "${widget.tasks[i].rewardAmount}",
.rewardAmount}",
size: 40, size: 40,
secondarySize: 24, secondarySize: 24,
color: theme.getColorFor( color: theme.getColorFor(
ThemeCode.buttonPrimary) ThemeCode.buttonPrimary,
) ),
),
], ],
) ),
], ],
), ),
), ),
@@ -730,17 +857,18 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: Text(
"Este es el mensaje que se enviará al cumplir con todas las tareas:", "Este es el mensaje que se enviará al cumplir con todas las tareas:",
style: TextStyle( style: TextStyle(fontSize: 14, letterSpacing: 0),
fontSize: 14, letterSpacing: 0),
), ),
), ),
Align( Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Text( child: Text(
"¡Enhorabuena, has cumplido todas las tareas de esta semana!", "¡Enhorabuena, has cumplido todas las tareas de esta semana!",
style: TextStyle(fontSize: 14, style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
),
), ),
), ),
], ],
@@ -752,21 +880,22 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
child: Row( child: Row(
spacing: 4, spacing: 4,
children: [ children: [
Icon(Icons.delete_outline_outlined, Icon(Icons.delete_outline_outlined, size: 24),
size: 24),
Text( Text(
"Eliminar lista de tareas", "Eliminar lista de tareas",
style: TextStyle(fontSize: 16, style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0), letterSpacing: 0,
) ),
],
)
), ),
)
], ],
), ),
) ),
),
],
),
),
), ),
SecondaryButton( SecondaryButton(
onPressed: () => {}, onPressed: () => {},
@@ -774,7 +903,7 @@ class TasksBlockState extends ConsumerState<TasksBlock>{
size: 14, size: 14,
), ),
], ],
) ),
); );
} else { } else {
return emptyBlock(fullPlan: true); return emptyBlock(fullPlan: true);

View File

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

View File

@@ -43,7 +43,7 @@ class _WageScreenState extends ConsumerState<WageScreen> {
PrimaryButton( PrimaryButton(
onPressed: () => {}, onPressed: () => {},
text: "Activar paga automática", text: "Activar paga automática",
color: theme.getColorFor(ThemeCode.buttonPrimary) color: theme.getColorFor(ThemeCode.buttonPrimary),
), ),
TextButton(onPressed: () {}, child: const Text('Cancelar')), TextButton(onPressed: () {}, child: const Text('Cancelar')),
], ],
@@ -59,14 +59,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column( child: Column(
spacing: 10, spacing: 10,
children: [ children: [
Align(alignment: Alignment.topLeft, Align(
alignment: Alignment.topLeft,
child: const Text( child: const Text(
"Paga automática", "Paga automática",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
), ),
), ),
CustomTextField( CustomTextField(
numeric: true, keyboardType: TextInputType.number,
label: "Cantidad", label: "Cantidad",
hint: "0€", hint: "0€",
), ),
@@ -84,13 +85,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column( child: Column(
spacing: 10, spacing: 10,
children: [ children: [
Align(alignment: Alignment.topLeft, Align(
alignment: Alignment.topLeft,
child: const Text( child: const Text(
"Frecuencia", "Frecuencia",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 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"), child: const Text("Cuándo se envía el dinero"),
), ),
CheckboxListTile( CheckboxListTile(
@@ -139,7 +142,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
Text("Sábado"), Text("Sábado"),
Text("Domingo"), Text("Domingo"),
], ],
values: ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"], values: [
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado",
"Domingo",
],
onChanged: (value) => {}, onChanged: (value) => {},
hint: "Día de la semana", hint: "Día de la semana",
), ),
@@ -174,13 +185,15 @@ class _WageScreenState extends ConsumerState<WageScreen> {
child: Column( child: Column(
spacing: 10, spacing: 10,
children: [ children: [
const Align(alignment: Alignment.topLeft, const Align(
alignment: Alignment.topLeft,
child: Text( child: Text(
"Condiciones", "Condiciones",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), 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"), child: Text("Este dato aparecerá en el reloj del peque"),
), ),
CheckboxListTile( CheckboxListTile(

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget { class PrimaryButton extends StatelessWidget {
final VoidCallback onPressed; final VoidCallback? onPressed;
final String text; final String text;
final Color color; final Color color;
final double height; final double height;
@@ -9,24 +9,35 @@ class PrimaryButton extends StatelessWidget {
final double size; final double size;
final double radius; final double radius;
final double padding; final double padding;
final Widget? leading;
final double leadingGap;
const PrimaryButton({ const PrimaryButton({
super.key, super.key,
required this.onPressed,
required this.text, required this.text,
required this.color, required this.color,
this.onPressed,
this.height = 60, this.height = 60,
this.width, this.width,
this.size = 18, this.size = 18,
this.radius = 18, this.radius = 18,
this.padding = 0, this.padding = 0,
this.leading,
this.leadingGap = 10,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bgResolver = WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.disabled)) {
return color.withValues(alpha: 0.5);
}
return color;
});
return FilledButton( return FilledButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(color), backgroundColor: bgResolver,
padding: WidgetStatePropertyAll<EdgeInsetsGeometry>( padding: WidgetStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsets.symmetric(horizontal: padding), EdgeInsets.symmetric(horizontal: padding),
), ),
@@ -40,17 +51,29 @@ class PrimaryButton extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: width, width: width,
height: height, height: height,
child: Center( child: Row(
child: Text( mainAxisAlignment: MainAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
children: [
if (leading != null) ...[
IconTheme.merge(
data: const IconThemeData(color: Colors.white, size: 20),
child: leading!,
),
SizedBox(width: leadingGap),
],
Text(
text, text,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: size, fontSize: size,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
letterSpacing: 0, letterSpacing: 0,
color: Colors.white, // theme.getColorFor(ThemeCode.textSecondary) color: Colors.white,
), ),
), ),
],
), ),
), ),
); );

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextField extends StatelessWidget { class CustomTextField extends StatelessWidget {
final bool? showPassword; final bool? showPassword;
final TextInputType keyboardType;
final TextInputAction? textInputAction;
final ValueChanged<String>? onSubmitted;
final VoidCallback? onVisibilityChanged; final VoidCallback? onVisibilityChanged;
final bool numeric;
final String hint; final String hint;
final String label; final String label;
final double labelSize; final double labelSize;
@@ -18,8 +19,10 @@ class CustomTextField extends StatelessWidget {
const CustomTextField({ const CustomTextField({
super.key, super.key,
this.showPassword, this.showPassword,
this.keyboardType = TextInputType.text,
this.textInputAction,
this.onSubmitted,
this.onVisibilityChanged, this.onVisibilityChanged,
this.numeric = false,
this.hint = '', this.hint = '',
this.label = '', this.label = '',
this.labelSize = 14, this.labelSize = 14,
@@ -45,17 +48,14 @@ class CustomTextField extends StatelessWidget {
), ),
), ),
TextFormField( TextFormField(
controller: controller, onFieldSubmitted: widget.onSubmitted,
keyboardType: numeric textInputAction: widget.textInputAction,
? TextInputType.number controller: widget.controller,
: TextInputType.text, keyboardType: widget.keyboardType,
obscureText: !(showPassword ?? true), obscureText: !(showPassword ?? true),
enableSuggestions: (showPassword ?? true), enableSuggestions: (showPassword ?? true),
autocorrect: !(showPassword ?? true), autocorrect: !(showPassword ?? true),
style: const TextStyle(color: Color(0xFF4B4B4B)), style: const TextStyle(color: Color(0xFF4B4B4B)),
inputFormatters: numeric
? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly]
: const <TextInputFormatter>[],
decoration: InputDecoration( decoration: InputDecoration(
counterText: "", counterText: "",
hintText: hint, hintText: hint,

View File

@@ -93,7 +93,11 @@ void main() {
) )
..addScenario( ..addScenario(
'numeric', 'numeric',
SizedBox(height: 70, width: 250, child: CustomTextField(numeric: true)), SizedBox(
height: 70,
width: 250,
child: CustomTextField(keyboardType: TextInputType.number),
),
) )
..addScenario( ..addScenario(
'password', 'password',

View File

@@ -21,6 +21,17 @@
"enter": "Weiter", "enter": "Weiter",
"didNotReceiveIt": "Hast du es nicht erhalten?", "didNotReceiveIt": "Hast du es nicht erhalten?",
"tryAgain": "Erneut versuchen", "tryAgain": "Erneut versuchen",
"welcome": "Wir heißen dich willkommen!",
"username": "Benutzername",
"password": "Passwort",
"forgotPassword": "Passwort vergessen?",
"signIn": "Anmelden",
"orContinueWith": "oder weiter mit",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "Du hast noch kein Konto?",
"createOneNow": "Jetzt eines erstellen",
"tryAgain": "Erneut versuchen",
"recoverPasswordTitle": "Passwort wiederherstellen", "recoverPasswordTitle": "Passwort wiederherstellen",
"recoverPasswordSubtitle": "Geben Sie Ihre E-Mail-Adresse ein, um Ihnen einen Wiederherstellungslink zu senden", "recoverPasswordSubtitle": "Geben Sie Ihre E-Mail-Adresse ein, um Ihnen einen Wiederherstellungslink zu senden",
"send": "Schicken", "send": "Schicken",

View File

@@ -21,6 +21,17 @@
"enter": "Enter", "enter": "Enter",
"didNotReceiveIt": "Didn't receive it?", "didNotReceiveIt": "Didn't receive it?",
"tryAgain": "Try again", "tryAgain": "Try again",
"welcome": "Welcome!",
"username": "Username",
"password": "Password",
"forgotPassword": "Forgot your password?",
"signIn": "Sign in",
"orContinueWith": "or continue with",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "Don't have an account?",
"createOneNow": "Create one now",
"tryAgain": "Try again",
"recoverPasswordTitle": "Recover password", "recoverPasswordTitle": "Recover password",
"recoverPasswordSubtitle": "Insert your email to send you a recovery link", "recoverPasswordSubtitle": "Insert your email to send you a recovery link",
"send": "Send", "send": "Send",

View File

@@ -21,6 +21,17 @@
"enter": "enter", "enter": "enter",
"didNotReceiveIt": "¿No lo has recibido?", "didNotReceiveIt": "¿No lo has recibido?",
"tryAgain": "Volver a intentarlo", "tryAgain": "Volver a intentarlo",
"welcome": "¡Te damos la bienvenida!",
"username": "Nombre de usuario",
"password": "Contraseña",
"forgotPassword": "¿Has olvidado la contraseña?",
"signIn": "Iniciar sesión",
"orContinueWith": "o continúa con",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "¿No tienes cuenta?",
"createOneNow": "Crear una ahora",
"tryAgain": "Volver a intentarlo",
"recoverPasswordTitle": "Recuperar contraseña", "recoverPasswordTitle": "Recuperar contraseña",
"recoverPasswordSubtitle": "Introduce tu email para enviarte un enlace de recuperación", "recoverPasswordSubtitle": "Introduce tu email para enviarte un enlace de recuperación",
"send": "Enviar", "send": "Enviar",

View File

@@ -21,6 +21,17 @@
"enter": "Entrer", "enter": "Entrer",
"didNotReceiveIt": "Tu ne l'as pas reçu ?", "didNotReceiveIt": "Tu ne l'as pas reçu ?",
"tryAgain": "Réessayer", "tryAgain": "Réessayer",
"welcome": "Nous te souhaitons la bienvenue !",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"forgotPassword": "Mot de passe oublié ?",
"signIn": "Se connecter",
"orContinueWith": "ou continuer avec",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "Tu n'as pas de compte ?",
"createOneNow": "Crée-en un maintenant",
"tryAgain": "Réessayer",
"recoverPasswordTitle": "Récupérer le mot de passe", "recoverPasswordTitle": "Récupérer le mot de passe",
"recoverPasswordSubtitle": "Entrez votre email pour vous envoyer un lien de récupération", "recoverPasswordSubtitle": "Entrez votre email pour vous envoyer un lien de récupération",
"send": "Envoyer", "send": "Envoyer",

View File

@@ -21,6 +21,17 @@
"enter": "Entra", "enter": "Entra",
"didNotReceiveIt": "Non lo hai ricevuto?", "didNotReceiveIt": "Non lo hai ricevuto?",
"tryAgain": "Riprova", "tryAgain": "Riprova",
"welcome": "Ti diamo il benvenuto!",
"username": "Nome utente",
"password": "Password",
"forgotPassword": "Hai dimenticato la password?",
"signIn": "Accedi",
"orContinueWith": "o continua con",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "Non hai un account?",
"createOneNow": "Creane uno adesso",
"tryAgain": "Riprova",
"recoverPasswordTitle": "Recupera la password", "recoverPasswordTitle": "Recupera la password",
"recoverPasswordSubtitle": "Inserisci la tua email per inviarti un collegamento di recupero", "recoverPasswordSubtitle": "Inserisci la tua email per inviarti un collegamento di recupero",
"send": "Inviare", "send": "Inviare",

View File

@@ -21,6 +21,17 @@
"enter": "Entrar", "enter": "Entrar",
"didNotReceiveIt": "Você não recebeu?", "didNotReceiveIt": "Você não recebeu?",
"tryAgain": "Tentar novamente", "tryAgain": "Tentar novamente",
"welcome": "Bem-vindo!",
"username": "Nome de usuário",
"password": "Senha",
"forgotPassword": "Esqueceu a senha?",
"signIn": "Iniciar sessão",
"orContinueWith": "ou continuar com",
"google": "Google",
"apple": "Apple",
"dontHaveAccount": "Não tem conta?",
"createOneNow": "Criar uma agora",
"tryAgain": "Tentar novamente",
"recoverPasswordTitle": "Recuperar senha", "recoverPasswordTitle": "Recuperar senha",
"recoverPasswordSubtitle": "Insira seu e-mail para enviar um link de recuperação", "recoverPasswordSubtitle": "Insira seu e-mail para enviar um link de recuperação",
"send": "Enviar", "send": "Enviar",

View File

@@ -3,6 +3,38 @@
class I18n { class I18n {
const I18n._(); const I18n._();
static const String example = 'example';
static const String start = 'start';
static const String next = 'next';
static const String skip = 'skip';
static const String onboardingTitle1 = 'onboardingTitle1';
static const String onboardingSubtitle1 = 'onboardingSubtitle1';
static const String onboardingTitle2 = 'onboardingTitle2';
static const String onboardingSubtitle2 = 'onboardingSubtitle2';
static const String onboardingTitle3 = 'onboardingTitle3';
static const String onboardingSubtitle3 = 'onboardingSubtitle3';
static const String linkPhoneTitle = 'linkPhoneTitle';
static const String linkPhoneSubtitle = 'linkPhoneSubtitle';
static const String mobilePhone = 'mobilePhone';
static const String phoneNumber = 'phoneNumber';
static const String selectYourCountry = 'selectYourCountry';
static const String errorMessagePhoneIsEmpty = 'errorMessagePhoneIsEmpty';
static const String connect = "connect";
static const String verificationCodeSentTo = "verificationCodeSentTo";
static const String enterCodeHere = "enterCodeHere";
static const String enter = "enter";
static const String didNotReceiveIt = "didNotReceiveIt";
static const String tryAgain = "tryAgain";
static const String welcome = "welcome";
static const String username = "username";
static const String password = "password";
static const String forgotPassword = "forgotPassword";
static const String signIn = "signIn";
static const String orContinueWith = "orContinueWith";
static const String google = "google";
static const String apple = "apple";
static const String dontHaveAccount = "dontHaveAccount";
static const String createOneNow = "createOneNow";
static const example = 'example'; static const example = 'example';
static const onboardingTitle1 = 'onboardingTitle1'; static const onboardingTitle1 = 'onboardingTitle1';
static const onboardingSubtitle1 = 'onboardingSubtitle1'; static const onboardingSubtitle1 = 'onboardingSubtitle1';

View File

@@ -38,7 +38,7 @@ class DepositBlock extends ConsumerWidget {
child: CustomTextField( child: CustomTextField(
label: "Cantidad", label: "Cantidad",
hint: "0€", hint: "0€",
numeric: true, keyboardType: TextInputType.number,
), ),
), ),
Align( Align(
@@ -49,7 +49,7 @@ class DepositBlock extends ConsumerWidget {
color: theme.getColorFor(ThemeCode.buttonPrimary), color: theme.getColorFor(ThemeCode.buttonPrimary),
padding: 24, padding: 24,
), ),
) ),
], ],
), ),
Align( Align(
@@ -60,10 +60,10 @@ class DepositBlock extends ConsumerWidget {
Icon(Icons.info_outline, size: 16), Icon(Icons.info_outline, size: 16),
Text("Máximo que puedes añadir: $max"), Text("Máximo que puedes añadir: $max"),
], ],
) ),
), ),
], ],
) ),
], ],
), ),
); );