From b9e87f457b4b627d627109925addb39bcc7f502b Mon Sep 17 00:00:00 2001 From: Santiago Gaona <53282017+SantiagoGaonaC@users.noreply.github.com> Date: Mon, 9 Oct 2023 19:48:37 -0500 Subject: [PATCH] feat: register & biometric added (#24) (#29) * feat: new type connection to proxy (#9) * refactor: change type variable .env * feat: connection to the proxy via HTTP & check new endpoints * feat: terms and conditions modal Terms and conditions modal is added at the time of user registration. In addition, the auto_size_text package is added for text management on different devices * feat: validation of fields in the registration form Implementation of the registration form fields with their validation. * feat: validating equal passwords in form. Password validation is done on the registration form, in addition to using modal when validating fields. * feat: register options with merge biometricts login registration functionality through username and password * feat: register added (#26) & change to secure storage * feat: customErros added of proxy response (#25) * refactor: recome closeModalCallback * fix: temporary authentication & authentication with biometrics under states (#24) --- android/build.gradle | 2 +- lib/config/router/app_router.dart | 5 +- lib/features/auth/domain/entities/folder.dart | 10 + lib/features/auth/domain/entities/user.dart | 2 +- .../datasources/auth_datasource_impl.dart | 32 ++- .../infrastructure/errors/auth_errors.dart | 14 +- .../presentation/providers/auth_provider.dart | 110 +++++-- .../presentation/providers/data_test.dart | 48 ++++ .../providers/login_form_provider.dart | 24 +- .../providers/register_form_provider.dart | 121 ++++++++ .../presentation/screens/login_screen.dart | 103 +++++-- .../presentation/screens/register_screen.dart | 270 ++++++++++++------ .../presentation/screens/drive_screen.dart | 77 ++++- .../presentation/screens/storage_screen.dart | 6 +- .../infrastructure/inputs/password.dart | 2 +- .../inputs/repeat_password.dart | 40 +++ .../services/key_value_storage_impl.dart | 35 +-- lib/features/shared/widgets/bottom_sheet.dart | 41 +-- .../widgets/custom_text_form_field.dart | 6 +- lib/features/shared/widgets/folder.dart | 0 lib/features/shared/widgets/side_menu.dart | 4 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 +++++ pubspec.yaml | 2 + test/widget_test.dart | 3 - .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + windows/runner/flutter_window.cpp | 5 + 30 files changed, 810 insertions(+), 227 deletions(-) create mode 100644 lib/features/auth/domain/entities/folder.dart create mode 100644 lib/features/auth/presentation/providers/data_test.dart create mode 100644 lib/features/shared/infrastructure/inputs/repeat_password.dart create mode 100644 lib/features/shared/widgets/folder.dart diff --git a/android/build.gradle b/android/build.gradle index 0bd4dfd..9af1eea 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,4 @@ -buildscript { +buildscript{ ext.kotlin_version = '1.7.10' repositories { google() diff --git a/lib/config/router/app_router.dart b/lib/config/router/app_router.dart index 0dc957d..f7e2032 100644 --- a/lib/config/router/app_router.dart +++ b/lib/config/router/app_router.dart @@ -54,7 +54,7 @@ final goRouterProvider = Provider((ref) { return '/login'; } - if (authStatus == AuthStatus.authenticated) { + if ((authStatus == AuthStatus.authenticated)) { if (isGoingTo == '/login' || isGoingTo == '/register' || isGoingTo == '/check-auth-status') { @@ -64,5 +64,6 @@ final goRouterProvider = Provider((ref) { return null; }, + ); -}); +}); \ No newline at end of file diff --git a/lib/features/auth/domain/entities/folder.dart b/lib/features/auth/domain/entities/folder.dart new file mode 100644 index 0000000..d86e425 --- /dev/null +++ b/lib/features/auth/domain/entities/folder.dart @@ -0,0 +1,10 @@ + class Folder{ + //final int id; + final String name; + final int color; + + Folder({ + required this.name, + required this.color, + }); +} \ No newline at end of file diff --git a/lib/features/auth/domain/entities/user.dart b/lib/features/auth/domain/entities/user.dart index b24507c..2285b85 100644 --- a/lib/features/auth/domain/entities/user.dart +++ b/lib/features/auth/domain/entities/user.dart @@ -1,6 +1,6 @@ class User { //final String uuid; - final String? username; + String? username; //final int statusCode; //final String? message; final String token; diff --git a/lib/features/auth/infrastructure/datasources/auth_datasource_impl.dart b/lib/features/auth/infrastructure/datasources/auth_datasource_impl.dart index 528b611..617c162 100644 --- a/lib/features/auth/infrastructure/datasources/auth_datasource_impl.dart +++ b/lib/features/auth/infrastructure/datasources/auth_datasource_impl.dart @@ -12,7 +12,7 @@ class AuthDataSourceImpl extends AuthDataSource { @override Future checkAuthStatus(String token) async { try { - final response = await dio.post('/refreshtoken', + final response = await dio.post('/auth/refresh', data: {'token': token}, options: Options(headers: {'Authorization': 'Bearer $token'})); final user = UserMapper.userJsonToEntity(response.data); @@ -30,13 +30,15 @@ class AuthDataSourceImpl extends AuthDataSource { @override Future login(String username, String password) async { try { - final responde = await dio - .post('/login', data: {'username': username, 'password': password}); + final responde = await dio.post('/auth/login', + data: {'username': username, 'password': password}); final user = UserMapper.userJsonToEntity(responde.data); return user; } on DioException catch (e) { if (e.response?.statusCode == 401) { - throw CustomError(e.response?.data['message'] ?? 'Credentials wrong'); + throw CustomError( + /*e.response?.data ?? */ + 'Username or Password wrong'); } if (e.type == DioExceptionType.connectionTimeout) { throw CustomError('Review your internet connection'); @@ -48,8 +50,24 @@ class AuthDataSourceImpl extends AuthDataSource { } @override - Future register(String username, String password) { - // TODO: implement register - throw UnimplementedError(); + Future register(String username, String password) async { + try { + final responde = await dio.post('/account/register', + data: {'username': username, 'password': password}); + final user = UserMapper.userJsonToEntity(responde.data); + return user; + } on DioException catch (e) { + if (e.response?.statusCode == 409) { + throw CustomError( + /*e.response?.data['message'] ?? */ + 'Username already registered'); + } + if (e.type == DioExceptionType.connectionTimeout) { + throw CustomError('Review your internet connection'); + } + throw Exception(); + } catch (e) { + throw Exception(); + } } } diff --git a/lib/features/auth/infrastructure/errors/auth_errors.dart b/lib/features/auth/infrastructure/errors/auth_errors.dart index 5083778..20c95bb 100644 --- a/lib/features/auth/infrastructure/errors/auth_errors.dart +++ b/lib/features/auth/infrastructure/errors/auth_errors.dart @@ -1,14 +1,12 @@ class WrongCredentials implements Exception { - -} - -class InvalidToken implements Exception { - + final String message; + + WrongCredentials(this.message); } -class ConectionTimeout implements Exception { +class InvalidToken implements Exception {} -} +class ConectionTimeout implements Exception {} class CustomError implements Exception { final String message; @@ -19,4 +17,4 @@ class CustomError implements Exception { CustomError(this.message, [this.loggedRequired = false]); */ CustomError(this.message); -} \ No newline at end of file +} diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index a4946c1..6f4adb3 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -4,10 +4,11 @@ Nos permite a nosotros llegar a nuestro backend directamente (Repositorio -> DataSource -> Backend) **************************************************************** DataSource tiene las conexiones e implementaciones necesarias +token -> biometric activate +tempToken -> login/register */ import 'dart:async'; - import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:local_auth/local_auth.dart'; @@ -33,12 +34,10 @@ class AuthNotifier extends StateNotifier { final AuthRepository authRepository; final KeyValueStorageService keyValueStorageService; final LocalAuthentication auth = LocalAuthentication(); - final Function? closeModalCallback; AuthNotifier({ required this.authRepository, required this.keyValueStorageService, - this.closeModalCallback, }) : super(AuthState()) { checkAuthStatus(); } //no hace falta mandar nada porque todo son valores opcinonales @@ -49,16 +48,15 @@ class AuthNotifier extends StateNotifier { await Future.delayed(const Duration(milliseconds: 500)); try { final user = await authRepository.login(username, password); - _setUsername(username); + //De momento el repositorio no devuelve el username solo token + /// se asigna el username al User + user.username = username; if (biometric == false) { _setTempLoggedUser(user); } if (biometric == true && state.user != null) { _setBiometricLoggedUser(user); } - if (closeModalCallback != null) { - closeModalCallback!(); - } } on CustomError catch (e) { if (biometric == false) { logout(e.message); @@ -74,15 +72,57 @@ class AuthNotifier extends StateNotifier { } } - //TODO: implementar el registro - void registerUser(String email, String password) async {} + Future registerUser(String username, String password) async { + await Future.delayed(const Duration(milliseconds: 500)); + try { + final user = await authRepository.register(username, password); + user.username = username; + _setTempLoggedUser(user); + } on CustomError catch (e) { + noRegister(e.message); + } catch (e) { + noRegister('Something went wrong'); + } + } + + void noRegister([String? errorMessage]) async { + state = state.copyWith( + user: null, + errorMessage: errorMessage, + authStatus: AuthStatus.notAuthenticated); + } //ver si es valido el token que contiene el usuario void checkAuthStatus() async { - final token = await keyValueStorageService.getValue('token'); - if (token == null) return logout(); + //Manejamos los estados de auth dependiendo del token si existe o no + //Si existe el token, lo verificamos + //Primero intentamos con el token de biometría + final biometricToken = + await keyValueStorageService.getValue('token'); + if (biometricToken != null) { + _verifyToken(biometricToken); + return; + } + //Después intentamos con el token temporal + final tempToken = + await keyValueStorageService.getValue('tempToken'); + if (tempToken != null) { + // Se hace el logout para que se borre el token temporal + // Si desea un usuario temp volver a entrar se borra + // Si se quiere cambiar el estado entonces hacer un _verifyTokenTemp(tempToken) + logout(); + //_verifyToken(tempToken); + return; + } + //Si llegamos hasta aquí, no hay token. + logout(); + } + + void _verifyToken(String token) async { try { final user = await authRepository.checkAuthStatus(token); + // Set new token + await keyValueStorageService.setKeyValue('token', user.token); _setBiometricLoggedUser(user); } catch (e) { logout(); @@ -90,12 +130,16 @@ class AuthNotifier extends StateNotifier { } void _setTempLoggedUser(User user) async { + // Si es temp no se guarda el username pero se guarda el token y el state + await keyValueStorageService.setKeyValue('tempToken', user.token); state = state.copyWith( user: user, errorMessage: '', authStatus: AuthStatus.authenticated); } Future _setBiometricLoggedUser(User user) async { + String usernameAsString = user.username.toString(); await keyValueStorageService.setKeyValue('token', user.token); + await keyValueStorageService.setKeyValue('username', usernameAsString); await keyValueStorageService.setKeyValue('hasBiometric', true); state = state.copyWith( user: user, @@ -108,10 +152,12 @@ class AuthNotifier extends StateNotifier { void disableBiometric(ref) async { await keyValueStorageService.removeKey('token'); await keyValueStorageService.removeKey('hasBiometric'); + await keyValueStorageService.removeKey('username'); + await keyValueStorageService.removeKey('tempToken'); state = state.copyWith( user: null, errorMessage: '', - authStatus: AuthStatus.authenticated, + authStatus: AuthStatus.notAuthenticated, hasBiometric: false); //Aqui puedo abrir otro modal // showModalBottomSheet( @@ -120,13 +166,17 @@ class AuthNotifier extends StateNotifier { // builder: (context) => const CustomLogin()); } - void _setUsername(String username) async { - await keyValueStorageService.setKeyValue('username', username); - } - +//Elimina el token solamente cuando la biometría no está activa Future logout([String? errorMessage]) async { - //await keyValueStorageService.removeKey('token'); - await keyValueStorageService.removeKey('username'); + final hasBiometric = state.hasBiometric; + // Always delete the 'tempToken' + await keyValueStorageService.removeKey('tempToken'); + // If the user has biometric, delete the token and username + if (hasBiometric != true) { + await keyValueStorageService.removeKey('token'); + await keyValueStorageService.removeKey('username'); + } + state = state.copyWith( user: null, errorMessage: errorMessage, @@ -135,14 +185,15 @@ class AuthNotifier extends StateNotifier { Future biometricError([String? errorMessage]) async { state = state.copyWith( - errorMessage: errorMessage, - ); + user: null, + errorMessage: errorMessage, + authStatus: AuthStatus.notAuthenticated); } +// se usa en handleBiometricAuthentication() de side_menu.dart Future authWithBiometrics() async { bool authenticated = false; try { - state = state.copyWith(authStatus: AuthStatus.checking); authenticated = await auth.authenticate( localizedReason: 'Scan your fingerprint (or face or whatever) to authenticate', @@ -154,10 +205,14 @@ class AuthNotifier extends StateNotifier { if (authenticated) { state = state.copyWith(authStatus: AuthStatus.authenticated); } else { - state = state.copyWith(authStatus: AuthStatus.notAuthenticated); + state = state.copyWith( + authStatus: AuthStatus.notAuthenticated, + user: null, + errorMessage: 'Biometric authentication failed'); } } on PlatformException catch (e) { state = state.copyWith( + user: null, errorMessage: 'Error - ${e.message}', authStatus: AuthStatus.notAuthenticated); } @@ -168,13 +223,22 @@ class AuthNotifier extends StateNotifier { try { final token = await keyValueStorageService.getValue('token'); if (token == null) { + state = state.copyWith( + errorMessage: 'Token not found', + authStatus: AuthStatus.notAuthenticated, + ); return false; } + //!TODO: Validar si es necesario guardar el token acá final user = await authRepository.checkAuthStatus(token); + await keyValueStorageService.setKeyValue('token', user.token); + final username = + await keyValueStorageService.getValue('username'); + user.username = username; state = state.copyWith( user: user, errorMessage: '', - authStatus: AuthStatus.authenticated, + authStatus: AuthStatus.checking, ); authWithBiometrics(); return true; diff --git a/lib/features/auth/presentation/providers/data_test.dart b/lib/features/auth/presentation/providers/data_test.dart new file mode 100644 index 0000000..46a49d1 --- /dev/null +++ b/lib/features/auth/presentation/providers/data_test.dart @@ -0,0 +1,48 @@ +String termsandcondition = ''' +September 2023 +Welcome to CapyFile. +These terms and conditions describe the rules and regulations for the use of CapyFile. + +By registering for this application, we assume that you accept these terms and conditions in full. Do not continue to use CapyFile if you do not agree to all the terms and conditions set forth below + +Access + +To participate in the CapyFile experience, you must allow the app to obtain information and access: +• Mobile device camera +• Device storage +• Image gallery + +Age requirements + +To use CapyFile you must be of legal age (18 years old). If for any reason we detect that you do not comply with this requirement, we have the authority to disable your account. +Service +Using CapyFile you can store, send, download, receive and delete files. The files you choose to store are your property, at no time will we claim credits for the content you store, share or receive, in addition to this, we will not use your files for advertising or promotional campaigns. +You have the control to decide who accesses your files, if you decide to share any files, the user who receives the content must be registered in our application, in addition to being logged in to view the file and enjoy the other features offered by CapyFile. + +Restrictions + +We may access your files for the sole purpose of ensuring that the content does not violate the rules for Prohibited content and if we find a violation, you may not be able to upload files, share them, or even your account may be inactive. +It is important that you know the content not allowed so as not to fall into infractions that bring sanctions that may bother you. + +Inactivity + +To keep your account active, you need to use the app at least once every 6 months, Otherwise, we have the permission to inactivate your account which gives way to delete the files and in general all the content related to your account. +Prohibited content +You have the freedom to use CapyFile to upload and share content from different areas, however, it is not allowed to upload and share content related to + +• Violent content. +• Harassment and threats. +• Sexual exploitation and abuse. +• Non-consensual explicit images. +• Unauthorized images of minors. +• Criminal activity. +• Identity theft. +• Malicious software. + +If we detect content of the points exposed above, your account will be inactive and the content you have uploaded to your account, which relates to the topics not allowed, as well as files that do not contain these themes will be deleted. + +Storage. + +Each user has a storage limit to upload files and save them in CapyFile, if you exceed this limit, you will not be able to upload more files to your account, in the same way, you always have the option to delete to manage your storage. + +'''; \ No newline at end of file diff --git a/lib/features/auth/presentation/providers/login_form_provider.dart b/lib/features/auth/presentation/providers/login_form_provider.dart index 8e834c8..8113135 100644 --- a/lib/features/auth/presentation/providers/login_form_provider.dart +++ b/lib/features/auth/presentation/providers/login_form_provider.dart @@ -83,25 +83,23 @@ class LoginFormNotifier extends StateNotifier { state = state.copyWith(isPosting: false); } - onFormSubmittedBiometric() async { - _touchEveryField(); - if (state.isValid) { - return await loginUserCallback(state.username.value, state.password.value, - biometric: true); - } - state = state.copyWith(isPosting: true); //actualiza el estado + onFormSubmittedBiometric(String username) async { + //Validate only the password since the username comes from authState. + final newPassword = Password.dirty(state.password.value); - state = state.copyWith(isPosting: false); - } + state = state.copyWith( + isFormPosted: true, + password: newPassword, + isValid: Formz.validate([newPassword]), + ); - onFormAuthBiometric() async { - _touchEveryField(); if (!state.isValid) { return; } - state = state.copyWith(isPosting: true); //actualiza el estado - await loginUserCallback(state.username.value, state.password.value); + state = state.copyWith(isPosting: true); + + await loginUserCallback(username, state.password.value, biometric: true); state = state.copyWith(isPosting: false); } diff --git a/lib/features/auth/presentation/providers/register_form_provider.dart b/lib/features/auth/presentation/providers/register_form_provider.dart index e69de29..e48f59a 100644 --- a/lib/features/auth/presentation/providers/register_form_provider.dart +++ b/lib/features/auth/presentation/providers/register_form_provider.dart @@ -0,0 +1,121 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:formz/formz.dart'; +import 'package:login_mobile/features/auth/presentation/providers/auth_provider.dart'; +import 'package:login_mobile/features/shared/infrastructure/inputs/repeat_password.dart'; +import 'package:login_mobile/features/shared/shared.dart'; + +class RegisterFormState { + final bool isPosting; + final bool isFormPosted; + final bool isValid; + final Username username; + final Password password; + final RepeatPassword repeatPassword; + + RegisterFormState( + {this.isPosting = false, + this.isFormPosted = false, + this.isValid = false, + this.username = const Username.pure(), //entrada sin modif + this.password = const Password.pure(), //entrada sin modif + this.repeatPassword = const RepeatPassword.pure()}); + + RegisterFormState copyWith({ + bool? isPosting, + bool? isFormPosted, + bool? isValid, + Username? username, + Password? password, + RepeatPassword? repeatPassword, + }) => + RegisterFormState( + //por defecto tiene el valor del estado inicial + isPosting: isPosting ?? this.isPosting, + isFormPosted: isFormPosted ?? this.isFormPosted, + isValid: isValid ?? this.isValid, + username: username ?? this.username, + password: password ?? this.password, + repeatPassword: repeatPassword ?? this.repeatPassword, + ); + + @override + String toString() { + return ''' + isPosting: $isPosting, + isFormPosted: $isFormPosted, + isValid: $isValid, + username: $username, + password: $password, + repeatPassword: $repeatPassword, + '''; + } +} + +class RegisterFormNotifier extends StateNotifier { + final Function(String, String) registerUserCallback; + RegisterFormNotifier({required this.registerUserCallback}) + : super(RegisterFormState()); + + onUsernameChange(String value) { + final newUsername = Username.dirty(value); + state = state.copyWith( + username: newUsername, + isValid: Formz.validate( + [newUsername, state.password, state.repeatPassword])); + } + + onPasswordChanged(String value) { + final newPassword = Password.dirty(value); + state = state.copyWith( + password: newPassword, + isValid: Formz.validate( + [newPassword, state.username, state.repeatPassword])); + } + + onRepeatPasswordChanged(String value) { + if(value != state.password.value){ + value = "no"; + } + final newRepeatPassword = RepeatPassword.dirty(value); + state = state.copyWith( + repeatPassword: newRepeatPassword, + isValid: Formz.validate( + [newRepeatPassword, state.username, state.password])); + } + + + onFormSubmitted() async { + _touchEveryField(); + if (!state.isValid) { + return; + } + state = state.copyWith(isPosting: true); + + await registerUserCallback(state.username.value, state.password.value); + + state = state.copyWith(isPosting: false); + } + + + + _touchEveryField() { + + final username = Username.dirty(state.username.value); + final password = Password.dirty(state.password.value); + final repeatpassword = RepeatPassword.dirty(state.repeatPassword.value); + state = state.copyWith( + isFormPosted: true, + username: username, + password: password, + repeatPassword: repeatpassword, + isValid: Formz.validate([username, password, repeatpassword])); + + } +} + +final registerFormProvider = + StateNotifierProvider.autoDispose( + (ref) { + final registerUserCallback = ref.watch(authProvider.notifier).registerUser; + return RegisterFormNotifier(registerUserCallback: registerUserCallback); +}); diff --git a/lib/features/auth/presentation/screens/login_screen.dart b/lib/features/auth/presentation/screens/login_screen.dart index 967c090..22ff53c 100644 --- a/lib/features/auth/presentation/screens/login_screen.dart +++ b/lib/features/auth/presentation/screens/login_screen.dart @@ -77,6 +77,8 @@ class _LoginForm extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final textStyles = Theme.of(context).textTheme; final hasBiometric = ref.watch(authProvider).hasBiometric; + final userState = ref.watch(authProvider).user; + String username = userState?.username ?? ''; final loginForm = ref.watch(loginFormProvider); //acceso al state no al notifier @@ -90,15 +92,23 @@ class _LoginForm extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 50), child: Column( children: [ - const SizedBox(height: 50), + const SizedBox(height: 30), Text('Login', style: textStyles.titleLarge), - const SizedBox(height: 90), + hasBiometric == true + ? Text('Welcome back $username', style: textStyles.bodyLarge) + : const SizedBox(height: 0), + const SizedBox(height: 30), + hasBiometric == false ? CustomTextFormField( label: 'Username', - keyboardType: TextInputType.emailAddress, + keyboardType: TextInputType.name, onChanged: ref.read(loginFormProvider.notifier).onUsernameChange, errorMessage: loginForm.isFormPosted ? loginForm.username.errorMessage : null, + ) : CustomTextFormField( + label: username, + keyboardType: TextInputType.name, + readOnly: true, ), const SizedBox(height: 30), CustomTextFormField( @@ -110,23 +120,35 @@ class _LoginForm extends ConsumerWidget { ), const SizedBox(height: 30), SizedBox( - width: double.infinity, - height: 60, - child: CustomFilledButton( - text: 'Sign in', - buttonColor: Colors.black, - onPressed: loginForm.isPosting - ? null - : ref - .read(loginFormProvider.notifier) - .onFormSubmitted //si no posteo mando ref a la func - )), + width: double.infinity, + height: 60, + child: CustomFilledButton( + text: 'Sign in', + buttonColor: Colors.black, + onPressed: loginForm.isPosting + ? null + : hasBiometric == true + ? () { + ref + .read(loginFormProvider.notifier) + .onFormSubmittedBiometric(username); + } + : () { + ref + .read(loginFormProvider.notifier) + .onFormSubmitted(); + }, + ), + ), const SizedBox(height: 10), if (hasBiometric == true) - SizedBox( - width: double.infinity, - height: 60, - child: CustomFilledButton( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: double.infinity, + height: 60, + child: CustomFilledButton( text: 'Sign in with biometric', buttonColor: Colors.black, onPressed: loginForm.isPosting @@ -135,18 +157,39 @@ class _LoginForm extends ConsumerWidget { ref .read(authProvider.notifier) .loginWithBiometrics(); - })), - const Spacer(flex: 2), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () => context.push('/register'), - child: const Text('Create account', - style: TextStyle(color: Color.fromRGBO(32, 159, 168, 1))), - ) - ], - ), + }, + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + ref.watch(authProvider.notifier).disableBiometric(ref); + }, + child: const Text( + 'Login with other account', + style: TextStyle( + color: Color.fromRGBO(27, 122, 129, 1), + ), + ), + ), + ], + ), + ], + ), + if (hasBiometric == false) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => context.push('/register'), + child: const Text('Create account', + style: TextStyle(color: Color.fromRGBO(32, 159, 168, 1))), + ) + ], + ), const Spacer(flex: 1), ], ), diff --git a/lib/features/auth/presentation/screens/register_screen.dart b/lib/features/auth/presentation/screens/register_screen.dart index 0182a3e..2b28102 100644 --- a/lib/features/auth/presentation/screens/register_screen.dart +++ b/lib/features/auth/presentation/screens/register_screen.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:login_mobile/features/auth/presentation/providers/providers.dart'; import 'package:login_mobile/features/shared/shared.dart'; - +import 'package:auto_size_text/auto_size_text.dart'; class RegisterScreen extends StatelessWidget { const RegisterScreen({super.key}); @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; final scaffoldBackgroundColor = Theme.of(context).scaffoldBackgroundColor; final textStyles = Theme.of(context).textTheme; @@ -16,127 +17,224 @@ class RegisterScreen extends StatelessWidget { return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: Scaffold( - body: GeometricalBackground( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: Column( + body: GeometricalBackground( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 80), + // Icon Banner + Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( height: 80 ), - // Icon Banner - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: (){ - if ( !context.canPop() ) return; - context.pop(); - }, - icon: const Icon( Icons.arrow_back_rounded, size: 40, color: Colors.white ) - ), - const Spacer(flex: 1), - Text('Create Account', style: textStyles.titleLarge?.copyWith(color: Colors.white )), - const Spacer(flex: 2), - ], + IconButton( + onPressed: () { + if (!context.canPop()) return; + context.pop(); + }, + icon: const Icon(Icons.arrow_back_rounded, + size: 40, color: Colors.white)), + const Spacer(flex: 1), //revisar + AutoSizeText( + 'Create Account', + style: textStyles.titleLarge?.copyWith(color: Colors.white), + maxLines: 1, + maxFontSize: 26, ), - - const SizedBox( height: 50 ), - - Container( - height: size.height - 260, // 80 los dos sizebox y 100 el ícono - width: double.infinity, - decoration: BoxDecoration( - color: scaffoldBackgroundColor, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(100)), - ), - child: const _RegisterForm(), - ) + const Spacer(flex: 2), ], ), - ) - ) - ), + const SizedBox(height: 30), + Container( + //quite el -260 para que se acople a los dispositivos + height: size.height - + 250, //- 260, // 80 los dos sizebox y 100 el ícono + width: double.infinity, + decoration: BoxDecoration( + color: scaffoldBackgroundColor, + borderRadius: + const BorderRadius.only(topLeft: Radius.circular(100)), + ), + child: const _RegisterForm(), + ), + ], + ), + ))), ); } } -class _RegisterForm extends StatelessWidget { +class _RegisterForm extends ConsumerWidget { const _RegisterForm(); + void showSnackbar(BuildContext context, String message) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + backgroundColor: Colors.red, + )); + } + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final registerForm = ref.watch(registerFormProvider); + + ref.listen(authProvider, (previous, next) { + if (next.errorMessage.isEmpty) return; + showSnackbar(context, next.errorMessage); + }); final textStyles = Theme.of(context).textTheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 50), child: Column( + mainAxisAlignment: MainAxisAlignment.start, // Añadido para alinear los widgets al inicio children: [ - const Spacer(), - //const SizedBox( height: 50 ), - Text('New Account', style: textStyles.titleMedium ), - //const SizedBox( height: 50 ), - const Spacer(), - const CustomTextFormField( - label: 'Full Name', - keyboardType: TextInputType.emailAddress, + const SizedBox(height: 90), + AutoSizeText( + 'New Account', + style: textStyles.titleMedium, + maxLines: 1, ), - const SizedBox( height: 30 ), - - const CustomTextFormField( + const SizedBox(height: 20), + CustomTextFormField( label: 'Username', keyboardType: TextInputType.emailAddress, + onChanged: ref.read(registerFormProvider.notifier).onUsernameChange, + errorMessage: registerForm.isFormPosted + ? registerForm.username.errorMessage + : null, ), - const SizedBox( height: 30 ), - - const CustomTextFormField( + const SizedBox(height: 30), + CustomTextFormField( label: 'Password', obscureText: true, + onChanged: + ref.read(registerFormProvider.notifier).onPasswordChanged, + errorMessage: registerForm.isFormPosted + ? registerForm.password.errorMessage + : null, ), - - const SizedBox( height: 30 ), - - const CustomTextFormField( + const SizedBox(height: 30), + CustomTextFormField( label: 'Repeat password', obscureText: true, + onChanged: + ref.read(registerFormProvider.notifier).onRepeatPasswordChanged, + errorMessage: registerForm.isFormPosted + ? registerForm.repeatPassword.errorMessage + : null, ), - - const SizedBox( height: 30 ), - + const SizedBox(height: 30), SizedBox( - width: double.infinity, - height: 60, - child: CustomFilledButton( - text: 'Create', - buttonColor: Colors.black, - onPressed: (){ - - }, - ) - ), - - const Spacer( flex: 2 ), - + width: double.infinity, + height: 60, + child: CustomFilledButton( + text: 'Create', + buttonColor: Colors.black, + onPressed: registerForm.isPosting + ? null + : ref.read(registerFormProvider.notifier).onFormSubmitted + )), + const SizedBox(height: 20), // Añadido para dar un pequeño espacio entre los elementos Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( - onPressed: (){ - if ( context.canPop()){ - return context.pop(); - } - context.go('/login'); - - }, - child: const Text('¿Already have an account?', - style: TextStyle(color: Color.fromRGBO(32, 159, 168, 1))) - ) + onPressed: () { + if (context.canPop()) { + return context.pop(); + } + context.go('/login'); + }, + child: const Text('¿Already have an account?', + style: TextStyle(color: Color.fromRGBO(32, 159, 168, 1)))) ], ), - - const Spacer( flex: 1), ], ), ); } -} \ No newline at end of file +} + + +/* + //modal de terminos y condiciones + _mySheet(BuildContext context) { + showModalBottomSheet( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0)), + ), + context: context, + builder: (context) { + return Scaffold( + backgroundColor: Color.fromARGB(26, 255, 255, 255), + body: SingleChildScrollView( + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height, + maxWidth: MediaQuery.of(context).size.width, + ), + child: Column( + children: [ + const SizedBox( + height: 10, + ), + SingleChildScrollView( + child: Container( + height: 700, + margin: const EdgeInsets.only(left: 9, right: 9), + child: Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 1, bottom: 1), + decoration: const BoxDecoration( + color: Color.fromARGB(155, 255, 255, 255), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.only( + bottom: 380, top: 12, left: 12, right: 12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AutoSizeText( + "By registering, you agree to our terms and conditions", + textAlign: TextAlign.justify, + maxFontSize: 15), + const SizedBox( + height: 11, + ), + Text( + termsandcondition, + textAlign: TextAlign.justify, + style: TextStyle( + color: Color.fromARGB(255, 151, 150, 150), + fontSize: 12, + ), + ), + const SizedBox( + height: 11, + ), + Container( + width: 180, + child: CustomFilledButton( + text: 'Accept', + buttonColor: Colors.black, + onPressed: () { + context.go('/'); + })), + ], + )), + ), + ), + ), + ], + ), + ), + ), + ); + }); + }*/ diff --git a/lib/features/drive/presentation/screens/drive_screen.dart b/lib/features/drive/presentation/screens/drive_screen.dart index 9c849e8..8c1d547 100644 --- a/lib/features/drive/presentation/screens/drive_screen.dart +++ b/lib/features/drive/presentation/screens/drive_screen.dart @@ -1,4 +1,6 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:login_mobile/features/shared/shared.dart'; class CapyDriveScreen extends StatelessWidget { @@ -6,35 +8,90 @@ class CapyDriveScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final scaffoldKey = GlobalKey(); return Scaffold( - drawer: SideMenu( scaffoldKey: scaffoldKey ), + drawer: SideMenu(scaffoldKey: scaffoldKey), appBar: AppBar( title: const Text('CapyFiles 𐃶'), actions: [ - IconButton( - onPressed: (){}, - icon: const Icon( Icons.search_rounded) - ) + IconButton(onPressed: () {}, icon: const Icon(Icons.search_rounded)) ], ), body: const _FilesView(), floatingActionButton: FloatingActionButton.extended( - label: const Text('New File 𐃶'), - icon: const Icon( Icons.add ), - onPressed: () {}, + label: const Text('New 𐃶'), + icon: const Icon(Icons.add), + onPressed: () { + _newModal(context); + }, ), ); } + + _newModal(BuildContext context) { + showModalBottomSheet( + backgroundColor: Color.fromARGB(119, 0, 0, 0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(60.0), topRight: Radius.circular(60.0), ), + ), + context: context, + builder: (context) { + return Container( + + + height: 150, + child: Row( + children: [ + iconTextButton("Folder", Color.fromARGB(255, 255, 255, 255), + Icon(Icons.folder), context, (){}), + iconTextButton("Upload", Color.fromARGB(255, 255, 255, 255), + Icon(Icons.file_upload), context, (){}) + ], + ), + ); + }); + } + + Widget iconTextButton( + String name, Color color, Icon icon, BuildContext context, Function function) { + return Container( + width: MediaQuery.of(context).size.width * 0.5, + child: Column( + children: [ + SizedBox( + height: 40, + ), + Container( + width: 80, + height: 50, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + child: icon, + ), + SizedBox( + height: 10, + ), + Text(name, + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontStyle: FontStyle.italic)), + ], + ), + ); + } + + } class _FilesView extends StatelessWidget { const _FilesView(); + @override Widget build(BuildContext context) { return const Center(child: Text('CapyFiles View')); + } -} \ No newline at end of file +} diff --git a/lib/features/drive/presentation/screens/storage_screen.dart b/lib/features/drive/presentation/screens/storage_screen.dart index 6554617..66cc364 100644 --- a/lib/features/drive/presentation/screens/storage_screen.dart +++ b/lib/features/drive/presentation/screens/storage_screen.dart @@ -17,11 +17,7 @@ class StorageScreen extends StatelessWidget { ], ), body: const _StorageView(), - floatingActionButton: FloatingActionButton.extended( - label: const Text('New File 𐃶'), - icon: const Icon(Icons.add), - onPressed: () {}, - ), + ); } } diff --git a/lib/features/shared/infrastructure/inputs/password.dart b/lib/features/shared/infrastructure/inputs/password.dart index 537dd2a..1240079 100644 --- a/lib/features/shared/infrastructure/inputs/password.dart +++ b/lib/features/shared/infrastructure/inputs/password.dart @@ -21,7 +21,7 @@ class Password extends FormzInput { if (displayError == PasswordError.empty) return 'Field is required'; if (displayError == PasswordError.length) return 'Minimum 6 characters'; if (displayError == PasswordError.format) { - return 'Must have a capital letter, letters and a number.'; + return 'Must have a capital letter, \nletters and a number.'; } return null; diff --git a/lib/features/shared/infrastructure/inputs/repeat_password.dart b/lib/features/shared/infrastructure/inputs/repeat_password.dart new file mode 100644 index 0000000..8c30f8c --- /dev/null +++ b/lib/features/shared/infrastructure/inputs/repeat_password.dart @@ -0,0 +1,40 @@ +import 'package:formz/formz.dart'; + +// Define input validation errors +enum RepeatPasswordError { empty, compare} + +// Extend FormzInput and provide the input type and error type. +class RepeatPassword extends FormzInput { + static final RegExp passwordRegExp = RegExp( + r'(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$', + ); + + + // Call super.pure to represent an unmodified form input. + const RepeatPassword.pure() : super.pure(''); + + // Call super.dirty to represent a modified form input. + const RepeatPassword.dirty(String value) : super.dirty(value); + + //const RepeatPassword.dirty(String value) : super.dirty(value); + + String? get errorMessage { + if (isValid || isPure) return null; + + if (displayError == RepeatPasswordError.empty) return 'Field is required'; + if (displayError == RepeatPasswordError.compare) return 'password does not match'; + + + return null; + } + + // Override validator to handle validating a given input value. + @override + RepeatPasswordError? validator(String value) { + if (value.isEmpty || value.trim().isEmpty) return RepeatPasswordError.empty; + if (value == "no") return RepeatPasswordError.compare; + + + return null; + } +} diff --git a/lib/features/shared/infrastructure/inputs/services/key_value_storage_impl.dart b/lib/features/shared/infrastructure/inputs/services/key_value_storage_impl.dart index 79256cd..b45a5ff 100644 --- a/lib/features/shared/infrastructure/inputs/services/key_value_storage_impl.dart +++ b/lib/features/shared/infrastructure/inputs/services/key_value_storage_impl.dart @@ -1,24 +1,26 @@ //Services para poder cambiar si más adelante se quiere SQLite //abs si yo quiero cambiar la implementación tiene que cumplir las siguientes relgas -import 'package:shared_preferences/shared_preferences.dart'; import 'key_value_storage_services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class KeyValueStorageServiceImpl extends KeyValueStorageService { - Future getSharePrefs() async { - return await SharedPreferences.getInstance(); - } + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); @override Future getValue(String key) async { - final prefs = await getSharePrefs(); + final String? stringValue = await _secureStorage.read(key: key); + + if (stringValue == null) { + return null; + } + switch (T) { case int: - return prefs.getInt(key) as T?; + return int.tryParse(stringValue) as T?; case String: - return prefs.getString(key) as T?; + return stringValue as T?; case bool: - return prefs.getBool(key) as T?; - + return (stringValue == 'true') as T?; default: throw UnimplementedError('GET Type not supported ${T.runtimeType}'); } @@ -26,26 +28,27 @@ class KeyValueStorageServiceImpl extends KeyValueStorageService { @override Future removeKey(String key) async { - final prefs = await getSharePrefs(); - return await prefs.remove(key); + await _secureStorage.delete(key: key); + //Note by SG: flutter_secure_storage does not return a bool, assume true upon successful delete + return true; } @override Future setKeyValue(String key, T value) async { - final prefs = await getSharePrefs(); + String stringValue; switch (T) { case int: - prefs.setInt(key, value as int); + stringValue = value.toString(); break; case String: - prefs.setString(key, value as String); + stringValue = value as String; break; case bool: - prefs.setBool(key, value as bool); + stringValue = (value as bool).toString(); break; - default: throw UnimplementedError('SET Type not supported ${T.runtimeType}'); } + await _secureStorage.write(key: key, value: stringValue); } } diff --git a/lib/features/shared/widgets/bottom_sheet.dart b/lib/features/shared/widgets/bottom_sheet.dart index 9dcf864..591ec06 100644 --- a/lib/features/shared/widgets/bottom_sheet.dart +++ b/lib/features/shared/widgets/bottom_sheet.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:login_mobile/config/config.dart'; import 'package:login_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:login_mobile/features/auth/presentation/providers/login_form_provider.dart'; import 'package:login_mobile/features/shared/shared.dart'; @@ -38,7 +37,7 @@ class CustomLogin extends ConsumerWidget { }); final textStyles = Theme.of(context).textTheme; - + String username = authState.user?.username ?? ''; return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: Scaffold( @@ -53,16 +52,8 @@ class CustomLogin extends ConsumerWidget { children: [ const SizedBox(height: 5), Text('Confirm Account', style: textStyles.titleLarge), - const SizedBox(height: 20), - CustomTextFormField( - label: 'Username', - keyboardType: TextInputType.emailAddress, - onChanged: - ref.read(loginFormProvider.notifier).onUsernameChange, - errorMessage: loginForm.isFormPosted - ? loginForm.username.errorMessage - : null, - ), + const SizedBox(height: 5), + Text(username, style: textStyles.titleSmall), const SizedBox(height: 20), CustomTextFormField( label: 'Insert you password', @@ -79,13 +70,25 @@ class CustomLogin extends ConsumerWidget { width: double.infinity, height: 55, child: CustomFilledButton( - text: 'Sign in', - buttonColor: Colors.black, - onPressed: loginForm.isPosting - ? null - : ref - .read(loginFormProvider.notifier) - .onFormSubmittedBiometric)), + text: 'Confirm', + buttonColor: Colors.black, + onPressed: loginForm.isPosting + ? null + : () async { + final authState = ref.read(authProvider); + if (authState.hasBiometric == true) { + WidgetsBinding.instance + .addPostFrameCallback((_) { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }); + } + await ref + .read(loginFormProvider.notifier) + .onFormSubmittedBiometric(username); + }, + )), const Spacer(flex: 2), ], ), diff --git a/lib/features/shared/widgets/custom_text_form_field.dart b/lib/features/shared/widgets/custom_text_form_field.dart index 3ecba1d..1157dcd 100644 --- a/lib/features/shared/widgets/custom_text_form_field.dart +++ b/lib/features/shared/widgets/custom_text_form_field.dart @@ -10,6 +10,7 @@ class CustomTextFormField extends StatelessWidget { final TextInputType? keyboardType; final Function(String)? onChanged; final String? Function(String?)? validator; + final bool readOnly; const CustomTextFormField({ super.key, @@ -19,7 +20,8 @@ class CustomTextFormField extends StatelessWidget { this.obscureText = false, this.keyboardType = TextInputType.text, this.onChanged, - this.validator, + this.validator, + this.readOnly = false, }); @override @@ -48,6 +50,8 @@ class CustomTextFormField extends StatelessWidget { ] ), child: TextFormField( + enabled: !readOnly, + readOnly: readOnly, onChanged: onChanged, validator: validator, obscureText: obscureText, diff --git a/lib/features/shared/widgets/folder.dart b/lib/features/shared/widgets/folder.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/shared/widgets/side_menu.dart b/lib/features/shared/widgets/side_menu.dart index 4880433..53fdd06 100644 --- a/lib/features/shared/widgets/side_menu.dart +++ b/lib/features/shared/widgets/side_menu.dart @@ -63,7 +63,9 @@ class SideMenuState extends ConsumerState { ), Padding( padding: const EdgeInsets.fromLTRB(20, 0, 16, 10), - child: Text('Tony Stark', style: textStyles.titleSmall), + child: Text(authState.user?.username ?? 'Fallback Value', + style: textStyles.titleSmall), + ), const NavigationDrawerDestination( icon: Icon(Icons.folder), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..37af1fe 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 60b7e83..5cdb5d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" boolean_selector: dependency: transitive description: @@ -126,6 +134,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -192,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 778c8a3..754e84a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,12 +28,14 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + auto_size_text: ^3.0.0 cupertino_icons: ^1.0.2 dio: ^5.3.2 flutter: sdk: flutter flutter_dotenv: ^5.1.0 flutter_riverpod: ^2.3.7 + flutter_secure_storage: ^9.0.0 fluttertoast: ^8.2.2 formz: ^0.6.0 go_router: ^10.1.0 diff --git a/test/widget_test.dart b/test/widget_test.dart index 106dc72..b22ef68 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,11 +5,8 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:login_mobile/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { }); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7407ddd..011734d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ef187dc..11485fc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows local_auth_windows ) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 490813d..c819cb0 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; }