From 03a51b97ad2979007edaf8e9ea564d1f819c7568 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 11 Nov 2022 16:53:36 -0500 Subject: [PATCH] feat(auth)!: add email verification, password reset support --- .../lib/src/core/constants/form_name.dart | 1 + .../lib/src/core/exceptions/exceptions.dart | 7 + .../core/exceptions/exceptions_firebase.dart | 7 + ...hentication_firebase_data_source_impl.dart | 98 ++++++++++++- .../lib/src/data/models/account_model.dart | 40 ++++- .../authentication_repository_impl.dart | 66 +++++++++ .../authentication_remote_data_source.dart | 15 ++ .../lib/src/domain/entities/account.dart | 51 ++++++- .../src/domain/entities/account_wrapper.dart | 6 +- .../authentication_repository.dart | 65 +++++++++ .../cubit/authentication_cubit.dart | 6 +- .../cubit/authentication_state.dart | 2 +- .../builder/email_verification_builder.dart | 65 +++++++++ .../cubit/email_verification_cubit.dart | 77 ++++++++++ .../cubit/email_verification_state.dart | 49 +++++++ .../email_verification.dart | 18 +++ .../lib/src/features/features.dart | 8 +- .../cubit/password_reset_cubit.dart | 138 ++++++++++++++++++ .../cubit/password_reset_state.dart | 45 ++++++ .../password_reset/password_reset.dart | 17 +++ .../sign_in/listener/sign_in_listener.dart | 66 +++++++++ .../lib/src/features/sign_in/sign_in.dart | 7 +- .../sign_up/listener/sign_up_listener.dart | 66 +++++++++ .../lib/src/features/sign_up/sign_up.dart | 7 +- 24 files changed, 907 insertions(+), 20 deletions(-) create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/email_verification/builder/email_verification_builder.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_cubit.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_state.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/email_verification/email_verification.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_cubit.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_state.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/password_reset/password_reset.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/sign_in/listener/sign_in_listener.dart create mode 100644 packages/wyatt_authentication_bloc/lib/src/features/sign_up/listener/sign_up_listener.dart diff --git a/packages/wyatt_authentication_bloc/lib/src/core/constants/form_name.dart b/packages/wyatt_authentication_bloc/lib/src/core/constants/form_name.dart index 5a788715..af623e70 100644 --- a/packages/wyatt_authentication_bloc/lib/src/core/constants/form_name.dart +++ b/packages/wyatt_authentication_bloc/lib/src/core/constants/form_name.dart @@ -17,4 +17,5 @@ abstract class AuthFormName { static const String signUpForm = 'wyattSignUpForm'; static const String signInForm = 'wyattSignInForm'; + static const String passwordResetForm = 'wyattPasswordResetForm'; } diff --git a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart index 11e7835d..5ef49c83 100644 --- a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart +++ b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart @@ -246,3 +246,10 @@ abstract class SignOutFailureInterface extends AuthenticationFailureInterface { /// {@macro sign_out_failure} SignOutFailureInterface.fromCode(super.code) : super.fromCode(); } + +abstract class GetIdTokenFailureInterface + extends AuthenticationFailureInterface { + GetIdTokenFailureInterface(super.code, super.msg); + + GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode(); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart index 42f410a2..7a6e37b4 100644 --- a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart +++ b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart @@ -270,3 +270,10 @@ class SignOutFailureFirebase extends SignOutFailureInterface { SignOutFailureFirebase.fromCode(super.code) : super.fromCode(); } + +class GetIdTokenFailureFirebase extends GetIdTokenFailureInterface { + GetIdTokenFailureFirebase([String? code, String? msg]) + : super(code ?? 'unknown', msg ?? 'An unknown error occurred.'); + + GetIdTokenFailureFirebase.fromCode(super.code) : super.fromCode(); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart index f0c55b8d..565b76cc 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart @@ -15,7 +15,6 @@ // along with this program. If not, see . import 'package:firebase_auth/firebase_auth.dart'; -import 'package:wyatt_architecture/wyatt_architecture.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_type_utils/wyatt_type_utils.dart'; @@ -28,7 +27,18 @@ class AuthenticationFirebaseDataSourceImpl Account _mapper(User user) => AccountModel( uid: user.uid, + emailVerified: user.emailVerified, + isAnonymous: user.isAnonymous, + providerId: user.providerData.first.providerId, + creationTime: user.metadata.creationTime, + lastSignInTime: user.metadata.lastSignInTime, + isNewUser: (user.metadata.creationTime != null && + user.metadata.lastSignInTime != null) + ? user.metadata.lastSignInTime! == user.metadata.creationTime! + : null, email: user.email, + phoneNumber: user.phoneNumber, + photoURL: user.photoURL, ); @override @@ -55,6 +65,8 @@ class AuthenticationFirebaseDataSourceImpl } @override + + /// {@macro signup} Future signUp({ required String email, required String password, @@ -95,9 +107,10 @@ class AuthenticationFirebaseDataSourceImpl } else { throw Exception(); // Get caught just after. } + } on FirebaseAuthException catch (e) { + throw GetIdTokenFailureFirebase.fromCode(e.code); } catch (_) { - // TODO(hpcl): implement a non ambiguous exception for this case - throw ServerException(); + throw GetIdTokenFailureFirebase(); } } @@ -107,4 +120,83 @@ class AuthenticationFirebaseDataSourceImpl final Account? account = (user.isNotNull) ? _mapper(user!) : null; return account; }); + + @override + Future confirmPasswordReset({ + required String code, + required String newPassword, + }) async { + try { + await _firebaseAuth.confirmPasswordReset( + code: code, + newPassword: newPassword, + ); + } on FirebaseAuthException catch (e) { + throw ConfirmPasswordResetFailureFirebase.fromCode(e.code); + } catch (_) { + throw ConfirmPasswordResetFailureFirebase(); + } + } + + @override + Future sendEmailVerification() async { + try { + await _firebaseAuth.currentUser!.sendEmailVerification(); + } on FirebaseAuthException catch (e) { + throw SendEmailVerificationFailureFirebase.fromCode(e.code); + } catch (_) { + throw SendEmailVerificationFailureFirebase(); + } + } + + @override + Future sendPasswordResetEmail({required String email}) async { + try { + await _firebaseAuth.sendPasswordResetEmail(email: email); + } on FirebaseAuthException catch (e) { + throw SendPasswordResetEmailFailureFirebase.fromCode(e.code); + } catch (_) { + throw SendPasswordResetEmailFailureFirebase(); + } + } + + @override + Future signInAnonymously() async { + try { + final userCredential = await _firebaseAuth.signInAnonymously(); + final user = userCredential.user; + if (user.isNotNull) { + return _mapper(user!); + } else { + throw Exception(); // Get caught just after. + } + } on FirebaseAuthException catch (e) { + throw SignInAnonymouslyFailureFirebase.fromCode(e.code); + } catch (_) { + throw SignInAnonymouslyFailureFirebase(); + } + } + + @override + Future verifyPasswordResetCode({required String code}) async { + try { + final email = await _firebaseAuth.verifyPasswordResetCode(code); + return email.isNotNullOrEmpty; + } on FirebaseAuthException catch (e) { + throw VerifyPasswordResetCodeFailureFirebase.fromCode(e.code); + } catch (_) { + throw VerifyPasswordResetCodeFailureFirebase(); + } + } + + @override + Future refresh() async { + try { + await _firebaseAuth.currentUser!.reload(); + } on FirebaseAuthException catch (e) { + throw RefreshFailureFirebase.fromCode(e.code); + } catch (_) { + throw RefreshFailureFirebase(); + } + } } diff --git a/packages/wyatt_authentication_bloc/lib/src/data/models/account_model.dart b/packages/wyatt_authentication_bloc/lib/src/data/models/account_model.dart index 0d1d6b65..2405747d 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/models/account_model.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/models/account_model.dart @@ -1,3 +1,4 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. // @@ -16,12 +17,47 @@ import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart'; -class AccountModel implements Account { +class AccountModel extends Account { @override final String uid; @override final String? email; - AccountModel({required this.uid, required this.email}); + @override + final DateTime? creationTime; + + @override + final bool emailVerified; + + @override + final bool isAnonymous; + + @override + final bool? isNewUser; + + @override + final DateTime? lastSignInTime; + + @override + final String? phoneNumber; + + @override + final String? photoURL; + + @override + final String providerId; + + AccountModel({ + required this.uid, + required this.emailVerified, + required this.isAnonymous, + required this.providerId, + this.lastSignInTime, + this.creationTime, + this.isNewUser, + this.email, + this.phoneNumber, + this.photoURL, + }); } diff --git a/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart b/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart index 00bf067c..a3ecfb6f 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart @@ -208,4 +208,70 @@ class AuthenticationRepositoryImpl AccountWrapperModel(account, null), ); }); + + @override + FutureResult confirmPasswordReset({ + required String code, + required String newPassword, + }) => + Result.tryCatchAsync( + () async { + await _authenticationRemoteDataSource.confirmPasswordReset( + code: code, + newPassword: newPassword, + ); + }, + (error) => error, + ); + + @override + FutureResult sendEmailVerification() => + Result.tryCatchAsync( + () async { + await _authenticationRemoteDataSource.sendEmailVerification(); + }, + (error) => error, + ); + + @override + FutureResult sendPasswordResetEmail({required String email}) => + Result.tryCatchAsync( + () async { + await _authenticationRemoteDataSource.sendPasswordResetEmail( + email: email, + ); + }, + (error) => error, + ); + + @override + FutureResult signInAnonymously() => + Result.tryCatchAsync( + () async { + final account = + await _authenticationRemoteDataSource.signInAnonymously(); + return account; + }, + (error) => error, + ); + + @override + FutureResult verifyPasswordResetCode({required String code}) => + Result.tryCatchAsync( + () async { + final response = await _authenticationRemoteDataSource + .verifyPasswordResetCode(code: code); + return response; + }, + (error) => error, + ); + + @override + FutureResult refresh() => + Result.tryCatchAsync( + () async { + await _authenticationRemoteDataSource.refresh(); + }, + (error) => error, + ); } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart b/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart index 3bd615e1..755aa157 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart @@ -30,7 +30,22 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource { Future signOut(); + Future refresh(); + Stream streamAccount(); Future getIdentityToken(); + + Future sendEmailVerification(); + + Future sendPasswordResetEmail({required String email}); + + Future confirmPasswordReset({ + required String code, + required String newPassword, + }); + + Future verifyPasswordResetCode({required String code}); + + Future signInAnonymously(); } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/entities/account.dart b/packages/wyatt_authentication_bloc/lib/src/domain/entities/account.dart index 97d5deeb..976be6be 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/entities/account.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/entities/account.dart @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:equatable/equatable.dart'; import 'package:wyatt_architecture/wyatt_architecture.dart'; -abstract class Account extends Entity { +abstract class Account extends Equatable implements Entity { /// The user's unique ID. String get uid; @@ -24,4 +25,52 @@ abstract class Account extends Entity { /// /// Will be `null` if signing in anonymously. String? get email; + + /// Returns whether the users email address has been verified. + /// + /// To send a verification email, see `SendEmailVerification`. + bool get emailVerified; + + /// Returns whether the user is a anonymous. + bool get isAnonymous; + + /// Returns the users account creation time. + /// + /// When this account was created as dictated by the server clock. + DateTime? get creationTime; + + /// When the user last signed in as dictated by the server clock. + DateTime? get lastSignInTime; + + /// Returns the users phone number. + /// + /// This property will be `null` if the user has not signed in or been has + /// their phone number linked. + String? get phoneNumber; + + /// Returns a photo URL for the user. + /// + /// This property will be populated if the user has signed in or been linked + /// with a 3rd party OAuth provider (such as Google). + String? get photoURL; + + /// The provider ID for the user. + String get providerId; + + /// Whether the user account has been recently created. + bool? get isNewUser; + + @override + List get props => [ + uid, + email, + emailVerified, + isAnonymous, + creationTime, + lastSignInTime, + phoneNumber, + photoURL, + providerId, + isNewUser, + ]; } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/entities/account_wrapper.dart b/packages/wyatt_authentication_bloc/lib/src/domain/entities/account_wrapper.dart index 1e1f0034..a83b1f80 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/entities/account_wrapper.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/entities/account_wrapper.dart @@ -14,10 +14,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:equatable/equatable.dart'; import 'package:wyatt_architecture/wyatt_architecture.dart'; import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart'; -abstract class AccountWrapper extends Entity { +abstract class AccountWrapper extends Equatable implements Entity { Account? get account; T? get data; + + @override + List get props => [account, data]; } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart b/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart index cece19c9..d1afeb42 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart @@ -22,18 +22,83 @@ import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; abstract class AuthenticationRepository extends BaseRepository { FormRepository get formRepository; + /// {@template signup} + /// Creates a new user with the provided [email] and [password]. + /// + /// Returns the newly created user's unique identifier. + /// + /// Throws a SignUpWithEmailAndPasswordFailureInterface if + /// an exception occurs. + /// {@endtemplate} FutureResult signUp({ required String email, required String password, }); + /// {@template send_email_verification} + /// Sends verification email to the account email. + /// + /// Throws a SendEmailVerificationFailureInterface if an exception occurs. + /// {@endtemplate} + FutureResult sendEmailVerification(); + + /// {@template send_password_reset_email} + /// Sends a password reset email to the provided [email]. + /// + /// Throws a SendPasswordResetEmailFailureInterface if an exception occurs. + /// {@endtemplate} + FutureResult sendPasswordResetEmail({required String email}); + + /// {@template confirm_password_reset} + /// Confirms the password reset with the provided [newPassword] and [code]. + /// + /// Throws a ConfirmPasswordResetFailureInterface if an exception occurs. + /// {@endtemplate} + FutureResult confirmPasswordReset({ + required String code, + required String newPassword, + }); + + /// {@template verify_password_reset_code} + /// Verify password reset code. + /// + /// Throws a VerifyPasswordResetCodeFailureInterface if an exception occurs. + /// {@endtemplate} + FutureResult verifyPasswordResetCode({required String code}); + + /// {@template signin_anom} + /// Sign in anonymously. + /// + /// Throws a SignInAnonymouslyFailureInterface if an exception occurs. + /// {@endtemplate} + FutureResult signInAnonymously(); + + /// {@template signin_pwd} + /// Signs in with the provided [email] and [password]. + /// + /// Throws a SignInWithEmailAndPasswordFailureInterface if + /// an exception occurs. + /// {@endtemplate} FutureResult signInWithEmailAndPassword({ required String email, required String password, }); + /// {@template signout} + /// Signs out the current user. + /// It also clears the cache and the associated data. + /// {@endtemplate} FutureResult signOut(); + FutureResult refresh(); + + /// {@template stream_account} + /// Stream of [AccountWrapper] which will emit the current account when + /// the authentication state changes. + /// + /// Emits [AccountWrapper] with null [Account] if the user is not + /// authenticated. + /// {@endtemplate} Stream>> streamAccount(); FutureResult getIdentityToken(); diff --git a/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_cubit.dart b/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_cubit.dart index af9cb0b0..7da6ff32 100644 --- a/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_cubit.dart +++ b/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_cubit.dart @@ -40,16 +40,16 @@ class AuthenticationCubit extends Cubit> { accountFutureResult.fold( (value) { if (value.account.isNotNull) { - emit(AuthenticationState.authenticated(value)); + emit(AuthenticationState.authenticated(value)); return; } _authenticationRepository.destroyCache(); - emit(const AuthenticationState.unauthenticated()); + emit(AuthenticationState.unauthenticated()); return; }, (error) { _authenticationRepository.destroyCache(); - emit(const AuthenticationState.unauthenticated()); + emit(AuthenticationState.unauthenticated()); return; }, ); diff --git a/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_state.dart b/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_state.dart index 0a3bc98e..bfd1e871 100644 --- a/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_state.dart +++ b/packages/wyatt_authentication_bloc/lib/src/features/authentication/cubit/authentication_state.dart @@ -35,7 +35,7 @@ class AuthenticationState extends Equatable { : this._(status: AuthenticationStatus.unauthenticated); @override - List get props => [status]; + List get props => [status, accountWrapper]; @override String toString() => diff --git a/packages/wyatt_authentication_bloc/lib/src/features/email_verification/builder/email_verification_builder.dart b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/builder/email_verification_builder.dart new file mode 100644 index 00000000..2bb77e9c --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/builder/email_verification_builder.dart @@ -0,0 +1,65 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wyatt_authentication_bloc/src/features/email_verification/cubit/email_verification_cubit.dart'; +import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; + +class EmailVerificationBuilder extends StatelessWidget { + const EmailVerificationBuilder({ + required this.verified, + required this.notVerified, + required this.onError, + this.customBuilder, + super.key, + }); + + final Widget Function(BuildContext context) verified; + final Widget Function(BuildContext context) notVerified; + final Widget Function( + BuildContext context, + FormStatus status, + String? errorMessage, + ) onError; + + final Widget Function(BuildContext context, EmailVerificationState)? + customBuilder; + + @override + Widget build(BuildContext context) => + BlocBuilder, EmailVerificationState>( + builder: (context, state) { + if (customBuilder != null) { + return customBuilder!(context, state); + } + + if (state.status.isSubmissionFailure || + state.status.isSubmissionCanceled) { + return onError( + context, + state.status, + state.errorMessage, + ); + } + + if (state.isVerified) { + return verified(context); + } + return notVerified(context); + }, + ); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_cubit.dart b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_cubit.dart new file mode 100644 index 00000000..ea829bb5 --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_cubit.dart @@ -0,0 +1,77 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wyatt_authentication_bloc/src/domain/repositories/repositories.dart'; +import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; + +part 'email_verification_state.dart'; + +class EmailVerificationCubit extends Cubit { + final AuthenticationRepository _authenticationRepository; + + EmailVerificationCubit({ + required AuthenticationRepository authenticationRepository, + }) : _authenticationRepository = authenticationRepository, + super(const EmailVerificationState()); + + FutureOr sendEmailVerification() async { + emit(state.copyWith(status: FormStatus.submissionInProgress)); + final response = await _authenticationRepository.sendEmailVerification(); + emit( + response.fold( + (value) => state.copyWith(status: FormStatus.submissionSuccess), + (error) => state.copyWith( + errorMessage: error.message, + status: FormStatus.submissionFailure, + ), + ), + ); + } + + FutureOr checkEmailVerification() async { + emit(state.copyWith(status: FormStatus.submissionInProgress)); + + final refresh = await _authenticationRepository.refresh(); + if (refresh.isErr) { + final refreshError = refresh.err!; + emit( + EmailVerificationState( + errorMessage: refreshError.message, + status: FormStatus.submissionFailure, + ), + ); + return; + } + + final currentAccount = await _authenticationRepository.getAccount(); + emit( + currentAccount.fold( + (value) => state.copyWith( + isVerified: value.emailVerified, + status: FormStatus.submissionSuccess, + ), + (error) => EmailVerificationState( + errorMessage: error.message, + status: FormStatus.submissionFailure, + ), + ), + ); + } +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_state.dart b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_state.dart new file mode 100644 index 00000000..5f9382ac --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/cubit/email_verification_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +part of 'email_verification_cubit.dart'; + +class EmailVerificationState extends Equatable { + final FormStatus status; + final bool isVerified; + final String? errorMessage; + + const EmailVerificationState({ + this.isVerified = false, + this.status = FormStatus.pure, + this.errorMessage, + }); + + EmailVerificationState copyWith({ + FormStatus? status, + bool? isVerified, + String? errorMessage, + }) => + EmailVerificationState( + status: status ?? this.status, + isVerified: isVerified ?? this.isVerified, + errorMessage: errorMessage ?? this.errorMessage, + ); + + @override + List get props => [status, isVerified, errorMessage]; + + @override + String toString() => 'EmailVerificationState(status: ${status.name} ' + '${(errorMessage != null) ? " [$errorMessage]" : ""}, ' + 'isVerified: $isVerified)'; +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/email_verification/email_verification.dart b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/email_verification.dart new file mode 100644 index 00000000..5624d94e --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/email_verification/email_verification.dart @@ -0,0 +1,18 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +export 'builder/email_verification_builder.dart'; +export 'cubit/email_verification_cubit.dart'; diff --git a/packages/wyatt_authentication_bloc/lib/src/features/features.dart b/packages/wyatt_authentication_bloc/lib/src/features/features.dart index e1b478e9..bd39a7f6 100644 --- a/packages/wyatt_authentication_bloc/lib/src/features/features.dart +++ b/packages/wyatt_authentication_bloc/lib/src/features/features.dart @@ -1,19 +1,21 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . export 'authentication/authentication.dart'; +export 'email_verification/email_verification.dart'; +export 'password_reset/password_reset.dart'; export 'sign_in/sign_in.dart'; export 'sign_up/sign_up.dart'; diff --git a/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_cubit.dart b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_cubit.dart new file mode 100644 index 00000000..8b793973 --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_cubit.dart @@ -0,0 +1,138 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:async'; + +import 'package:wyatt_authentication_bloc/src/core/constants/form_field.dart'; +import 'package:wyatt_authentication_bloc/src/core/constants/form_name.dart'; +import 'package:wyatt_authentication_bloc/src/domain/repositories/authentication_repository.dart'; +import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; +import 'package:wyatt_type_utils/wyatt_type_utils.dart'; + +part 'password_reset_state.dart'; + +class PasswordResetCubit extends FormDataCubit { + final AuthenticationRepository _authenticationRepository; + FormRepository get _formRepository => + _authenticationRepository.formRepository; + + PasswordResetCubit({ + required AuthenticationRepository authenticationRepository, + }) : _authenticationRepository = authenticationRepository, + super( + PasswordResetState( + form: authenticationRepository.formRepository + .accessForm(AuthFormName.passwordResetForm), + ), + ); + + @override + String get formName => AuthFormName.passwordResetForm; + + void emailChanged(String value) { + final Email email = Email.dirty(value); + dataChanged(AuthFormField.email, email); + } + + @override + FutureOr dataChanged( + String key, + FormInputValidator dirtyValue, + ) { + final form = _formRepository.accessForm(formName).clone(); + + try { + form.updateValidator(key, dirtyValue); + _formRepository.updateForm(form); + } catch (e) { + rethrow; + } + + emit( + state.copyWith(form: form, status: form.validate()), + ); + } + + @override + FutureOr reset() { + final form = state.form.reset(); + _formRepository.updateForm(form); + emit( + state.copyWith(form: form, status: form.validate()), + ); + } + + @override + FutureOr submit() async { + if (!state.status.isValidated) { + return; + } + + emit(state.copyWith(status: FormStatus.submissionInProgress)); + + final form = _formRepository.accessForm(formName); + final email = form.valueOf(AuthFormField.email); + + if (email.isNullOrEmpty) { + emit( + state.copyWith( + errorMessage: 'An error occured while retrieving data from the form.', + status: FormStatus.submissionFailure, + ), + ); + } + + final response = await _authenticationRepository.sendPasswordResetEmail( + email: email!, + ); + + emit( + response.fold( + (value) => state.copyWith(status: FormStatus.submissionSuccess), + (error) => state.copyWith( + errorMessage: error.message, + status: FormStatus.submissionFailure, + ), + ), + ); + } + + @override + FutureOr update( + WyattForm form, { + SetOperation operation = SetOperation.replace, + }) { + final WyattForm current = _formRepository.accessForm(formName).clone(); + final WyattForm newForm = operation.operation.call(current, form); + _formRepository.updateForm(newForm); + + emit( + state.copyWith( + form: newForm, + status: newForm.validate(), + ), + ); + } + + @override + FutureOr validate() { + emit( + state.copyWith( + status: _formRepository.accessForm(formName).validate(), + ), + ); + } +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_state.dart b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_state.dart new file mode 100644 index 00000000..8a11957d --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/cubit/password_reset_state.dart @@ -0,0 +1,45 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +part of 'password_reset_cubit.dart'; + +class PasswordResetState extends FormDataState { + Email get email => form.validatorOf(AuthFormField.email); + + const PasswordResetState({ + required super.form, + super.status = FormStatus.pure, + super.errorMessage, + }); + + PasswordResetState copyWith({ + WyattForm? form, + FormStatus? status, + String? errorMessage, + }) => + PasswordResetState( + form: form ?? this.form, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + + @override + List get props => [email, status]; + + @override + String toString() => 'PasswordResetState(status: ${status.name} ' + '${(errorMessage != null) ? " [$errorMessage]" : ""}, $form)'; +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/password_reset/password_reset.dart b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/password_reset.dart new file mode 100644 index 00000000..43919e02 --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/password_reset/password_reset.dart @@ -0,0 +1,17 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +export 'cubit/password_reset_cubit.dart'; diff --git a/packages/wyatt_authentication_bloc/lib/src/features/sign_in/listener/sign_in_listener.dart b/packages/wyatt_authentication_bloc/lib/src/features/sign_in/listener/sign_in_listener.dart new file mode 100644 index 00000000..e51a1edd --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/sign_in/listener/sign_in_listener.dart @@ -0,0 +1,66 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wyatt_authentication_bloc/src/features/sign_in/sign_in.dart'; +import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; + +class SignInListener extends StatelessWidget { + const SignInListener({ + required this.child, + this.onProgress, + this.onSuccess, + this.onError, + this.customBuilder, + super.key, + }); + + final void Function(BuildContext context)? onProgress; + final void Function(BuildContext context)? onSuccess; + final void Function( + BuildContext context, + FormStatus status, + String? errorMessage, + )? onError; + final void Function(BuildContext context, SignInState state)? customBuilder; + final Widget child; + + @override + Widget build(BuildContext context) => + BlocListener, SignInState>( + listener: (context, state) { + if (customBuilder != null) { + return customBuilder!(context, state); + } + + if (onSuccess != null && + state.status == FormStatus.submissionSuccess) { + return onSuccess!(context); + } + if (onProgress != null && + state.status == FormStatus.submissionInProgress) { + return onProgress!(context); + } + if (onError != null && + (state.status == FormStatus.submissionCanceled || + state.status == FormStatus.submissionFailure)) { + return onError!(context, state.status, state.errorMessage); + } + }, + child: child, + ); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/sign_in/sign_in.dart b/packages/wyatt_authentication_bloc/lib/src/features/sign_in/sign_in.dart index ea803529..88fb9a39 100644 --- a/packages/wyatt_authentication_bloc/lib/src/features/sign_in/sign_in.dart +++ b/packages/wyatt_authentication_bloc/lib/src/features/sign_in/sign_in.dart @@ -1,17 +1,18 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . export 'cubit/sign_in_cubit.dart'; +export 'listener/sign_in_listener.dart'; diff --git a/packages/wyatt_authentication_bloc/lib/src/features/sign_up/listener/sign_up_listener.dart b/packages/wyatt_authentication_bloc/lib/src/features/sign_up/listener/sign_up_listener.dart new file mode 100644 index 00000000..925f0294 --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/features/sign_up/listener/sign_up_listener.dart @@ -0,0 +1,66 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wyatt_authentication_bloc/src/features/sign_up/cubit/sign_up_cubit.dart'; +import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; + +class SignUpListener extends StatelessWidget { + const SignUpListener({ + required this.child, + this.onProgress, + this.onSuccess, + this.onError, + this.customBuilder, + super.key, + }); + + final void Function(BuildContext context)? onProgress; + final void Function(BuildContext context)? onSuccess; + final void Function( + BuildContext context, + FormStatus status, + String? errorMessage, + )? onError; + final void Function(BuildContext context, SignUpState state)? customBuilder; + final Widget child; + + @override + Widget build(BuildContext context) => + BlocListener, SignUpState>( + listener: (context, state) { + if (customBuilder != null) { + return customBuilder!(context, state); + } + + if (onSuccess != null && + state.status == FormStatus.submissionSuccess) { + return onSuccess!(context); + } + if (onProgress != null && + state.status == FormStatus.submissionInProgress) { + return onProgress!(context); + } + if (onError != null && + (state.status == FormStatus.submissionCanceled || + state.status == FormStatus.submissionFailure)) { + return onError!(context, state.status, state.errorMessage); + } + }, + child: child, + ); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/features/sign_up/sign_up.dart b/packages/wyatt_authentication_bloc/lib/src/features/sign_up/sign_up.dart index d03bdb75..0eb01f0e 100644 --- a/packages/wyatt_authentication_bloc/lib/src/features/sign_up/sign_up.dart +++ b/packages/wyatt_authentication_bloc/lib/src/features/sign_up/sign_up.dart @@ -1,17 +1,18 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . export 'cubit/sign_up_cubit.dart'; +export 'listener/sign_up_listener.dart';