diff --git a/.taskmaster/tasks/task_001.txt b/.taskmaster/tasks/task_001.txt new file mode 100644 index 0000000..84bc9e6 --- /dev/null +++ b/.taskmaster/tasks/task_001.txt @@ -0,0 +1,11 @@ +# Task ID: 1 +# Title: Migrate to Riverpod 3.0 +# Status: done +# Dependencies: None +# Priority: high +# Description: Update the application to use the latest stable version of Riverpod 3.0. This includes updating the `hooks_riverpod` dependency, refactoring providers and widgets, and ensuring the state management is robust. +# Details: + + +# Test Strategy: + diff --git a/.taskmaster/tasks/task_002.txt b/.taskmaster/tasks/task_002.txt new file mode 100644 index 0000000..896b860 --- /dev/null +++ b/.taskmaster/tasks/task_002.txt @@ -0,0 +1,11 @@ +# Task ID: 2 +# Title: Modernize the User Interface +# Status: done +# Dependencies: 1 +# Priority: high +# Description: Redesign the login, registration, and home pages to be more modern and visually appealing. Adopt a clean design language like Material You, create a consistent theme, and add animations to enhance the user experience. +# Details: + + +# Test Strategy: + diff --git a/.taskmaster/tasks/task_003.txt b/.taskmaster/tasks/task_003.txt new file mode 100644 index 0000000..3ad4f75 --- /dev/null +++ b/.taskmaster/tasks/task_003.txt @@ -0,0 +1,11 @@ +# Task ID: 3 +# Title: Refactor and Clean Up Codebase +# Status: pending +# Dependencies: 1 +# Priority: medium +# Description: Improve the quality, readability, and maintainability of the codebase. This includes updating all dependencies to their latest stable versions, improving error handling, enhancing input validation, and ensuring the code is well-organized. +# Details: + + +# Test Strategy: + diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 9e5dc5e..69fbca4 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -24,9 +24,67 @@ "title": "Refactor and Clean Up Codebase", "description": "Improve the quality, readability, and maintainability of the codebase. This includes updating all dependencies to their latest stable versions, improving error handling, enhancing input validation, and ensuring the code is well-organized.", "status": "pending", - "priority": "medium", "dependencies": [ 1 + ], + "priority": "medium", + "details": "", + "testStrategy": "", + "subtasks": [ + { + "id": 1, + "title": "Refactor Domain Layer", + "description": "Fix typos, enhance email validation with industry-standard regex, strengthen password security (8+ chars)", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 2, + "title": "Refactor Application Layer", + "description": "Streamline AuthStateController, improve error handling, cleaner code structure", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 3, + "title": "Refactor Services Layer", + "description": "Enhance Firebase Auth Facade with centralized error mapping and better null safety", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 4, + "title": "Refactor UI Components", + "description": "Refactor Login/Registration pages with modular structure, extract helper methods, enhance validation", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 5, + "title": "App Level Improvements", + "description": "Simplify routing, add global page transitions, performance improvements", + "status": "done", + "dependencies": [], + "details": "", + "testStrategy": "" + }, + { + "id": 6, + "title": "Update Dependencies", + "description": "Update all dependencies to their latest stable versions", + "status": "pending", + "dependencies": [], + "details": "", + "testStrategy": "" + } ] }, { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f5e91b6..78d4fa7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,11 +4,11 @@ PODS: - FirebaseAuth (~> 11.15.0) - Firebase/CoreOnly (11.15.0): - FirebaseCore (~> 11.15.0) - - firebase_auth (5.6.2): + - firebase_auth (5.7.0): - Firebase/Auth (= 11.15.0) - firebase_core - Flutter - - firebase_core (3.15.1): + - firebase_core (3.15.2): - Firebase/CoreOnly (= 11.15.0) - Flutter - FirebaseAppCheckInterop (11.15.0) @@ -83,8 +83,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e - firebase_auth: b8ed959bf77eca5cf0312b5e29708fe8311a0ddf - firebase_core: cf4d42a8ac915e51c0c2dc103442f3036d941a2d + firebase_auth: 5342db41af2ba5ed32a6177d9e326eecbebda912 + firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd diff --git a/lib/app.dart b/lib/app.dart index 1525c92..342e62e 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,7 +10,6 @@ class FirebaseAuthenticationDDD extends StatelessWidget { @override Widget build(BuildContext context) { - // Set system UI overlay style SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, @@ -26,49 +25,11 @@ class FirebaseAuthenticationDDD extends StatelessWidget { title: "Firebase Auth DDD", debugShowCheckedModeBanner: kDebugMode, - // Apply our modern Material You theme system theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.system, - // Improved route transitions - onGenerateRoute: (settings) { - switch (settings.name) { - case "/": - return _createRoute(LoginPage()); - default: - return _createRoute(LoginPage()); - } - }, - - home: LoginPage(), - ); - } - - // Custom page route with smooth transitions - PageRoute _createRoute(Widget page) { - return PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => page, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(0.0, 0.1); - const end = Offset.zero; - const curve = Curves.easeOutCubic; - - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); - - var fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: animation, curve: curve), - ); - - return SlideTransition( - position: animation.drive(tween), - child: FadeTransition( - opacity: fadeAnimation, - child: child, - ), - ); - }, - transitionDuration: const Duration(milliseconds: 400), + home: const LoginPage(), ); } } diff --git a/lib/application/authentication/auth_state_controller.dart b/lib/application/authentication/auth_state_controller.dart index c75151c..7dc2dc9 100644 --- a/lib/application/authentication/auth_state_controller.dart +++ b/lib/application/authentication/auth_state_controller.dart @@ -8,6 +8,10 @@ import "../../services/authentication/firebase_auth_facade.dart"; import "auth_events.dart"; import "auth_states.dart"; +final authStateControllerProvider = NotifierProvider( + AuthStateController.new, +); + class AuthStateController extends Notifier { @override AuthStates build() { @@ -19,87 +23,84 @@ class AuthStateController extends Notifier { Future mapEventsToStates(AuthEvents events) async { await events.map( emailChanged: (value) async { - state = state.copyWith( - emailAddress: EmailAddress( - email: value.email, - ), - authFailureOrSuccess: none()); + _updateEmail(value.email!); }, passwordChanged: (value) async { - state = state.copyWith( - password: Password( - password: value.password, - ), - authFailureOrSuccess: none(), - ); + _updatePassword(value.password!); }, signUpWithEmailAndPasswordPressed: (value) async { - await _performAuthAction( - _authFacade.registerWithEmailAndPassword, - ); + await signUpWithEmailAndPassword(); }, signInWithEmailAndPasswordPressed: (value) async { - await _performAuthAction( - _authFacade.signInWithEmailAndPassword, - ); + await signInWithEmailAndPassword(); }, ); } void emailChanged(String email) { + _updateEmail(email); + } + + void passwordChanged(String password) { + _updatePassword(password); + } + + void _updateEmail(String email) { state = state.copyWith( emailAddress: EmailAddress(email: email), authFailureOrSuccess: none(), + showError: false, ); } - void passwordChanged(String password) { + void _updatePassword(String password) { state = state.copyWith( password: Password(password: password), authFailureOrSuccess: none(), + showError: false, ); } Future signUpWithEmailAndPassword() async { - await _performAuthAction( - _authFacade.registerWithEmailAndPassword, - ); + await _performAuthAction(_authFacade.registerWithEmailAndPassword); } Future signInWithEmailAndPassword() async { - await _performAuthAction( - _authFacade.signInWithEmailAndPassword, - ); + await _performAuthAction(_authFacade.signInWithEmailAndPassword); } Future _performAuthAction( - Future> Function({required EmailAddress emailAddress, required Password password}) - forwardCall, + Future> Function({ + required EmailAddress emailAddress, + required Password password, + }) forwardCall, ) async { final isEmailValid = state.emailAddress.isValid(); final isPasswordValid = state.password.isValid(); - Either? failureOrSuccess; - if (isEmailValid && isPasswordValid) { + if (!isEmailValid || !isPasswordValid) { state = state.copyWith( - isSubmitting: true, + showError: true, authFailureOrSuccess: none(), ); - - failureOrSuccess = await forwardCall( - emailAddress: state.emailAddress, - password: state.password, - ); + return; } + state = state.copyWith( + isSubmitting: true, + authFailureOrSuccess: none(), + showError: false, + ); + + final failureOrSuccess = await forwardCall( + emailAddress: state.emailAddress, + password: state.password, + ); + state = state.copyWith( isSubmitting: false, - showError: true, + showError: failureOrSuccess.isLeft(), authFailureOrSuccess: optionOf(failureOrSuccess), ); } } - -final authStateControllerProvider = NotifierProvider(() { - return AuthStateController(); -}); diff --git a/lib/application/authentication/auth_states.freezed.dart b/lib/application/authentication/auth_states.freezed.dart index 3ab7bc3..2a5a006 100644 --- a/lib/application/authentication/auth_states.freezed.dart +++ b/lib/application/authentication/auth_states.freezed.dart @@ -15,36 +15,38 @@ T _$identity(T value) => value; /// @nodoc mixin _$AuthStates { EmailAddress get emailAddress; - Password get password; - bool get isSubmitting; - bool get showError; - Option> get authFailureOrSuccess; /// Create a copy of AuthStates /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - $AuthStatesCopyWith get copyWith => _$AuthStatesCopyWithImpl(this as AuthStates, _$identity); + $AuthStatesCopyWith get copyWith => + _$AuthStatesCopyWithImpl(this as AuthStates, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is AuthStates && - (identical(other.emailAddress, emailAddress) || other.emailAddress == emailAddress) && - (identical(other.password, password) || other.password == password) && - (identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting) && - (identical(other.showError, showError) || other.showError == showError) && + (identical(other.emailAddress, emailAddress) || + other.emailAddress == emailAddress) && + (identical(other.password, password) || + other.password == password) && + (identical(other.isSubmitting, isSubmitting) || + other.isSubmitting == isSubmitting) && + (identical(other.showError, showError) || + other.showError == showError) && (identical(other.authFailureOrSuccess, authFailureOrSuccess) || other.authFailureOrSuccess == authFailureOrSuccess)); } @override - int get hashCode => Object.hash(runtimeType, emailAddress, password, isSubmitting, showError, authFailureOrSuccess); + int get hashCode => Object.hash(runtimeType, emailAddress, password, + isSubmitting, showError, authFailureOrSuccess); @override String toString() { @@ -54,8 +56,9 @@ mixin _$AuthStates { /// @nodoc abstract mixin class $AuthStatesCopyWith<$Res> { - factory $AuthStatesCopyWith(AuthStates value, $Res Function(AuthStates) _then) = _$AuthStatesCopyWithImpl; - + factory $AuthStatesCopyWith( + AuthStates value, $Res Function(AuthStates) _then) = + _$AuthStatesCopyWithImpl; @useResult $Res call( {EmailAddress emailAddress, @@ -199,7 +202,11 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult maybeWhen( - TResult Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, + TResult Function( + EmailAddress emailAddress, + Password password, + bool isSubmitting, + bool showError, Option> authFailureOrSuccess)? $default, { required TResult orElse(), @@ -207,8 +214,8 @@ extension AuthStatesPatterns on AuthStates { final _that = this; switch (_that) { case _AuthStates() when $default != null: - return $default( - _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); + return $default(_that.emailAddress, _that.password, _that.isSubmitting, + _that.showError, _that.authFailureOrSuccess); case _: return orElse(); } @@ -229,15 +236,19 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult when( - TResult Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, + TResult Function( + EmailAddress emailAddress, + Password password, + bool isSubmitting, + bool showError, Option> authFailureOrSuccess) $default, ) { final _that = this; switch (_that) { case _AuthStates(): - return $default( - _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); + return $default(_that.emailAddress, _that.password, _that.isSubmitting, + _that.showError, _that.authFailureOrSuccess); } } @@ -255,15 +266,19 @@ extension AuthStatesPatterns on AuthStates { @optionalTypeArgs TResult? whenOrNull( - TResult? Function(EmailAddress emailAddress, Password password, bool isSubmitting, bool showError, + TResult? Function( + EmailAddress emailAddress, + Password password, + bool isSubmitting, + bool showError, Option> authFailureOrSuccess)? $default, ) { final _that = this; switch (_that) { case _AuthStates() when $default != null: - return $default( - _that.emailAddress, _that.password, _that.isSubmitting, _that.showError, _that.authFailureOrSuccess); + return $default(_that.emailAddress, _that.password, _that.isSubmitting, + _that.showError, _that.authFailureOrSuccess); case _: return null; } @@ -296,23 +311,29 @@ class _AuthStates implements AuthStates { @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') - _$AuthStatesCopyWith<_AuthStates> get copyWith => __$AuthStatesCopyWithImpl<_AuthStates>(this, _$identity); + _$AuthStatesCopyWith<_AuthStates> get copyWith => + __$AuthStatesCopyWithImpl<_AuthStates>(this, _$identity); @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _AuthStates && - (identical(other.emailAddress, emailAddress) || other.emailAddress == emailAddress) && - (identical(other.password, password) || other.password == password) && - (identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting) && - (identical(other.showError, showError) || other.showError == showError) && + (identical(other.emailAddress, emailAddress) || + other.emailAddress == emailAddress) && + (identical(other.password, password) || + other.password == password) && + (identical(other.isSubmitting, isSubmitting) || + other.isSubmitting == isSubmitting) && + (identical(other.showError, showError) || + other.showError == showError) && (identical(other.authFailureOrSuccess, authFailureOrSuccess) || other.authFailureOrSuccess == authFailureOrSuccess)); } @override - int get hashCode => Object.hash(runtimeType, emailAddress, password, isSubmitting, showError, authFailureOrSuccess); + int get hashCode => Object.hash(runtimeType, emailAddress, password, + isSubmitting, showError, authFailureOrSuccess); @override String toString() { @@ -321,9 +342,11 @@ class _AuthStates implements AuthStates { } /// @nodoc -abstract mixin class _$AuthStatesCopyWith<$Res> implements $AuthStatesCopyWith<$Res> { - factory _$AuthStatesCopyWith(_AuthStates value, $Res Function(_AuthStates) _then) = __$AuthStatesCopyWithImpl; - +abstract mixin class _$AuthStatesCopyWith<$Res> + implements $AuthStatesCopyWith<$Res> { + factory _$AuthStatesCopyWith( + _AuthStates value, $Res Function(_AuthStates) _then) = + __$AuthStatesCopyWithImpl; @override @useResult $Res call( diff --git a/lib/domain/authentication/auth_failures.dart b/lib/domain/authentication/auth_failures.dart index 580ef23..b6acce7 100644 --- a/lib/domain/authentication/auth_failures.dart +++ b/lib/domain/authentication/auth_failures.dart @@ -8,5 +8,5 @@ class AuthFailures with _$AuthFailures { const factory AuthFailures.emailAlreadyInUse() = EmailAlreadyInUse; - const factory AuthFailures.invalidEmailAndPasswordCombination() = InavalidEmailAndPasswordCombination; + const factory AuthFailures.invalidEmailAndPasswordCombination() = InvalidEmailAndPasswordCombination; } diff --git a/lib/domain/authentication/auth_failures.freezed.dart b/lib/domain/authentication/auth_failures.freezed.dart index 5bb9558..193283e 100644 --- a/lib/domain/authentication/auth_failures.freezed.dart +++ b/lib/domain/authentication/auth_failures.freezed.dart @@ -51,7 +51,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult maybeMap({ TResult Function(ServerError value)? serverError, TResult Function(EmailAlreadyInUse value)? emailAlreadyInUse, - TResult Function(InavalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, + TResult Function(InvalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, required TResult orElse(), }) { final _that = this; @@ -60,7 +60,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(_that); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(_that); - case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: + case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(_that); case _: return orElse(); @@ -84,7 +84,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult map({ required TResult Function(ServerError value) serverError, required TResult Function(EmailAlreadyInUse value) emailAlreadyInUse, - required TResult Function(InavalidEmailAndPasswordCombination value) invalidEmailAndPasswordCombination, + required TResult Function(InvalidEmailAndPasswordCombination value) invalidEmailAndPasswordCombination, }) { final _that = this; switch (_that) { @@ -92,7 +92,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(_that); case EmailAlreadyInUse(): return emailAlreadyInUse(_that); - case InavalidEmailAndPasswordCombination(): + case InvalidEmailAndPasswordCombination(): return invalidEmailAndPasswordCombination(_that); case _: throw StateError('Unexpected subclass'); @@ -115,7 +115,7 @@ extension AuthFailuresPatterns on AuthFailures { TResult? mapOrNull({ TResult? Function(ServerError value)? serverError, TResult? Function(EmailAlreadyInUse value)? emailAlreadyInUse, - TResult? Function(InavalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, + TResult? Function(InvalidEmailAndPasswordCombination value)? invalidEmailAndPasswordCombination, }) { final _that = this; switch (_that) { @@ -123,7 +123,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(_that); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(_that); - case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: + case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(_that); case _: return null; @@ -155,7 +155,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(); - case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: + case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(); case _: return orElse(); @@ -187,7 +187,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(); case EmailAlreadyInUse(): return emailAlreadyInUse(); - case InavalidEmailAndPasswordCombination(): + case InvalidEmailAndPasswordCombination(): return invalidEmailAndPasswordCombination(); case _: throw StateError('Unexpected subclass'); @@ -218,7 +218,7 @@ extension AuthFailuresPatterns on AuthFailures { return serverError(); case EmailAlreadyInUse() when emailAlreadyInUse != null: return emailAlreadyInUse(); - case InavalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: + case InvalidEmailAndPasswordCombination() when invalidEmailAndPasswordCombination != null: return invalidEmailAndPasswordCombination(); case _: return null; @@ -266,12 +266,12 @@ class EmailAlreadyInUse implements AuthFailures { /// @nodoc -class InavalidEmailAndPasswordCombination implements AuthFailures { - const InavalidEmailAndPasswordCombination(); +class InvalidEmailAndPasswordCombination implements AuthFailures { + const InvalidEmailAndPasswordCombination(); @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType && other is InavalidEmailAndPasswordCombination); + return identical(this, other) || (other.runtimeType == runtimeType && other is InvalidEmailAndPasswordCombination); } @override diff --git a/lib/domain/authentication/auth_value_validators.dart b/lib/domain/authentication/auth_value_validators.dart index 0d77fc6..b67e11f 100644 --- a/lib/domain/authentication/auth_value_validators.dart +++ b/lib/domain/authentication/auth_value_validators.dart @@ -4,40 +4,43 @@ import "package:fpdart/fpdart.dart"; Either, String> validateEmailAddress({ required String? email, }) { - final emailRegex = RegExp(r'^[a-zA-Z0-9.a-zA-Z0-9.!#$%&"*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+'); + // Improved email regex pattern for better validation + const emailRegex = + r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"; + final emailPattern = RegExp(emailRegex); - if (emailRegex.hasMatch(email!)) { - return right(email); + if (email == null || email.trim().isEmpty) { + return left(AuthValueFailures.invalidEmail(failedValue: email ?? "")); + } + + final trimmedEmail = email.trim().toLowerCase(); + if (emailPattern.hasMatch(trimmedEmail)) { + return right(trimmedEmail); } else { - return left( - AuthValueFailures.invalidEmail(failedValue: email), - ); + return left(AuthValueFailures.invalidEmail(failedValue: email)); } } Either, String> validatePassword({ required String? password, }) { - final hasMinLength = password!.length > 6; - final hasUppercase = password.contains(RegExp("[A-Z]")); - final hasDigits = password.contains(RegExp("[0-9]")); - final hasSpecialCharacters = password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); + if (password == null || password.isEmpty) { + return left(AuthValueFailures.shortPassword(failedValue: password ?? "")); + } + + final hasMinLength = password.length >= 8; // Changed from > 6 to >= 8 for better security + final hasUppercase = RegExp(r"[A-Z]").hasMatch(password); + final hasDigits = RegExp(r"[0-9]").hasMatch(password); + final hasSpecialCharacters = RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password); + if (!hasMinLength) { - return left( - AuthValueFailures.shortPassword(failedValue: password), - ); + return left(AuthValueFailures.shortPassword(failedValue: password)); } else if (!hasUppercase) { - return left( - AuthValueFailures.noUpperCase(failedValue: password), - ); + return left(AuthValueFailures.noUpperCase(failedValue: password)); } else if (!hasDigits) { - return left( - AuthValueFailures.noNumber(failedValue: password), - ); + return left(AuthValueFailures.noNumber(failedValue: password)); } else if (!hasSpecialCharacters) { - return left( - AuthValueFailures.noSpecialSymbol(failedValue: password), - ); + return left(AuthValueFailures.noSpecialSymbol(failedValue: password)); } else { return right(password); } diff --git a/lib/domain/authentication/i_auth_facade.dart b/lib/domain/authentication/i_auth_facade.dart index 0c1a4a9..e9b7596 100644 --- a/lib/domain/authentication/i_auth_facade.dart +++ b/lib/domain/authentication/i_auth_facade.dart @@ -4,10 +4,10 @@ import "package:fpdart/fpdart.dart"; abstract class IAuthFacade { Future> registerWithEmailAndPassword( - {required EmailAddress? emailAddress, required Password? password}); + {required EmailAddress emailAddress, required Password password}); Future> signInWithEmailAndPassword( - {required EmailAddress? emailAddress, required Password? password}); + {required EmailAddress emailAddress, required Password password}); Future> getSignedInUser(); diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index 91b7b44..511fbba 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -41,7 +41,7 @@ class _HomePageState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, body: CustomScrollView( slivers: [ // Modern App Bar with Material You design @@ -70,8 +70,8 @@ class _HomePageState extends State with TickerProviderStateMixin { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), - Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.3), + Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3), + Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.3), ], ), ), @@ -90,7 +90,7 @@ class _HomePageState extends State with TickerProviderStateMixin { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3), spreadRadius: 0, blurRadius: 20, offset: const Offset(0, 10), @@ -259,7 +259,7 @@ class _HomePageState extends State with TickerProviderStateMixin { SlideInWidget( delay: 700, child: Card( - color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3), child: Padding( padding: const EdgeInsets.all(24), child: Column( @@ -338,10 +338,10 @@ class _HomePageState extends State with TickerProviderStateMixin { borderRadius: BorderRadius.circular(20), child: Container( decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: Border.all( - color: color.withOpacity(0.3), + color: color.withValues(alpha: 0.3), width: 1, ), ), diff --git a/lib/screens/login_page.dart b/lib/screens/login_page.dart index cbba9ac..812430a 100644 --- a/lib/screens/login_page.dart +++ b/lib/screens/login_page.dart @@ -4,6 +4,7 @@ import "package:firebase_auth_flutter_ddd/screens/home_page.dart"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "../application/authentication/auth_state_controller.dart"; @@ -12,70 +13,24 @@ import "registration_page.dart"; import "utils/custom_snackbar.dart"; class LoginPage extends HookConsumerWidget { - LoginPage({Key? key}) : super(key: key); - - final formKey = GlobalKey(); - final emailController = TextEditingController(); - final passwordController = TextEditingController(); + const LoginPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + // Use hooks to create persistent controllers that survive rebuilds + final formKey = useMemoized(() => GlobalKey()); + final emailController = useTextEditingController(); + final passwordController = useTextEditingController(); + final formStates = ref.watch(authStateControllerProvider); - final formNotifier = ref.watch(authStateControllerProvider.notifier); + final formNotifier = ref.read(authStateControllerProvider.notifier); ref.listen(authStateControllerProvider, (previous, next) { - next.authFailureOrSuccess.fold( - () {}, - (either) => either.fold( - (failure) { - HapticFeedback.lightImpact(); - buildCustomSnackBar( - context: context, - flashBackground: Theme.of(context).colorScheme.error, - icon: Icons.error_outline_rounded, - content: Text( - failure.when( - serverError: () => "Server error occurred", - emailAlreadyInUse: () => "User already exists", - invalidEmailAndPasswordCombination: () => "Invalid email or password"), - style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), - )); - }, - (success) { - HapticFeedback.lightImpact(); // Changed from successImpact to lightImpact - buildCustomSnackBar( - context: context, - flashBackground: Theme.of(context).colorScheme.primary, - icon: CupertinoIcons.check_mark_circled_solid, - content: Text( - "Welcome back! Login successful", - style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), - )); - Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => const HomePage(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOutCubic; - - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); - - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - transitionDuration: const Duration(milliseconds: 300), - )); - }, - ), - ); + _handleAuthStateChanges(context, next); }); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, // Changed from background to surface + backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), @@ -87,202 +42,233 @@ class LoginPage extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 40), - - // Hero Section with Animation - SlideInWidget( - delay: 100, - child: Column( - children: [ - Container( - height: 120, - width: 120, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - Icons.login_rounded, - size: 64, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ), - const SizedBox(height: 32), - Text( - "Welcome back", - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, // Changed from onBackground - ), - ), - const SizedBox(height: 8), - Text( - "Sign in to your account to continue", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - + _buildHeroSection(context), const SizedBox(height: 48), - - // Email Field - SlideInWidget( - delay: 200, - child: AnimatedFormField( - label: "Email", - hint: "Enter your email address", - keyboardType: TextInputType.emailAddress, - prefixIcon: Icons.email_outlined, - controller: emailController, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter your email"; - } - if (!value.contains("@")) { - // Fixed quote usage - return "Please enter a valid email"; - } - return null; - }, - onChanged: (email) { - formNotifier.emailChanged(email); - }, - ), - ), - + _buildEmailField(emailController, formNotifier), const SizedBox(height: 20), + _buildPasswordField(passwordController, formNotifier), + const SizedBox(height: 32), + _buildSignInButton(context, formKey, formStates, formNotifier), + const SizedBox(height: 24), + _buildSignUpPrompt(context), + ], + ), + ), + ), + ), + ), + ); + } - // Password Field - Simplified without showPassword dependency - SlideInWidget( - delay: 300, - child: AnimatedFormField( - label: "Password", - hint: "Enter your password", - obscureText: true, - // Simplified to always obscure - prefixIcon: Icons.lock_outline, - controller: passwordController, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter your password"; - } - if (value.length < 6) { - return "Password must be at least 6 characters"; - } - return null; - }, - onChanged: (password) { - formNotifier.passwordChanged(password); - }, - ), - ), + void _handleAuthStateChanges(BuildContext context, AuthStates state) { + state.authFailureOrSuccess.fold( + () {}, + (either) => either.fold( + (failure) => _showErrorSnackBar(context, failure), + (success) => _handleSuccessfulAuth(context), + ), + ); + } - const SizedBox(height: 32), + void _showErrorSnackBar(BuildContext context, AuthFailures failure) { + HapticFeedback.lightImpact(); + final errorMessage = failure.when( + serverError: () => "Server error occurred", + emailAlreadyInUse: () => "User already exists", + invalidEmailAndPasswordCombination: () => "Invalid email or password", + ); - // Login Button - SlideInWidget( - delay: 400, - child: AnimatedButton( - text: "Sign In", - icon: Icons.arrow_forward_rounded, - isLoading: formStates.isSubmitting, - onPressed: formStates.isSubmitting - ? null - : () { - if (formKey.currentState!.validate()) { - HapticFeedback.lightImpact(); - formNotifier.signInWithEmailAndPassword(); // Fixed method name - } else { - HapticFeedback.lightImpact(); - } - }, - ), - ), + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.error, + icon: Icons.error_outline_rounded, + content: Text( + errorMessage, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ); + } - const SizedBox(height: 24), + void _handleSuccessfulAuth(BuildContext context) { + HapticFeedback.lightImpact(); + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.primary, + icon: CupertinoIcons.check_mark_circled_solid, + content: Text( + "Welcome back! Login successful", + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ); - // Forgot Password - SlideInWidget( - delay: 500, - child: Center( - child: TextButton( - onPressed: () { - // TODO: Navigate to forgot password page - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Forgot password feature coming soon!"), - ), - ); - }, - child: const Text("Forgot your password?"), - ), - ), - ), + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => const HomePage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + return SlideTransition(position: animation.drive(tween), child: child); + }, + transitionDuration: const Duration(milliseconds: 300), + ), + ); + } - const SizedBox(height: 32), + Widget _buildHeroSection(BuildContext context) { + return SlideInWidget( + delay: 100, + child: Column( + children: [ + Container( + height: 120, + width: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Icons.login_rounded, + size: 64, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 32), + Text( + "Welcome back", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + "Sign in to your account to continue", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } - // Register Section - SlideInWidget( - delay: 600, - child: Card( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Text( - "Don't have an account?", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: () { - Navigator.push( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => RegistrationPage(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOutCubic; + Widget _buildEmailField(TextEditingController controller, AuthStateController notifier) { + return SlideInWidget( + delay: 200, + child: AnimatedFormField( + label: "Email", + hint: "Enter your email address", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + controller: controller, + validator: _validateEmail, + onChanged: notifier.emailChanged, + ), + ); + } - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + Widget _buildPasswordField(TextEditingController controller, AuthStateController notifier) { + return SlideInWidget( + delay: 300, + child: AnimatedFormField( + label: "Password", + hint: "Enter your password", + obscureText: true, + prefixIcon: Icons.lock_outline, + controller: controller, + validator: _validatePassword, + onChanged: notifier.passwordChanged, + ), + ); + } - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - transitionDuration: const Duration(milliseconds: 300), - ), - ); - }, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: const Text("Create Account"), - ), - ), - ], - ), - ), - ), - ), + Widget _buildSignInButton( + BuildContext context, + GlobalKey formKey, + AuthStates formStates, + AuthStateController formNotifier, + ) { + return SlideInWidget( + delay: 400, + child: AnimatedButton( + text: "Sign In", + icon: Icons.arrow_forward_rounded, + isLoading: formStates.isSubmitting, + onPressed: formStates.isSubmitting ? null : () => _handleSignIn(formKey, formNotifier), + ), + ); + } - const SizedBox(height: 24), - ], + Widget _buildSignUpPrompt(BuildContext context) { + return SlideInWidget( + delay: 500, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Don't have an account? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: () => _navigateToRegistration(context), + child: Text( + "Sign Up", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, ), ), ), - ), + ], + ), + ); + } + + String? _validateEmail(String? value) { + if (value == null || value.trim().isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + return "Please enter a valid email"; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 8) { + return "Password must be at least 8 characters"; + } + return null; + } + + void _handleSignIn(GlobalKey formKey, AuthStateController notifier) { + if (formKey.currentState?.validate() ?? false) { + HapticFeedback.lightImpact(); + notifier.signInWithEmailAndPassword(); + } else { + HapticFeedback.lightImpact(); + } + } + + void _navigateToRegistration(BuildContext context) { + Navigator.push( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => const RegistrationPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + return SlideTransition(position: animation.drive(tween), child: child); + }, + transitionDuration: const Duration(milliseconds: 300), ), ); } diff --git a/lib/screens/registration_page.dart b/lib/screens/registration_page.dart index b32545b..ec021ca 100644 --- a/lib/screens/registration_page.dart +++ b/lib/screens/registration_page.dart @@ -3,6 +3,7 @@ import "package:firebase_auth_flutter_ddd/domain/authentication/auth_failures.da import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "../application/authentication/auth_state_controller.dart"; @@ -11,67 +12,21 @@ import "login_page.dart"; import "utils/custom_snackbar.dart"; class RegistrationPage extends HookConsumerWidget { - RegistrationPage({super.key}); - - final formKey = GlobalKey(); - final emailController = TextEditingController(); - final passwordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); + const RegistrationPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + // Use hooks to create persistent controllers that survive rebuilds + final formKey = useMemoized(() => GlobalKey()); + final emailController = useTextEditingController(); + final passwordController = useTextEditingController(); + final confirmPasswordController = useTextEditingController(); + final formStates = ref.watch(authStateControllerProvider); - final formNotifier = ref.watch(authStateControllerProvider.notifier); + final formNotifier = ref.read(authStateControllerProvider.notifier); ref.listen(authStateControllerProvider, (previous, next) { - next.authFailureOrSuccess.fold( - () {}, - (either) => either.fold( - (failure) { - HapticFeedback.lightImpact(); - buildCustomSnackBar( - context: context, - flashBackground: Theme.of(context).colorScheme.error, - icon: Icons.error_outline_rounded, - content: Text( - failure.when( - serverError: () => "Server error occurred", - emailAlreadyInUse: () => "This email is already registered", - invalidEmailAndPasswordCombination: () => "Invalid email or password format"), - style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), - )); - }, - (success) { - HapticFeedback.lightImpact(); // Fixed from successImpact - buildCustomSnackBar( - context: context, - flashBackground: Theme.of(context).colorScheme.primary, - icon: CupertinoIcons.check_mark_circled_solid, - content: Text( - "Account created successfully! Welcome aboard!", - style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white), - )); - Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => LoginPage(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(-1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOutCubic; - - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); - - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - transitionDuration: const Duration(milliseconds: 300), - )); - }, - ), - ); + _handleAuthStateChanges(context, next); }); return Scaffold( @@ -84,253 +39,272 @@ class RegistrationPage extends HookConsumerWidget { child: Form( key: formKey, child: Column( + spacing: 20, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 20), + _buildBackButton(context), + _buildHeroSection(context), + const SizedBox(height: 28), + _buildEmailField(emailController, formNotifier), + _buildPasswordField(passwordController, formNotifier), + _buildConfirmPasswordField(confirmPasswordController, passwordController), + const SizedBox(height: 12), + _buildSignUpButton(context, formKey, formStates, formNotifier), + const SizedBox(height: 4), + _buildSignInPrompt(context), + ], + ), + ), + ), + ), + ), + ); + } - // Back Button - SlideInWidget( - delay: 50, - begin: const Offset(-0.3, 0), - child: Align( - alignment: Alignment.centerLeft, - child: IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon( - Icons.arrow_back_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ), - - const SizedBox(height: 20), - - // Hero Section with Animation - SlideInWidget( - delay: 100, - child: Column( - children: [ - Container( - height: 120, - width: 120, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - Icons.person_add_rounded, - size: 64, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - ), - const SizedBox(height: 32), - Text( - "Create Account", - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 8), - Text( - "Join us today and start your journey", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - - const SizedBox(height: 48), - - // Email Field - SlideInWidget( - delay: 200, - child: AnimatedFormField( - label: "Email Address", - hint: "Enter your email address", - keyboardType: TextInputType.emailAddress, - prefixIcon: Icons.email_outlined, - controller: emailController, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter your email"; - } - if (!value.contains("@")) { - return "Please enter a valid email"; - } - return null; - }, - onChanged: (email) { - formNotifier.emailChanged(email); - }, - ), - ), - - const SizedBox(height: 20), - - // Password Field - Simplified without showPassword dependency - SlideInWidget( - delay: 300, - child: AnimatedFormField( - label: "Password", - hint: "Create a strong password", - obscureText: true, - // Simplified to always obscure - prefixIcon: Icons.lock_outline, - controller: passwordController, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a password"; - } - if (value.length < 6) { - return "Password must be at least 6 characters"; - } - return null; - }, - onChanged: (password) { - formNotifier.passwordChanged(password); - }, - ), - ), + void _handleAuthStateChanges(BuildContext context, AuthStates state) { + state.authFailureOrSuccess.fold( + () {}, + (either) => either.fold( + (failure) => _showErrorSnackBar(context, failure), + (success) => _handleSuccessfulRegistration(context), + ), + ); + } - const SizedBox(height: 20), + void _showErrorSnackBar(BuildContext context, AuthFailures failure) { + HapticFeedback.lightImpact(); + final errorMessage = failure.when( + serverError: () => "Server error occurred", + emailAlreadyInUse: () => "This email is already registered", + invalidEmailAndPasswordCombination: () => "Invalid email or password format", + ); - // Confirm Password Field - Simplified without showPassword dependency - SlideInWidget( - delay: 400, - child: AnimatedFormField( - label: "Confirm Password", - hint: "Re-enter your password", - obscureText: true, - // Simplified to always obscure - prefixIcon: Icons.lock_outline, - controller: confirmPasswordController, - validator: (value) { - if (value == null || value.isEmpty) { - return "Please confirm your password"; - } - if (value != passwordController.text) { - return "Passwords do not match"; - } - return null; - }, - ), - ), + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.error, + icon: Icons.error_outline_rounded, + content: Text( + errorMessage, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ); + } - const SizedBox(height: 32), + void _handleSuccessfulRegistration(BuildContext context) { + HapticFeedback.lightImpact(); + buildCustomSnackBar( + context: context, + flashBackground: Theme.of(context).colorScheme.primary, + icon: CupertinoIcons.check_mark_circled_solid, + content: Text( + "Account created successfully! Welcome aboard!", + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ); - // Terms and Conditions - SlideInWidget( - delay: 500, - child: Card( - color: Theme.of(context).colorScheme.surfaceContainerHigh.withValues(alpha: 0.3), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline_rounded, - color: Theme.of(context).colorScheme.primary, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - "By creating an account, you agree to our Terms of Service and Privacy Policy.", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ), - ), - ), + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => const LoginPage(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(-1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOutCubic; + final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + return SlideTransition(position: animation.drive(tween), child: child); + }, + transitionDuration: const Duration(milliseconds: 300), + ), + ); + } - const SizedBox(height: 32), + Widget _buildBackButton(BuildContext context) { + return SlideInWidget( + delay: 50, + begin: const Offset(-0.3, 0), + child: Align( + alignment: Alignment.centerLeft, + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ); + } - // Register Button - SlideInWidget( - delay: 600, - child: AnimatedButton( - text: "Create Account", - icon: Icons.person_add_rounded, - isLoading: formStates.isSubmitting, - onPressed: formStates.isSubmitting - ? null - : () { - if (formKey.currentState!.validate()) { - HapticFeedback.lightImpact(); - formNotifier.signUpWithEmailAndPassword(); // Fixed method name - } else { - HapticFeedback.lightImpact(); - } - }, - ), - ), + Widget _buildHeroSection(BuildContext context) { + return SlideInWidget( + delay: 100, + child: Column( + children: [ + Container( + height: 120, + width: 120, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person_add_rounded, + size: 64, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 32), + Text( + "Create Account", + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + "Join us and start your journey today", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } - const SizedBox(height: 32), + Widget _buildEmailField(TextEditingController controller, AuthStateController notifier) { + return SlideInWidget( + delay: 200, + child: AnimatedFormField( + label: "Email", + hint: "Enter your email address", + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + controller: controller, + validator: _validateEmail, + onChanged: notifier.emailChanged, + ), + ); + } - // Login Section - SlideInWidget( - delay: 700, - child: Card( - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Text( - "Already have an account?", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: TextButton( - onPressed: () { - Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => LoginPage(), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(-1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOutCubic; + Widget _buildPasswordField(TextEditingController controller, AuthStateController notifier) { + return SlideInWidget( + delay: 300, + child: AnimatedFormField( + label: "Password", + hint: "Create a strong password", + obscureText: true, + prefixIcon: Icons.lock_outline, + controller: controller, + validator: _validatePassword, + onChanged: notifier.passwordChanged, + ), + ); + } - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + Widget _buildConfirmPasswordField(TextEditingController controller, TextEditingController passwordController) { + return SlideInWidget( + delay: 350, + child: AnimatedFormField( + label: "Confirm Password", + hint: "Confirm your password", + obscureText: true, + prefixIcon: Icons.lock_outline, + controller: controller, + validator: (value) => _validateConfirmPassword(value, passwordController.text), + ), + ); + } - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - transitionDuration: const Duration(milliseconds: 300), - ), - ); - }, - child: const Text("Sign In Instead"), - ), - ), - ], - ), - ), - ), - ), + Widget _buildSignUpButton( + BuildContext context, + GlobalKey formKey, + AuthStates formStates, + AuthStateController formNotifier, + ) { + return SlideInWidget( + delay: 400, + child: AnimatedButton( + text: "Create Account", + icon: Icons.arrow_forward_rounded, + isLoading: formStates.isSubmitting, + onPressed: formStates.isSubmitting ? null : () => _handleSignUp(formKey, formNotifier), + ), + ); + } - const SizedBox(height: 24), - ], + Widget _buildSignInPrompt(BuildContext context) { + return SlideInWidget( + delay: 500, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Already have an account? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + "Sign In", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, ), ), ), - ), + ], ), ); } + + String? _validateEmail(String? value) { + if (value == null || value.trim().isEmpty) { + return "Please enter your email"; + } + if (!value.contains("@")) { + return "Please enter a valid email"; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return "Please enter your password"; + } + if (value.length < 8) { + return "Password must be at least 8 characters"; + } + if (!RegExp(r"[A-Z]").hasMatch(value)) { + return "Password must contain at least one uppercase letter"; + } + if (!RegExp(r"[0-9]").hasMatch(value)) { + return "Password must contain at least one number"; + } + if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) { + return "Password must contain at least one special character"; + } + return null; + } + + String? _validateConfirmPassword(String? value, String password) { + if (value == null || value.isEmpty) { + return "Please confirm your password"; + } + if (value != password) { + return "Passwords do not match"; + } + return null; + } + + void _handleSignUp(GlobalKey formKey, AuthStateController notifier) { + if (formKey.currentState?.validate() ?? false) { + HapticFeedback.lightImpact(); + notifier.signUpWithEmailAndPassword(); + } else { + HapticFeedback.lightImpact(); + } + } } diff --git a/lib/services/authentication/firebase_auth_facade.dart b/lib/services/authentication/firebase_auth_facade.dart index de63593..98e1960 100644 --- a/lib/services/authentication/firebase_auth_facade.dart +++ b/lib/services/authentication/firebase_auth_facade.dart @@ -7,52 +7,44 @@ import "../../domain/authentication/auth_value_objects.dart"; import "../../domain/authentication/i_auth_facade.dart"; import "../../domain/core/errors.dart"; -// Manual providers without code generation -final firebaseAuthProvider = Provider((ref) { - return FirebaseAuth.instance; -}); +// Optimized providers with better separation of concerns +final firebaseAuthProvider = Provider((ref) => FirebaseAuth.instance); -final firebaseAuthFacadeProvider = Provider((ref) { +final firebaseAuthFacadeProvider = Provider((ref) { return FirebaseAuthFacade(ref.read(firebaseAuthProvider)); }); class FirebaseAuthFacade implements IAuthFacade { - FirebaseAuthFacade(this._firebaseAuth); + const FirebaseAuthFacade(this._firebaseAuth); final FirebaseAuth _firebaseAuth; @override - Future> registerWithEmailAndPassword( - {required EmailAddress? emailAddress, required Password? password}) async { - final emailAddressString = emailAddress!.valueObject!.fold((l) => throw UnExpectedValueError(l), (r) => r); - final passwordString = password!.valueObject!.fold((l) => throw UnExpectedValueError(l), (r) => r); - try { - await _firebaseAuth.createUserWithEmailAndPassword(email: emailAddressString, password: passwordString); - return right(unit); - } on FirebaseAuthException catch (e) { - if (e.code == "email-already-in-use") { - return left(const AuthFailures.emailAlreadyInUse()); - } else { - return left(const AuthFailures.serverError()); - } - } + Future> registerWithEmailAndPassword({ + required EmailAddress emailAddress, + required Password password, + }) async { + return _executeAuthOperation( + () => _firebaseAuth.createUserWithEmailAndPassword( + email: _extractEmailValue(emailAddress), + password: _extractPasswordValue(password), + ), + onEmailAlreadyInUse: () => const AuthFailures.emailAlreadyInUse(), + ); } @override - Future> signInWithEmailAndPassword( - {required EmailAddress? emailAddress, required Password? password}) async { - final emailAddressString = emailAddress!.valueObject!.fold((l) => throw UnExpectedValueError(l), (r) => r); - final passwordString = password!.valueObject!.fold((l) => throw UnExpectedValueError(l), (r) => r); - try { - await _firebaseAuth.signInWithEmailAndPassword(email: emailAddressString, password: passwordString); - return right(unit); - } on FirebaseAuthException catch (e) { - if (e.code == "wrong-password" || e.code == "user-not-found") { - return left(const AuthFailures.invalidEmailAndPasswordCombination()); - } else { - return left(const AuthFailures.serverError()); - } - } + Future> signInWithEmailAndPassword({ + required EmailAddress emailAddress, + required Password password, + }) async { + return _executeAuthOperation( + () => _firebaseAuth.signInWithEmailAndPassword( + email: _extractEmailValue(emailAddress), + password: _extractPasswordValue(password), + ), + onInvalidCredentials: () => const AuthFailures.invalidEmailAndPasswordCombination(), + ); } @override @@ -64,4 +56,62 @@ class FirebaseAuthFacade implements IAuthFacade { Future signOut() async { await _firebaseAuth.signOut(); } + + // Helper method to extract email value safely + String _extractEmailValue(EmailAddress emailAddress) { + return emailAddress.valueObject?.fold( + (failure) => throw UnExpectedValueError(failure), + (email) => email, + ) ?? + ""; + } + + // Helper method to extract password value safely + String _extractPasswordValue(Password password) { + return password.valueObject?.fold( + (failure) => throw UnExpectedValueError(failure), + (pwd) => pwd, + ) ?? + ""; + } + + // Generic method to handle Firebase Auth operations with proper error mapping + Future> _executeAuthOperation( + Future Function() operation, { + AuthFailures Function()? onEmailAlreadyInUse, + AuthFailures Function()? onInvalidCredentials, + }) async { + try { + await operation(); + return right(unit); + } on FirebaseAuthException catch (e) { + return left(_mapFirebaseError(e, onEmailAlreadyInUse, onInvalidCredentials)); + } catch (e) { + return left(const AuthFailures.serverError()); + } + } + + // Centralized Firebase error mapping + AuthFailures _mapFirebaseError( + FirebaseAuthException exception, + AuthFailures Function()? onEmailAlreadyInUse, + AuthFailures Function()? onInvalidCredentials, + ) { + switch (exception.code) { + case "email-already-in-use": + return onEmailAlreadyInUse?.call() ?? const AuthFailures.serverError(); + case "wrong-password": + case "user-not-found": + case "invalid-email": + case "user-disabled": + case "invalid-credential": + return onInvalidCredentials?.call() ?? const AuthFailures.serverError(); + case "weak-password": + case "operation-not-allowed": + case "too-many-requests": + case "network-request-failed": + default: + return const AuthFailures.serverError(); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 4991bea..8970dd7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241 + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 url: "https://pub.dev" source: hosted - version: "1.3.58" + version: "1.3.59" analyzer: dependency: transitive description: @@ -221,34 +221,34 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: f5b640f664aae71774b398ed765740c1b5d34a339f4c4975d4dde61d59a623f6 + sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" url: "https://pub.dev" source: hosted - version: "5.6.2" + version: "5.7.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "62199aeda6a688cbdefbcbbac53ede71be3ac8807cec00a8066d444797a08806" + sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" url: "https://pub.dev" source: hosted - version: "7.7.2" + version: "7.7.3" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: caaf29b7eb9d212dcec36d2eaa66504c5bd523fe844302833680c9df8460fbc0 + sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e url: "https://pub.dev" source: hosted - version: "5.15.2" + version: "5.15.3" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" url: "https://pub.dev" source: hosted - version: "3.15.1" + version: "3.15.2" firebase_core_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d0fb6cc..27965ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,25 +8,25 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.2.3 <4.0.0' + sdk: '>=3.2.3 <4.0.0' dependencies: - cupertino_icons: ^1.0.8 - firebase_auth: ^5.6.2 - firebase_core: ^3.15.1 - flutter: - sdk: flutter - flutter_hooks: ^0.21.2 - fpdart: ^1.1.1 - freezed_annotation: ^3.1.0 - hooks_riverpod: ^3.0.0-dev.16 + cupertino_icons: ^1.0.8 + firebase_auth: ^5.7.0 + firebase_core: ^3.15.2 + flutter: + sdk: flutter + flutter_hooks: ^0.21.2 + fpdart: ^1.1.1 + freezed_annotation: ^3.1.0 + hooks_riverpod: ^3.0.0-dev.16 dev_dependencies: - build_runner: ^2.6.0 - flutter_lints: ^6.0.0 - flutter_test: - sdk: flutter - freezed: ^3.2.0 + build_runner: ^2.6.0 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + freezed: ^3.2.0 flutter: - uses-material-design: true + uses-material-design: true