Compare commits

..
Author SHA1 Message Date
hugo 291c195ceb chore(release): publish packages
- wyatt_authentication_bloc@0.3.0
2022-11-11 19:01:49 -05:00
hugo 1896497c1a refactor(auth): use correct deps 2022-11-11 19:01:00 -05:00
hugo 87254ef547 feat(auth): add fully functionnal mock data source 2022-11-11 18:48:30 -05:00
hugo da34acd35b fix(auth): return exception and not throwing it anymore 2022-11-11 18:47:59 -05:00
hugo d9d0625c67 fix(auth): exception returns msg as message 2022-11-11 18:47:16 -05:00
hugo 08f789725b feat(auth): add toString and equality on entities and models 2022-11-11 18:46:37 -05:00
hugo 33ac5c6280 doc(auth): update pubspec and readme 2022-11-11 16:54:58 -05:00
hugo 762b9bcd11 refactor(auth): update example with new listeners 2022-11-11 16:54:40 -05:00
hugo 38480d84f4 test(auth): add tests for all features 2022-11-11 16:54:08 -05:00
hugo 03a51b97ad feat(auth)!: add email verification, password reset support 2022-11-11 16:53:36 -05:00
hugo 1dd49fa080 chore(release): publish packages
- wyatt_architecture@0.0.2
 - wyatt_authentication_bloc@0.2.1+6
 - wyatt_form_bloc@0.1.0+1
2022-11-10 22:59:51 -05:00
hugo 0219e3d557 doc(arch): update readme 2022-11-10 22:58:35 -05:00
hugo 3de7d01173 feat(arch): add new typedef for stream and simple result 2022-11-10 22:58:19 -05:00
hugo 4183da6259 feat(arch): make base class const 2022-11-10 22:57:51 -05:00
hugo 1f24a76717 chore(form): add hosted dependecy 2022-11-10 13:51:37 -05:00
58 changed files with 2965 additions and 695 deletions
+59
View File
@@ -3,6 +3,65 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2022-11-11
### Changes
---
Packages with breaking changes:
- [`wyatt_authentication_bloc` - `v0.3.0`](#wyatt_authentication_bloc---v030)
Packages with other changes:
- There are no other changes in this release.
---
#### `wyatt_authentication_bloc` - `v0.3.0`
- **REFACTOR**: use correct deps.
- **REFACTOR**: update example with new listeners.
- **FIX**: return exception and not throwing it anymore.
- **FIX**: exception returns msg as message.
- **FEAT**: add fully functionnal mock data source.
- **FEAT**: add toString and equality on entities and models.
- **BREAKING** **FEAT**: add email verification, password reset support.
## 2022-11-10
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_architecture` - `v0.0.2`](#wyatt_architecture---v002)
- [`wyatt_authentication_bloc` - `v0.2.1+6`](#wyatt_authentication_bloc---v0216)
- [`wyatt_form_bloc` - `v0.1.0+1`](#wyatt_form_bloc---v0101)
Packages graduated to a stable release (see pre-releases prior to the stable version for changelog entries):
- `wyatt_architecture` - `v0.0.2`
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `wyatt_authentication_bloc` - `v0.2.1+6`
- `wyatt_form_bloc` - `v0.1.0+1`
---
#### `wyatt_architecture` - `v0.0.2`
## 2022-11-10 ## 2022-11-10
### Changes ### Changes
+4
View File
@@ -1,3 +1,7 @@
## 0.0.2
- Graduate package to a stable release. See pre-releases prior to this version for changelog entries.
## 0.0.2-dev.0 ## 0.0.2-dev.0
- **FEAT**: add exceptions, datasources, repositories, and usecases. - **FEAT**: add exceptions, datasources, repositories, and usecases.
+142 -14
View File
@@ -7,7 +7,7 @@
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* any later version. * any later version.
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
@@ -16,31 +16,159 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
# Flutter - Wyatt Architecture # Flutter - Wyatt Architecture
<p align="left"> <p align="left">
<a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis"> <a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis">
<img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" /> <img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" />
</a> </a>
<img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" /> <img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
</p> </p>
Architecture for Flutter. The Wyatt Architecture for Flutter.
Following: <https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html>
## Features ## Features
- Usecase * Usecase
- Repository * Repository
- DataSource * DataSource
- Entity * Entity
## Getting started
<!-- TODO -->
## Usage ## Usage
<!-- TODO --> ### Domain
Create your entities by extending `Entity` :
```dart
class Photo extends Entity {
final int id;
final String url;
const Photo({
required this.id,
required this.url,
});
}
```
Then create the data sources by extending `BaseLocalDataSource` or `BaseRemoteDataSource` depending the type of data source.
```dart
abstract class PhotoRemoteDataSource extends BaseRemoteDataSource {
Future<Photo> getPhoto(int id);
Future<List<Photo>> getAllPhotos({int? start, int? limit});
}
```
Then you can create your repositories by extenting `BaseRepository` :
```dart
abstract class PhotoRepository extends BaseRepository {
FutureResult<Photo> getPhoto(int id);
FutureResult<List<Photo>> getAllPhotos({int? start, int? limit});
}
```
> Here the repository is just a proxy of the data sources with result type (to have beautiful error handling).
And finaly create your different usecases by using `UseCase<Parameters, ReturnType>` :
```dart
class RetrieveAllPhoto extends UseCase<QueryParameters, List<Photo>> {
final PhotoRepository _photoRepository;
RetrieveAllPhotos(this._photoRepository);
@override
FutureResult<List<Photo>> call(QueryParameters params) {
final photos = _photoRepository.getAllPhotos(
start: params.start,
limit: params.limit,
);
return photos;
}
}
```
> In fact, here we need a new parameter object, so let's create it:
```dart
class QueryParameters {
final int? start;
final int? limit;
QueryParameters(this.start, this.limit);
}
```
### Data
We start by creating models for photos and list of photos. You can use `freezed`. The `PhotoModel` extends `Photo` with some de/serializer capabilities. And those are used only in data layer.
Then implements your data sources:
```dart
class PhotoApiDataSourceImpl extends PhotoRemoteDataSource {
final MiddlewareClient _client;
PhotoApiDataSourceImpl(this._client);
@override
Future<Photo> getPhoto(int id) async {
final response = await _client.get(Uri.parse('/photos/$id'));
final photo =
PhotoModel.fromJson(jsonDecode(response.body) as Map<String, Object?>);
return photo;
}
@override
Future<List<Photo>> getAllPhotos({int? start, int? limit}) async {
final startQuery = start.isNotNull ? '_start=$start' : '';
final limitQuery = limit.isNotNull ? '_limit=$limit' : '';
final delimiter1 =
(startQuery.isNotEmpty || limitQuery.isNotEmpty) ? '?' : '';
final delimiter2 =
(startQuery.isNotEmpty && limitQuery.isNotEmpty) ? '&' : '';
final url = '/photos$delimiter1$startQuery$delimiter2$limitQuery';
final response = await _client.get(Uri.parse(url));
final photos =
ListPhotoModel.fromJson({'photos': jsonDecode(response.body)});
return photos.photos;
}
}
```
> 1: Note that here we use `MiddlewareClient` from our http package.
> 2: You can create multiple implementations (one real and one mock for example).
And implement the repositories:
```dart
class PhotoRepositoryImpl extends PhotoRepository {
final PhotoRemoteDataSource _photoRemoteDataSource;
PhotoRepositoryImpl(
this._photoRemoteDataSource,
);
@override
FutureResult<Photo> getPhoto(int id) => Result.tryCatchAsync(
() => _photoRemoteDataSource.getPhoto(id),
(error) => ServerException('Cannot retrieve photo $id.'),
);
@override
FutureResult<List<Photo>> getAllPhotos({int? start, int? limit}) async =>
Result.tryCatchAsync(
() => _photoRemoteDataSource.getAllPhotos(start: start, limit: limit),
(error) => ServerException('Cannot retrieve all photos.'),
);
}
```
That's all.
@@ -14,4 +14,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
abstract class BaseDataSource {} abstract class BaseDataSource {
const BaseDataSource();
}
@@ -16,4 +16,6 @@
import 'package:wyatt_architecture/src/domain/data_sources/base_data_source.dart'; import 'package:wyatt_architecture/src/domain/data_sources/base_data_source.dart';
abstract class BaseLocalDataSource extends BaseDataSource {} abstract class BaseLocalDataSource extends BaseDataSource {
const BaseLocalDataSource();
}
@@ -16,4 +16,6 @@
import 'package:wyatt_architecture/src/domain/data_sources/base_data_source.dart'; import 'package:wyatt_architecture/src/domain/data_sources/base_data_source.dart';
abstract class BaseRemoteDataSource extends BaseDataSource {} abstract class BaseRemoteDataSource extends BaseDataSource {
const BaseRemoteDataSource();
}
@@ -14,4 +14,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
class BaseRepository {} abstract class BaseRepository {
const BaseRepository();
}
@@ -18,6 +18,8 @@ import 'package:wyatt_architecture/src/core/exceptions/exceptions.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart'; import 'package:wyatt_type_utils/wyatt_type_utils.dart';
typedef FutureResult<T> = Future<Result<T, AppException>>; typedef FutureResult<T> = Future<Result<T, AppException>>;
typedef StreamResult<T> = Stream<Result<T, AppException>>;
typedef Res<T> = Result<T, AppException>;
// ignore: one_member_abstracts // ignore: one_member_abstracts
abstract class UseCase<Parameters, ReturnType> { abstract class UseCase<Parameters, ReturnType> {
@@ -14,4 +14,5 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'no_param.dart';
export 'usecase.dart'; export 'usecase.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
name: wyatt_architecture name: wyatt_architecture
description: A new Wyatt package description: A new Wyatt package
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_architecture repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_architecture
version: 0.0.2-dev.0 version: 0.0.2
publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub
@@ -14,4 +14,4 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
// TODO(wyatt): Add some tests // Nothing to test as there is no logic in this package.
@@ -1,3 +1,19 @@
## 0.3.0
> Note: This release has breaking changes.
- **REFACTOR**: use correct deps.
- **REFACTOR**: update example with new listeners.
- **FIX**: return exception and not throwing it anymore.
- **FIX**: exception returns msg as message.
- **FEAT**: add fully functionnal mock data source.
- **FEAT**: add toString and equality on entities and models.
- **BREAKING** **FEAT**: add email verification, password reset support.
## 0.2.1+6
- Update a dependency to the latest release.
## 0.2.1+5 ## 0.2.1+5
- Update a dependency to the latest release. - Update a dependency to the latest release.
+20 -258
View File
@@ -29,43 +29,28 @@ Authentication Bloc for Flutter.
## Features ## Features
- UserInterface - Wyatt Architecture
* UserFirebase : FirebaseAuth user implementation - Entities:
- AuthenticationRepositoryInterface - Account : AccountModel -> Contains account information from provider
* AuthenticationRepositoryFirebase : FirebaseAuth implementation - AccountWrapper : AccountWrapperModel -> Contains account and extra data.
- ExceptionsInterface - Data Sources:
* ExceptionsFirebase : FirebaseAuth Exception parsing implementation - Local:
- AuthenticationBloc - Cached Authentication Data : AuthenticationCacheDataSourceImpl -> Provides a cache implementation
* Tracks every user changes - Remote:
- Right after the listener has been registered. - Remote Authentication Data : AuthenticationFirebaseDataSourceImpl -> Provides a proxy to FirebaseAuth
- When a user is signed in. - Repositories:
- When the current user is signed out. - AuthenticationRepository : AuthenticationRepositoryImpl -> Provides all authentication methods
- When there is a change in the current user's token. - Features:
- On `refresh()` - Authentication:
* Start/Stop listening on demand - AuthenticationBuilder : widget to build reactive view from authentication state
- `start()` to listen to user changes - AuthenticationCubit : tracks every auth changes, have sign out capability.
- `stop()` to cancel listener - SignUp:
- SignUpCubit - SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign up
* Handles email/password validation and password confirmation - SignIn:
* Handles register with email/password - SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign in
* Handles custom form fields thanks `wyatt_form_bloc`
- Use `entries` to pass a `FormData` object
- You can use several pre configured `FormInput` for validation
- You can use `updateFormData()` to change FormData and validators during runtime (intersection, union, difference or replace)
- SignInCubit
* Handles email/password validation
* Handles login with email/password
- EmailVerificationCubit
* Handles send email verification process
* Handles email verification check
- PasswordResetCubit
* Handles send password reset email process
- Builders
* AuthenticationBuilder to build widgets on user state changes
- Consistent - Consistent
* Every class have same naming convention * Every class have same naming convention
- Tested - Tested
* Partially tested with *bloc_test*
## Getting started ## Getting started
@@ -77,227 +62,4 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
## Usage ## Usage
Create an authentication repository: // TODO
```dart
final AuthenticationRepositoryInterface _authenticationRepository = AuthenticationRepositoryFirebase();
```
Create an authentication cubit:
```dart
final AuthenticationCubit _authenticationCubit = AuthenticationCubit(
authenticationRepository: _authenticationRepository,
);
```
Create a sign up cubit:
```dart
final SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
);
```
You can use `AuthenticationBloc` to route your app.
```dart
return MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthenticationRepositoryInterface>(
create: (context) => _authenticationRepository,
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthenticationCubit>(
create: (context) => _authenticationCubit..init(),
),
BlocProvider<SignUpCubit>(
create: (context) => _signUpCubit,
),
],
child: const AppView(),
),
);
```
> Don't forget to call `init()` on authentication cubit.
And in `AppView` use an `AuthenticationBuilder`:
```dart
AuthenticationBuilder(
unknown: (context) => const LoadingPage(),
unauthenticated: (context) => const LoginPage(),
authenticated: (context, user, userData) => const HomePage(),
)
```
To create a `SignInCubit` you'll need the same `AuthenticationRepository`, you can use the `context`:
```dart
BlocProvider(
create: (_) => SignInCubit(context.read<AuthenticationRepositoryInterface>()),
child: const LoginForm(),
),
```
> In practice it's better to create it in the main `MultiBlocProvider` because the LoginPage can be destroyed, and cubit closed, before login flow ends
## Recipes
### Password confirmation
In this recipe we'll se how to create a custom `FormEntry` to confirm password.
First, create an entry at the SignUpCubit creation:
```dart
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
entries: const FormData([
FormEntry('form_field_confirmPassword', ConfirmedPassword.pure()),
]),
);
```
Then, in the sign up form, create an input for password confirmation:
- `ConfirmedPassword` validator need password value and confirm password value to compare.
```dart
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (confirmPassword) => context
.read<SignUpCubit>()
.dataChanged(
'form_field_confirmPassword',
ConfirmedPassword.dirty(
password: context.read<SignUpCubit>().state.password.value,
value: confirmPassword,
),
),
obscureText: true,
decoration: InputDecoration(
labelText: 'confirm password',
errorText: state.data!.input('form_field_confirmPassword').invalid
? 'passwords do not match'
: null,
),
);
},
);
```
> `form_field_confirmPassword` is the field identifier used in all application to retrieve data. You can use a constant to avoid typos.
You'll need to update password input to update confirm state on password update !
```dart
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<SignUpCubit>().passwordChanged(password);
context.read<SignUpCubit>().dataChanged(
'form_field_confirmPassword',
ConfirmedPassword.dirty(
password: password,
value: context
.read<SignUpCubit>()
.state
.data!
.input('form_field_confirmPassword')
.value,
),
);
},
obscureText: true,
decoration: InputDecoration(
labelText: 'password',
errorText: state.password.invalid ? 'invalid password' : null,
),
);
},
);
```
> Here you call standard `passwordChanged()` AND `dataChanged()`.
And voilà !
### Create Firestore Document on Sign Up
In this recipe we'll se how to create a Firestore Document on sign up success.
First create a callback function:
```dart
Future<void> onSignUpSuccess(SignUpState state, String? uid) async {
if (uid != null) {
final user = {
'uid': uid,
'email': state.email.value,
...state.data.toMap(),
};
await FirebaseFirestore.instance.collection('users').doc(uid).set(user);
}
}
```
Then create SignUpCubit with custom entries and register callback:
```dart
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
entries: const FormData([
FormEntry('form_field_name', Name.pure(), fieldName: 'name'),
FormEntry('form_field_phone', Phone.pure(), fieldName: 'phone'),
FormEntry('form_field_confirmPassword', ConfirmedPassword.pure(), export: false),
]),
onSignUpSuccess: onSignUpSuccess,
);
```
> Use `fieldName` and `export` to control `.toMap()` result on FormData ! Useful to disable exportation of sensible data like passwords.
Create widgets for each inputs:
```dart
class _PhoneInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return TextField(
onChanged: (phone) => context
.read<SignUpCubit>()
.dataChanged('form_field_phone', Phone.dirty(phone)),
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'phone',
helperText: '',
errorText: state.data!.input('form_field_phone').invalid
? 'invalid phone'
: null,
),
);
},
);
}
}
```
> Create widgets for Name and ConfirmedPassword too.
Then add a sign up button with:
```dart
context.read<SignUpCubit>().signUpFormSubmitted()
```
And voilà, a document with `uid` as id, and fields `email`, `name`, `phone`, `uid` will be create in `users` collection.
@@ -38,7 +38,7 @@ dependencies:
wyatt_form_bloc: wyatt_form_bloc:
git: git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.1.0 ref: wyatt_form_bloc-v0.1.0+1
path: packages/wyatt_form_bloc path: packages/wyatt_form_bloc
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
@@ -16,6 +16,7 @@
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
final getIt = GetIt.I; final getIt = GetIt.I;
@@ -23,12 +24,33 @@ abstract class GetItInitializer {
static Future<void> init() async { static Future<void> init() async {
getIt getIt
..registerLazySingleton<AuthenticationRemoteDataSource>( ..registerLazySingleton<AuthenticationRemoteDataSource>(
() => AuthenticationFirebaseDataSourceImpl(), () => AuthenticationMockDataSourceImpl(registeredAccounts: [
Pair(
AccountModel(
uid: '1',
emailVerified: true,
isAnonymous: false,
providerId: 'wyatt',
email: 'toto@test.fr',
),
'toto1234',
),
Pair(
AccountModel(
uid: '2',
emailVerified: false,
isAnonymous: false,
providerId: 'wyatt',
email: 'tata@test.fr',
),
'tata1234',
),
]),
) )
..registerLazySingleton<AuthenticationCacheDataSource<int>>( ..registerLazySingleton<AuthenticationCacheDataSource<int>>(
() => AuthenticationCacheDataSourceImpl<int>(), () => AuthenticationCacheDataSourceImpl<int>(),
); );
await getIt.allReady(); await getIt.allReady();
} }
} }
@@ -3,12 +3,11 @@
// ----- // -----
// File: sign_in_form.dart // File: sign_in_form.dart
// Created Date: 19/08/2022 15:24:37 // Created Date: 19/08/2022 15:24:37
// Last Modified: Thu Nov 10 2022 // Last Modified: Fri Nov 11 2022
// ----- // -----
// Copyright (c) 2022 // Copyright (c) 2022
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
@@ -73,16 +72,12 @@ class SignInForm extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<SignInCubit<int>, SignInState>( return SignInListener<int>(
listener: (context, state) { onError: (context, status, errorMessage) => ScaffoldMessenger.of(context)
if (state.status.isSubmissionFailure) { ..hideCurrentSnackBar()
ScaffoldMessenger.of(context) ..showSnackBar(
..hideCurrentSnackBar() SnackBar(content: Text(errorMessage ?? 'Sign In Failure')),
..showSnackBar( ),
SnackBar(content: Text(state.errorMessage ?? 'Sign In Failure')),
);
}
},
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@@ -3,13 +3,12 @@
// ----- // -----
// File: sign_up_form.dart // File: sign_up_form.dart
// Created Date: 19/08/2022 14:41:08 // Created Date: 19/08/2022 14:41:08
// Last Modified: Thu Nov 10 2022 // Last Modified: Fri Nov 11 2022
// ----- // -----
// Copyright (c) 2022 // Copyright (c) 2022
import 'package:example_router/core/constants/form_field.dart'; import 'package:example_router/core/constants/form_field.dart';
import 'package:flutter/material.dart' hide FormField; import 'package:flutter/material.dart' hide FormField;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
@@ -110,29 +109,25 @@ class SignUpForm extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<SignUpCubit<int>, SignUpState>( return SignUpListener<int>(
listener: (context, state) { onError: (context, status, errorMessage) =>
if (state.status.isSubmissionFailure) { ScaffoldMessenger.of(context)
ScaffoldMessenger.of(context) ..hideCurrentSnackBar()
..hideCurrentSnackBar() ..showSnackBar(
..showSnackBar( SnackBar(content: Text(errorMessage ?? 'Sign Up Failure')),
SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')), ),
); child: SingleChildScrollView(
} child: Column(
}, children: [
child: SingleChildScrollView( _EmailInput(),
child: Column( const SizedBox(height: 8),
children: [ _PasswordInput(),
_EmailInput(), const SizedBox(height: 8),
const SizedBox(height: 8), _ConfirmPasswordInput(),
_PasswordInput(), const SizedBox(height: 16),
const SizedBox(height: 8), _SignUpButton(),
_ConfirmPasswordInput(), ],
const SizedBox(height: 16), ),
_SignUpButton(), ));
],
),
),
);
} }
} }
@@ -33,25 +33,26 @@ dependencies:
go_router: ^5.1.5 go_router: ^5.1.5
firebase_core: ^2.1.1 firebase_core: ^2.1.1
flutter_bloc: ^8.1.1 flutter_bloc: ^8.1.1
get_it: ^7.2.0
wyatt_authentication_bloc: wyatt_authentication_bloc:
path: "../" path: "../"
wyatt_form_bloc: wyatt_form_bloc:
git: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages version: 0.1.0+1
ref: wyatt_form_bloc-v0.1.0
path: packages/wyatt_form_bloc wyatt_architecture:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.0.2
wyatt_type_utils: wyatt_type_utils:
git: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages version: 0.0.3+1
ref: wyatt_type_utils-v0.0.3+1
path: packages/wyatt_type_utils
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
get_it: ^7.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -17,4 +17,5 @@
abstract class AuthFormName { abstract class AuthFormName {
static const String signUpForm = 'wyattSignUpForm'; static const String signUpForm = 'wyattSignUpForm';
static const String signInForm = 'wyattSignInForm'; static const String signInForm = 'wyattSignInForm';
static const String passwordResetForm = 'wyattPasswordResetForm';
} }
@@ -23,6 +23,9 @@ abstract class AuthenticationFailureInterface extends AppException
String code; String code;
String msg; String msg;
@override
String get message => msg;
AuthenticationFailureInterface(this.code, this.msg); AuthenticationFailureInterface(this.code, this.msg);
AuthenticationFailureInterface.fromCode(this.code) AuthenticationFailureInterface.fromCode(this.code)
: msg = 'An unknown error occurred.'; : msg = 'An unknown error occurred.';
@@ -246,3 +249,10 @@ abstract class SignOutFailureInterface extends AuthenticationFailureInterface {
/// {@macro sign_out_failure} /// {@macro sign_out_failure}
SignOutFailureInterface.fromCode(super.code) : super.fromCode(); SignOutFailureInterface.fromCode(super.code) : super.fromCode();
} }
abstract class GetIdTokenFailureInterface
extends AuthenticationFailureInterface {
GetIdTokenFailureInterface(super.code, super.msg);
GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode();
}
@@ -270,3 +270,10 @@ class SignOutFailureFirebase extends SignOutFailureInterface {
SignOutFailureFirebase.fromCode(super.code) : super.fromCode(); 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();
}
@@ -1,18 +1,19 @@
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// any later version. // any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'local/authentication_cache_data_source_impl.dart'; export 'local/authentication_cache_data_source_impl.dart';
export 'remote/authentication_firebase_data_source_impl.dart'; export 'remote/authentication_firebase_data_source_impl.dart';
export 'remote/authentication_mock_data_source_impl.dart';
@@ -15,7 +15,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:firebase_auth/firebase_auth.dart'; 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_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart'; import 'package:wyatt_type_utils/wyatt_type_utils.dart';
@@ -28,7 +27,18 @@ class AuthenticationFirebaseDataSourceImpl
Account _mapper(User user) => AccountModel( Account _mapper(User user) => AccountModel(
uid: user.uid, 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, email: user.email,
phoneNumber: user.phoneNumber,
photoURL: user.photoURL,
); );
@override @override
@@ -55,6 +65,8 @@ class AuthenticationFirebaseDataSourceImpl
} }
@override @override
/// {@macro signup}
Future<Account> signUp({ Future<Account> signUp({
required String email, required String email,
required String password, required String password,
@@ -95,9 +107,10 @@ class AuthenticationFirebaseDataSourceImpl
} else { } else {
throw Exception(); // Get caught just after. throw Exception(); // Get caught just after.
} }
} on FirebaseAuthException catch (e) {
throw GetIdTokenFailureFirebase.fromCode(e.code);
} catch (_) { } catch (_) {
// TODO(hpcl): implement a non ambiguous exception for this case throw GetIdTokenFailureFirebase();
throw ServerException();
} }
} }
@@ -107,4 +120,83 @@ class AuthenticationFirebaseDataSourceImpl
final Account? account = (user.isNotNull) ? _mapper(user!) : null; final Account? account = (user.isNotNull) ? _mapper(user!) : null;
return account; return account;
}); });
@override
Future<void> 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<void> sendEmailVerification() async {
try {
await _firebaseAuth.currentUser!.sendEmailVerification();
} on FirebaseAuthException catch (e) {
throw SendEmailVerificationFailureFirebase.fromCode(e.code);
} catch (_) {
throw SendEmailVerificationFailureFirebase();
}
}
@override
Future<void> 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<Account> 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<bool> 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<void> refresh() async {
try {
await _firebaseAuth.currentUser!.reload();
} on FirebaseAuthException catch (e) {
throw RefreshFailureFirebase.fromCode(e.code);
} catch (_) {
throw RefreshFailureFirebase();
}
}
} }
@@ -0,0 +1,204 @@
// 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 <https://www.gnu.org/licenses/>.
import 'dart:async';
import 'dart:math';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
Pair<Account, String>? _connectedMock;
Pair<Account, String>? _registeredMock;
final StreamController<Account?> _streamAccount = StreamController();
final List<Pair<Account, String>>? registeredAccounts;
final String idToken;
AuthenticationMockDataSourceImpl({
this.idToken = 'fake-id-token',
this.registeredAccounts,
});
Future<void> _randomDelay() async {
await Future<void>.delayed(
Duration(milliseconds: Random().nextInt(400) + 200),
);
return;
}
@override
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
}) async {
await _randomDelay();
}
@override
Future<String> getIdentityToken() async {
await _randomDelay();
return idToken;
}
@override
Future<void> refresh() async {
await _randomDelay();
if (_connectedMock.isNull) {
throw RefreshFailureFirebase();
}
final refresh = DateTime.now();
final mock = (_connectedMock?.left as AccountModel?)
?.copyWith(lastSignInTime: refresh);
_connectedMock = _connectedMock?.copyWith(left: mock);
_streamAccount.add(mock);
}
@override
Future<void> sendEmailVerification() async {
await _randomDelay();
if (_connectedMock.isNotNull) {
final refresh = DateTime.now();
final mock = (_connectedMock?.left as AccountModel?)?.copyWith(
emailVerified: false,
lastSignInTime: refresh,
);
_streamAccount.add(mock);
_connectedMock = _connectedMock?.copyWith(left: mock);
return;
}
throw SendEmailVerificationFailureFirebase();
}
@override
Future<void> sendPasswordResetEmail({required String email}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
return;
}
}
if (_registeredMock.isNotNull) {
if (_registeredMock?.left?.email != email) {
throw SendPasswordResetEmailFailureFirebase();
}
return;
}
throw SendPasswordResetEmailFailureFirebase();
}
@override
Future<Account> signInAnonymously() async {
await _randomDelay();
final creation = DateTime.now();
final mock = AccountModel(
uid: 'mock-id-anom',
emailVerified: false,
isAnonymous: true,
providerId: 'wyatt',
creationTime: creation,
lastSignInTime: creation,
);
_streamAccount.add(mock);
_connectedMock = _connectedMock?.copyWith(left: mock);
return Future.value(mock);
}
@override
Future<Account> signInWithEmailAndPassword({
required String email,
required String password,
}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
final account = accounts?.first;
if (account?.right != password) {
throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
}
_streamAccount.add(account!.left);
_connectedMock = account.copyWith();
return account.left!;
}
}
if (_registeredMock.isNotNull) {
if (_registeredMock?.left?.email != email) {
throw SignInWithCredentialFailureFirebase.fromCode('user-not-found');
}
if (_registeredMock?.right != password) {
throw SignInWithCredentialFailureFirebase.fromCode('wrong-password');
}
_streamAccount.add(_registeredMock!.left);
_connectedMock = _registeredMock!.copyWith();
return _registeredMock!.left!;
}
throw SignInWithCredentialFailureFirebase();
}
@override
Future<void> signOut() async {
_connectedMock = null;
_streamAccount.add(null);
}
@override
Future<Account> signUp({
required String email,
required String password,
}) async {
await _randomDelay();
if (registeredAccounts.isNotNull) {
final accounts =
registeredAccounts?.where((pair) => pair.left?.email == email);
if (accounts.isNotNullOrEmpty) {
throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
'email-already-in-use',
);
}
}
if (_registeredMock?.left?.email == email) {
throw SignUpWithEmailAndPasswordFailureFirebase.fromCode(
'email-already-in-use',
);
}
final creation = DateTime.now();
final mock = AccountModel(
uid: 'mock-id-email',
emailVerified: false,
isAnonymous: false,
providerId: 'wyatt',
email: email,
creationTime: creation,
lastSignInTime: creation,
);
_streamAccount.add(mock);
_registeredMock = Pair(mock, password);
return Future.value(mock);
}
@override
Stream<Account?> streamAccount() => _streamAccount.stream.asBroadcastStream();
@override
Future<bool> verifyPasswordResetCode({required String code}) async {
await _randomDelay();
return true;
}
}
@@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
@@ -16,12 +17,71 @@
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart'; import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
class AccountModel implements Account { class AccountModel extends Account {
@override @override
final String uid; final String uid;
@override @override
final String? email; 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,
});
AccountModel copyWith({
String? uid,
String? email,
DateTime? creationTime,
bool? emailVerified,
bool? isAnonymous,
bool? isNewUser,
DateTime? lastSignInTime,
String? phoneNumber,
String? photoURL,
String? providerId,
}) => AccountModel(
uid: uid ?? this.uid,
email: email ?? this.email,
creationTime: creationTime ?? this.creationTime,
emailVerified: emailVerified ?? this.emailVerified,
isAnonymous: isAnonymous ?? this.isAnonymous,
isNewUser: isNewUser ?? this.isNewUser,
lastSignInTime: lastSignInTime ?? this.lastSignInTime,
phoneNumber: phoneNumber ?? this.phoneNumber,
photoURL: photoURL ?? this.photoURL,
providerId: providerId ?? this.providerId,
);
} }
@@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
@@ -24,4 +25,12 @@ class AccountWrapperModel<T> extends AccountWrapper<T> {
final T? data; final T? data;
AccountWrapperModel(this.account, this.data); AccountWrapperModel(this.account, this.data);
AccountWrapperModel<T> copyWith({
Account? account,
T? data,
}) => AccountWrapperModel<T>(
account ?? this.account,
data ?? this.data,
);
} }
@@ -132,7 +132,7 @@ class AuthenticationRepositoryImpl<T extends Object>
); );
await dataResult.foldAsync( await dataResult.foldAsync(
_authenticationLocalDataSource.storeData, _authenticationLocalDataSource.storeData,
(error) => throw error, (error) async => error,
); );
} }
return account; return account;
@@ -208,4 +208,70 @@ class AuthenticationRepositoryImpl<T extends Object>
AccountWrapperModel<T>(account, null), AccountWrapperModel<T>(account, null),
); );
}); });
@override
FutureResult<void> confirmPasswordReset({
required String code,
required String newPassword,
}) =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.confirmPasswordReset(
code: code,
newPassword: newPassword,
);
},
(error) => error,
);
@override
FutureResult<void> sendEmailVerification() =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.sendEmailVerification();
},
(error) => error,
);
@override
FutureResult<void> sendPasswordResetEmail({required String email}) =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.sendPasswordResetEmail(
email: email,
);
},
(error) => error,
);
@override
FutureResult<Account> signInAnonymously() =>
Result.tryCatchAsync<Account, AppException, AppException>(
() async {
final account =
await _authenticationRemoteDataSource.signInAnonymously();
return account;
},
(error) => error,
);
@override
FutureResult<bool> verifyPasswordResetCode({required String code}) =>
Result.tryCatchAsync<bool, AppException, AppException>(
() async {
final response = await _authenticationRemoteDataSource
.verifyPasswordResetCode(code: code);
return response;
},
(error) => error,
);
@override
FutureResult<void> refresh() =>
Result.tryCatchAsync<void, AppException, AppException>(
() async {
await _authenticationRemoteDataSource.refresh();
},
(error) => error,
);
} }
@@ -30,7 +30,22 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource {
Future<void> signOut(); Future<void> signOut();
Future<void> refresh();
Stream<Account?> streamAccount(); Stream<Account?> streamAccount();
Future<String> getIdentityToken(); Future<String> getIdentityToken();
Future<void> sendEmailVerification();
Future<void> sendPasswordResetEmail({required String email});
Future<void> confirmPasswordReset({
required String code,
required String newPassword,
});
Future<bool> verifyPasswordResetCode({required String code});
Future<Account> signInAnonymously();
} }
@@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:equatable/equatable.dart';
import 'package:wyatt_architecture/wyatt_architecture.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. /// The user's unique ID.
String get uid; String get uid;
@@ -24,4 +25,59 @@ abstract class Account extends Entity {
/// ///
/// Will be `null` if signing in anonymously. /// Will be `null` if signing in anonymously.
String? get email; 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<Object?> get props => [
uid,
email,
emailVerified,
isAnonymous,
creationTime,
lastSignInTime,
phoneNumber,
photoURL,
providerId,
isNewUser,
];
@override
String toString() => 'AccountModel(uid: $uid, email: $email, '
'creationTime: $creationTime, emailVerified: $emailVerified, '
'isAnonymous: $isAnonymous, isNewUser: $isNewUser, lastSignInTime: '
'$lastSignInTime, phoneNumber: $phoneNumber, photoURL: $photoURL, '
'providerId: $providerId)';
} }
@@ -14,10 +14,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:equatable/equatable.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart'; import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart'; import 'package:wyatt_authentication_bloc/src/domain/entities/account.dart';
abstract class AccountWrapper<T> extends Entity { abstract class AccountWrapper<T> extends Equatable implements Entity {
Account? get account; Account? get account;
T? get data; T? get data;
@override
List<Object?> get props => [account, data];
@override
String toString() => 'AccountWrapper($account, data: $data)';
} }
@@ -22,18 +22,83 @@ import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
abstract class AuthenticationRepository<T> extends BaseRepository { abstract class AuthenticationRepository<T> extends BaseRepository {
FormRepository get formRepository; 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<Account> signUp({ FutureResult<Account> signUp({
required String email, required String email,
required String password, required String password,
}); });
/// {@template send_email_verification}
/// Sends verification email to the account email.
///
/// Throws a SendEmailVerificationFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<void> sendEmailVerification();
/// {@template send_password_reset_email}
/// Sends a password reset email to the provided [email].
///
/// Throws a SendPasswordResetEmailFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<void> 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<void> 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<bool> verifyPasswordResetCode({required String code});
/// {@template signin_anom}
/// Sign in anonymously.
///
/// Throws a SignInAnonymouslyFailureInterface if an exception occurs.
/// {@endtemplate}
FutureResult<Account> signInAnonymously();
/// {@template signin_pwd}
/// Signs in with the provided [email] and [password].
///
/// Throws a SignInWithEmailAndPasswordFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureResult<Account> signInWithEmailAndPassword({ FutureResult<Account> signInWithEmailAndPassword({
required String email, required String email,
required String password, required String password,
}); });
/// {@template signout}
/// Signs out the current user.
/// It also clears the cache and the associated data.
/// {@endtemplate}
FutureResult<void> signOut(); FutureResult<void> signOut();
FutureResult<void> 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<FutureResult<AccountWrapper<T>>> streamAccount(); Stream<FutureResult<AccountWrapper<T>>> streamAccount();
FutureResult<String> getIdentityToken(); FutureResult<String> getIdentityToken();
@@ -40,16 +40,16 @@ class AuthenticationCubit<Extra> extends Cubit<AuthenticationState<Extra>> {
accountFutureResult.fold( accountFutureResult.fold(
(value) { (value) {
if (value.account.isNotNull) { if (value.account.isNotNull) {
emit(AuthenticationState.authenticated(value)); emit(AuthenticationState<Extra>.authenticated(value));
return; return;
} }
_authenticationRepository.destroyCache(); _authenticationRepository.destroyCache();
emit(const AuthenticationState.unauthenticated()); emit(AuthenticationState<Extra>.unauthenticated());
return; return;
}, },
(error) { (error) {
_authenticationRepository.destroyCache(); _authenticationRepository.destroyCache();
emit(const AuthenticationState.unauthenticated()); emit(AuthenticationState<Extra>.unauthenticated());
return; return;
}, },
); );
@@ -35,7 +35,7 @@ class AuthenticationState<Extra> extends Equatable {
: this._(status: AuthenticationStatus.unauthenticated); : this._(status: AuthenticationStatus.unauthenticated);
@override @override
List<Object?> get props => [status]; List<Object?> get props => [status, accountWrapper];
@override @override
String toString() => String toString() =>
@@ -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 <https://www.gnu.org/licenses/>.
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<Extra> 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<EmailVerificationCubit<Extra>, 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);
},
);
}
@@ -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 <https://www.gnu.org/licenses/>.
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<Extra> extends Cubit<EmailVerificationState> {
final AuthenticationRepository<Extra> _authenticationRepository;
EmailVerificationCubit({
required AuthenticationRepository<Extra> authenticationRepository,
}) : _authenticationRepository = authenticationRepository,
super(const EmailVerificationState());
FutureOr<void> 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<void> 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,
),
),
);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
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<Object?> get props => [status, isVerified, errorMessage];
@override
String toString() => 'EmailVerificationState(status: ${status.name} '
'${(errorMessage != null) ? " [$errorMessage]" : ""}, '
'isVerified: $isVerified)';
}
@@ -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 <https://www.gnu.org/licenses/>.
export 'builder/email_verification_builder.dart';
export 'cubit/email_verification_cubit.dart';
@@ -1,19 +1,21 @@
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// any later version. // any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'authentication/authentication.dart'; export 'authentication/authentication.dart';
export 'email_verification/email_verification.dart';
export 'password_reset/password_reset.dart';
export 'sign_in/sign_in.dart'; export 'sign_in/sign_in.dart';
export 'sign_up/sign_up.dart'; export 'sign_up/sign_up.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 <https://www.gnu.org/licenses/>.
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<Extra> extends FormDataCubit<PasswordResetState> {
final AuthenticationRepository<Extra> _authenticationRepository;
FormRepository get _formRepository =>
_authenticationRepository.formRepository;
PasswordResetCubit({
required AuthenticationRepository<Extra> 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<void> dataChanged<Value>(
String key,
FormInputValidator<Value, ValidationError> 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<void> reset() {
final form = state.form.reset();
_formRepository.updateForm(form);
emit(
state.copyWith(form: form, status: form.validate()),
);
}
@override
FutureOr<void> submit() async {
if (!state.status.isValidated) {
return;
}
emit(state.copyWith(status: FormStatus.submissionInProgress));
final form = _formRepository.accessForm(formName);
final email = form.valueOf<String?>(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<void> 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<void> validate() {
emit(
state.copyWith(
status: _formRepository.accessForm(formName).validate(),
),
);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
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<Object> get props => [email, status];
@override
String toString() => 'PasswordResetState(status: ${status.name} '
'${(errorMessage != null) ? " [$errorMessage]" : ""}, $form)';
}
@@ -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 <https://www.gnu.org/licenses/>.
export 'cubit/password_reset_cubit.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 <https://www.gnu.org/licenses/>.
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<Extra> 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<SignInCubit<Extra>, 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,
);
}
@@ -1,17 +1,18 @@
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// any later version. // any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'cubit/sign_in_cubit.dart'; export 'cubit/sign_in_cubit.dart';
export '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 <https://www.gnu.org/licenses/>.
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<Extra> 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<SignUpCubit<Extra>, 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,
);
}
@@ -1,17 +1,18 @@
// Copyright (C) 2022 WYATT GROUP // Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details. // Please see the AUTHORS file for details.
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or // the Free Software Foundation, either version 3 of the License, or
// any later version. // any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details. // GNU General Public License for more details.
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'cubit/sign_up_cubit.dart'; export 'cubit/sign_up_cubit.dart';
export 'listener/sign_up_listener.dart';
@@ -1,7 +1,7 @@
name: wyatt_authentication_bloc name: wyatt_authentication_bloc
description: Authentication BLoC for Flutter description: Authentication BLoC for Flutter
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc
version: 0.2.1+5 version: 0.3.0
publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub
@@ -12,7 +12,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
crypto: ^3.0.2 crypto: ^3.0.2
flutter_bloc: ^8.1.1 flutter_bloc: ^8.1.1
equatable: ^2.0.5 equatable: ^2.0.5
@@ -23,28 +23,23 @@ dependencies:
twitter_login: ^4.2.3 twitter_login: ^4.2.3
wyatt_form_bloc: wyatt_form_bloc:
git: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages version: 0.1.0+1
ref: wyatt_form_bloc-v0.1.0
path: packages/wyatt_form_bloc
wyatt_architecture: wyatt_architecture:
git: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages version: 0.0.2
ref: wyatt_architecture-v0.0.2-dev.0
path: packages/wyatt_architecture
wyatt_type_utils: wyatt_type_utils:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.0.3+1 version: 0.0.3+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
bloc_test: ^9.1.0 bloc_test: ^9.1.0
mocktail: ^0.3.0 mocktail: ^0.3.0
wyatt_analysis: wyatt_analysis:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 2.2.2 version: 2.2.2
@@ -17,100 +17,114 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {} implements AuthenticationRepository<int> {}
class MockUser extends Mock implements User {} class MockAccount extends Mock implements Account {}
void main() { void main() {
group('AuthenticationCubit<T>', () { group('AuthenticationCubit<T>', () {
final MockUser user = MockUser(); final MockAccount account = MockAccount();
late AuthenticationRepository authenticationRepository; final AccountWrapper<int> wrapper = AccountWrapperModel(account, 10);
late AuthenticationRepository<int> authenticationRepository;
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository(); authenticationRepository = MockAuthenticationRepository();
when(() => authenticationRepository.user).thenAnswer( when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => const Stream.empty(), (_) => const Stream.empty(),
); );
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.fromIterable([AuthCubitStatus.stoped]),
);
when( when(
() => authenticationRepository.currentUser, () => authenticationRepository.getAccount(),
).thenReturn(user); ).thenAnswer((_) async => Ok(account));
}); });
test('initial auth state is `unknown`', () { test('initial auth state is `unknown`', () {
expect( expect(
AuthenticationCubit<void>( AuthenticationCubit<int>(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
).state, ).state,
const AuthenticationState<Never>.unknown(), const AuthenticationState<Never>.unknown(),
); );
}); });
test('initial cubit status is `stoped`', () async { group('ListenForAuthenticationChanges', () {
expect( blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
await AuthenticationCubit<void>( 'emits authenticated when stream contains account',
authenticationRepository: authenticationRepository,
).status,
AuthCubitStatus.stoped,
);
});
group('UserChanged', () {
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'emits authenticated when user is not empty',
setUp: () { setUp: () {
when(() => user.isNotEmpty).thenReturn(true); when(() => authenticationRepository.streamAccount()).thenAnswer(
when(() => authenticationRepository.user).thenAnswer( (_) => Stream.fromIterable([
(_) => Stream.value(user), Future.value(
Ok(wrapper),
)
]),
); );
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.value(AuthCubitStatus.started),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
)..init(),
seed: () => const AuthenticationState.unknown(),
expect: () => [AuthenticationState<void>.authenticated(user, null)],
);
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'emits unauthenticated when user is empty',
setUp: () {
when(() => user.isEmpty).thenReturn(true);
when(() => user.isNotEmpty).thenReturn(false);
when(() => authenticationRepository.user).thenAnswer(
(_) => Stream.value(user),
);
when(() => authenticationRepository.cubitStatus).thenAnswer(
(_) => Stream.value(AuthCubitStatus.started),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
)..init(),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<Never>.unauthenticated()],
);
});
group('LogoutRequested', () {
blocTest<AuthenticationCubit<void>, AuthenticationState<void>>(
'invokes signOut',
setUp: () {
when(
() => authenticationRepository.signOut(),
).thenAnswer((_) async {});
}, },
build: () => AuthenticationCubit( build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
act: (cubit) => cubit.logOut(), seed: () => const AuthenticationState.unknown(),
expect: () => [AuthenticationState<int>.authenticated(wrapper)],
);
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits unauthenticated when account stream is empty',
setUp: () {
when(
() => authenticationRepository.destroyCache(),
).thenAnswer((_) async => const Ok(null));
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Ok(AccountWrapperModel(null, 1)),
)
]),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<int>.unauthenticated()],
);
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits unauthenticated when there is an error in stream',
setUp: () {
when(
() => authenticationRepository.destroyCache(),
).thenAnswer((_) async => const Ok(null));
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Err(ServerException()),
)
]),
);
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
expect: () => [const AuthenticationState<int>.unauthenticated()],
);
});
group('SignOut', () {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'invokes signOut',
setUp: () {
when(
() => authenticationRepository.signOut(),
).thenAnswer((_) async => const Ok(null));
},
build: () => AuthenticationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.signOut(),
verify: (_) { verify: (_) {
verify(() => authenticationRepository.signOut()).called(1); verify(() => authenticationRepository.signOut()).called(1);
}, },
@@ -18,7 +18,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
class MockUser extends Mock implements User {} class MockAccount extends Mock implements Account {}
void main() { void main() {
group('AuthenticationState', () { group('AuthenticationState', () {
@@ -27,29 +27,33 @@ void main() {
const AuthenticationState<void> state = const AuthenticationState<void> state =
AuthenticationState.unauthenticated(); AuthenticationState.unauthenticated();
expect(state.status, AuthenticationStatus.unauthenticated); expect(state.status, AuthenticationStatus.unauthenticated);
expect(state.user, null); expect(state.accountWrapper, null);
}); });
}); });
group('authenticated', () { group('authenticated', () {
test('has correct status', () { test('has correct status', () {
final MockUser user = MockUser(); final MockAccount account = MockAccount();
final AuthenticationState<void> state = final AuthenticationState<void> state =
AuthenticationState.authenticated(user, null); AuthenticationState.authenticated(
AccountWrapperModel<void>(account, null),
);
expect(state.status, AuthenticationStatus.authenticated); expect(state.status, AuthenticationStatus.authenticated);
expect(state.user, user); expect(state.accountWrapper?.account, account);
}); });
}); });
group('authenticated with extra data', () { group('authenticated with extra data', () {
test('has correct status', () { test('has correct status', () {
final MockUser user = MockUser(); final MockAccount account = MockAccount();
const String extra = 'AwesomeExtraData'; const String extra = 'AwesomeExtraData';
final AuthenticationState<String> state = final AuthenticationState<String> state =
AuthenticationState.authenticated(user, extra); AuthenticationState.authenticated(
AccountWrapperModel(account, extra),
);
expect(state.status, AuthenticationStatus.authenticated); expect(state.status, AuthenticationStatus.authenticated);
expect(state.user, user); expect(state.accountWrapper?.account, account);
expect(state.extra, extra); expect(state.accountWrapper?.data, extra);
}); });
}); });
}); });
@@ -0,0 +1,249 @@
// 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 <https://www.gnu.org/licenses/>.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
group('EmailVerificationCubit<T>', () {
late MockAccount account;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
when(
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
account = MockAccount();
when(
() => account.emailVerified,
).thenAnswer((_) => true);
});
test('initial state is `false`', () {
expect(
EmailVerificationCubit<int>(
authenticationRepository: authenticationRepository,
).state,
const EmailVerificationState(),
);
});
group('SendVerificationEmail', () {
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes sendEmailVerification,',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.sendEmailVerification(),
verify: (_) {
verify(() => authenticationRepository.sendEmailVerification())
.called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.sendEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure',
setUp: () {
when(() => authenticationRepository.sendEmailVerification())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.sendEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
});
group('CheckEmailVerification', () {
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes refresh,',
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.checkEmailVerification(),
verify: (_) {
verify(() => authenticationRepository.refresh()).called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'invokes emailVerified,',
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.checkEmailVerification(),
verify: (_) {
verify(() => account.emailVerified).called(1);
},
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success with true if verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
isVerified: true,
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits success with false if not verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
status: FormStatus.submissionSuccess,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure on refresh error',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
blocTest<EmailVerificationCubit<int>, EmailVerificationState>(
'emits failure on get account error',
setUp: () {
when(() => authenticationRepository.getAccount())
.thenAnswer((_) async => Err(ServerException('erreur')));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const EmailVerificationState(),
act: (cubit) => cubit.checkEmailVerification(),
expect: () => [
const EmailVerificationState(
status: FormStatus.submissionInProgress,
),
const EmailVerificationState(
errorMessage: 'erreur',
status: FormStatus.submissionFailure,
)
],
);
});
});
}
@@ -0,0 +1,48 @@
// 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 <https://www.gnu.org/licenses/>.
import 'package:flutter_test/flutter_test.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
group('EmailVerificationState', () {
test('supports value comparisons', () {
expect(
const EmailVerificationState(isVerified: true),
const EmailVerificationState(isVerified: true),
);
});
test('returns same object when no properties are passed', () {
expect(
const EmailVerificationState(isVerified: true).copyWith(),
const EmailVerificationState(isVerified: true),
);
});
test('returns object with updated status when status is passed', () {
expect(
const EmailVerificationState(isVerified: true)
.copyWith(status: FormStatus.invalid),
const EmailVerificationState(
isVerified: true,
status: FormStatus.invalid,
),
);
});
});
}
@@ -0,0 +1,335 @@
// 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 <https://www.gnu.org/licenses/>.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock
implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
}
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() {
const String invalidEmailString = 'invalid';
const String validEmailString = 'test@gmail.com';
group('PasswordResetCubit', () {
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.passwordResetForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
formRepository = MockFormRepository();
when(
() => authenticationRepository.sendPasswordResetEmail(
email: any(named: 'email'),
),
).thenAnswer((_) async => const Ok(null));
when(
() => authenticationRepository.formRepository,
).thenAnswer((_) => formRepository);
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer((_) => form);
});
test('initial state is pure', () {
expect(
PasswordResetCubit(
authenticationRepository: authenticationRepository,
).state,
PasswordResetState(form: form),
);
});
group('emailChanged', () {
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [invalid] when email is invalid',
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.invalid,
),
],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [valid] when email is valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
],
);
});
group('submit', () {
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'does nothing when status is not validated',
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.submit(),
expect: () => const <PasswordResetState>[],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'calls sendPasswordResetEmail with correct email',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
verify: (_) {
verify(
() => authenticationRepository.sendPasswordResetEmail(
email: validEmailString,
),
).called(1);
},
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [submissionInProgress, submissionSuccess] '
'when sendPasswordResetEmail succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionInProgress,
),
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionSuccess,
)
],
);
blocTest<PasswordResetCubit<int>, PasswordResetState>(
'emits [submissionInProgress, submissionFailure] '
'when sendPasswordResetEmail fails',
setUp: () {
when(
() => authenticationRepository.sendPasswordResetEmail(
email: any(named: 'email'),
),
).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.passwordResetForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
);
},
build: () => PasswordResetCubit(
authenticationRepository: authenticationRepository,
),
seed: () => PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <PasswordResetState>[
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionInProgress,
),
PasswordResetState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
],
name: AuthFormName.passwordResetForm,
),
status: FormStatus.submissionFailure,
)
],
);
});
});
}
@@ -0,0 +1,56 @@
// 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 <https://www.gnu.org/licenses/>.
import 'package:flutter_test/flutter_test.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() {
final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
],
name: AuthFormName.passwordResetForm,
);
group('PasswordResetState', () {
test('supports value comparisons', () {
expect(
PasswordResetState(
form: form,
),
PasswordResetState(form: form),
);
});
test('returns same object when no properties are passed', () {
expect(
PasswordResetState(form: form).copyWith(),
PasswordResetState(form: form),
);
});
test('returns object with updated status when status is passed', () {
expect(
PasswordResetState(form: form).copyWith(status: FormStatus.invalid),
PasswordResetState(
form: form,
status: FormStatus.invalid,
),
);
});
});
}
@@ -17,46 +17,61 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {} implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
implements AuthenticationCubit<void> {} }
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() { void main() {
const String invalidEmailString = 'invalid'; const String invalidEmailString = 'invalid';
const Email invalidEmail = Email.dirty(invalidEmailString);
const String validEmailString = 'test@gmail.com'; const String validEmailString = 'test@gmail.com';
const Email validEmail = Email.dirty(validEmailString);
const String invalidPasswordString = 'invalid'; const String invalidPasswordString = 'invalid';
const Password invalidPassword = Password.dirty(invalidPasswordString);
const String validPasswordString = 't0pS3cret1234'; const String validPasswordString = 't0pS3cret1234';
const Password validPassword = Password.dirty(validPasswordString);
group('SignInCubit', () { group('SignInCubit', () {
late AuthenticationRepository authenticationRepository; final MockAccount account = MockAccount();
late AuthenticationCubit<void> authenticationCubit; final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository(); authenticationRepository = MockAuthenticationRepository();
authenticationCubit = MockAuthenticationCubit(); formRepository = MockFormRepository();
when( when(
() => authenticationRepository.signInWithEmailAndPassword( () => authenticationRepository.signInWithEmailAndPassword(
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),
).thenAnswer((_) async {}); ).thenAnswer((_) async => Ok(account));
when( when(
() => authenticationCubit.start(), () => authenticationRepository.formRepository,
).thenReturn(true); ).thenAnswer((_) => formRepository);
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer((_) => form);
}); });
test('initial state is SignInState', () { test('initial state is SignInState', () {
@@ -64,33 +79,90 @@ void main() {
SignInCubit( SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
).state, ).state,
const SignInState(), SignInState(form: form),
); );
}); });
group('emailChanged', () { group('emailChanged', () {
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [invalid] when email/password are invalid', 'emits [invalid] when email/password are invalid',
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
act: (cubit) => cubit.emailChanged(invalidEmailString), act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState(email: invalidEmail, status: FormStatus.invalid), SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
], ],
); );
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [valid] when email/password are valid', 'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const SignInState(password: validPassword), seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.emailChanged(validEmailString), act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState( SignInState(
email: validEmail, form: WyattFormImpl(
password: validPassword, [
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid, status: FormStatus.valid,
), ),
], ],
@@ -98,58 +170,145 @@ void main() {
}); });
group('passwordChanged', () { group('passwordChanged', () {
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [invalid] when email/password are invalid', 'emits [invalid] when email/password are invalid',
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
act: (cubit) => cubit.passwordChanged(invalidPasswordString), act: (cubit) => cubit.passwordChanged(invalidPasswordString),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState( SignInState(
password: invalidPassword, form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(invalidPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid, status: FormStatus.invalid,
), ),
], ],
); );
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [valid] when email/password are valid', 'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const SignInState(email: validEmail), seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.invalid,
),
act: (cubit) => cubit.passwordChanged(validPasswordString), act: (cubit) => cubit.passwordChanged(validPasswordString),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState( SignInState(
email: validEmail, form: WyattFormImpl(
password: validPassword, [
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid, status: FormStatus.valid,
), ),
], ],
); );
}); });
group('logInWithCredentials', () { group('submit', () {
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'does nothing when status is not validated', 'does nothing when status is not validated',
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
act: (cubit) => cubit.signInWithEmailAndPassword(), act: (cubit) => cubit.submit(),
expect: () => const <SignInState>[], expect: () => const <SignInState>[],
); );
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'calls signInWithEmailAndPassword with correct email/password', 'calls signInWithEmailAndPassword with correct email/password',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const SignInState( seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid, status: FormStatus.valid,
email: validEmail,
password: validPassword,
), ),
act: (cubit) => cubit.signInWithEmailAndPassword(), act: (cubit) => cubit.submit(),
verify: (_) { verify: (_) {
verify( verify(
() => authenticationRepository.signInWithEmailAndPassword( () => authenticationRepository.signInWithEmailAndPassword(
@@ -160,33 +319,85 @@ void main() {
}, },
); );
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [submissionInProgress, submissionSuccess] ' 'emits [submissionInProgress, submissionSuccess] '
'when signInWithEmailAndPassword succeeds', 'when signInWithEmailAndPassword succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
},
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const SignInState( seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid, status: FormStatus.valid,
email: validEmail,
password: validPassword,
), ),
act: (cubit) => cubit.signInWithEmailAndPassword(), act: (cubit) => cubit.submit(),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState( SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionInProgress, status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
), ),
SignInState( SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionSuccess, status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
) )
], ],
); );
blocTest<SignInCubit, SignInState>( blocTest<SignInCubit<int>, SignInState>(
'emits [submissionInProgress, submissionFailure] ' 'emits [submissionInProgress, submissionFailure] '
'when signInWithEmailAndPassword fails', 'when signInWithEmailAndPassword fails',
setUp: () { setUp: () {
@@ -195,27 +406,77 @@ void main() {
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),
).thenThrow(Exception('oops')); ).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.signInForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
);
}, },
build: () => SignInCubit( build: () => SignInCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const SignInState( seed: () => SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.valid, status: FormStatus.valid,
email: validEmail,
password: validPassword,
), ),
act: (cubit) => cubit.signInWithEmailAndPassword(), act: (cubit) => cubit.submit(),
expect: () => const <SignInState>[ expect: () => <SignInState>[
SignInState( SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionInProgress, status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
), ),
SignInState( SignInState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signInForm,
),
status: FormStatus.submissionFailure, status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
) )
], ],
); );
@@ -19,36 +19,38 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() { void main() {
const Email email = Email.dirty('email'); final WyattForm form = WyattFormImpl(
const Password password = Password.dirty('password'); [
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
group('SignInState', () { group('SignInState', () {
test('supports value comparisons', () { test('supports value comparisons', () {
expect(const SignInState(), const SignInState()); expect(
SignInState(
form: form,
),
SignInState(form: form),
);
}); });
test('returns same object when no properties are passed', () { test('returns same object when no properties are passed', () {
expect(const SignInState().copyWith(), const SignInState()); expect(
SignInState(form: form).copyWith(),
SignInState(form: form),
);
}); });
test('returns object with updated status when status is passed', () { test('returns object with updated status when status is passed', () {
expect( expect(
const SignInState().copyWith(status: FormStatus.pure), SignInState(form: form).copyWith(status: FormStatus.invalid),
const SignInState(), SignInState(
); form: form,
}); status: FormStatus.invalid,
),
test('returns object with updated email when email is passed', () {
expect(
const SignInState().copyWith(email: email),
const SignInState(email: email),
);
});
test('returns object with updated password when password is passed', () {
expect(
const SignInState().copyWith(password: password),
const SignInState(password: password),
); );
}); });
}); });
@@ -17,163 +17,298 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class MockAuthenticationRepository extends Mock class MockAuthenticationRepository extends Mock
implements AuthenticationRepository {} implements AuthenticationRepository<int> {}
class MockAuthenticationCubit extends Mock class MockAuthenticationCubit extends Mock implements AuthenticationCubit<int> {
implements AuthenticationCubit<void> {} }
class MockAccount extends Mock implements Account {}
class MockFormRepository extends Mock implements FormRepository {}
void main() { void main() {
const String invalidEmailString = 'invalid'; const String invalidEmailString = 'invalid';
const Email invalidEmail = Email.dirty(invalidEmailString);
const String validEmailString = 'test@gmail.com'; const String validEmailString = 'test@gmail.com';
const Email validEmail = Email.dirty(validEmailString);
const String invalidPasswordString = 'invalid'; const String invalidPasswordString = 'invalid';
const Password invalidPassword = Password.dirty(invalidPasswordString);
const String validPasswordString = 't0pS3cret1234'; const String validPasswordString = 't0pS3cret1234';
const Password validPassword = Password.dirty(validPasswordString);
group('SignUpCubit', () { group('SignUpCubit', () {
late AuthenticationRepository authenticationRepository; final MockAccount account = MockAccount();
late AuthenticationCubit<void> authenticationCubit; final WyattForm form = WyattFormImpl(
[
FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signUpForm,
);
late MockFormRepository formRepository;
late AuthenticationRepository<int> authenticationRepository;
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository(); authenticationRepository = MockAuthenticationRepository();
authenticationCubit = MockAuthenticationCubit(); formRepository = MockFormRepository();
when( when(
() => authenticationRepository.signUp( () => authenticationRepository.signUp(
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),
).thenAnswer((_) async => 'uid'); ).thenAnswer((_) async => Ok(account));
when( when(
() => authenticationCubit.start(), () => authenticationRepository.formRepository,
).thenReturn(true); ).thenAnswer((_) => formRepository);
when( when(
() => authenticationCubit.stop(), () => formRepository.accessForm(AuthFormName.signUpForm),
).thenReturn(true); ).thenAnswer((_) => form);
}); });
test('initial state is SignUpState', () { test('initial state is SignUpState', () {
expect( expect(
SignUpCubit( SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
).state, ).state,
const SignUpState(data: FormData.empty()), SignUpState(form: form),
); );
}); });
group('emailChanged', () { group('emailChanged', () {
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [invalid] when email/password are invalid', 'emits [invalid] when email/password are invalid',
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
act: (cubit) => cubit.emailChanged(invalidEmailString), act: (cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <SignUpState>[ expect: () => <SignUpState>[
const SignUpState( SignUpState(
email: invalidEmail, form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(invalidEmailString),
),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid, status: FormStatus.invalid,
data: FormData.empty(),
), ),
], ],
); );
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [valid] when email/password are valid', 'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
seed: () => const SignUpState( seed: () => SignUpState(
password: validPassword, form: WyattFormImpl(
data: FormData.empty(), [
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
), ),
act: (cubit) => cubit.emailChanged(validEmailString), act: (cubit) => cubit.emailChanged(validEmailString),
expect: () => <SignUpState>[ expect: () => <SignUpState>[
const SignUpState( SignUpState(
email: validEmail, form: WyattFormImpl(
password: validPassword, [
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid, status: FormStatus.valid,
data: FormData.empty(),
), ),
], ],
); );
}); });
group('passwordChanged', () { group('passwordChanged', () {
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [invalid] when email/password are invalid', 'emits [invalid] when email/password are invalid',
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
act: (cubit) => cubit.passwordChanged(invalidPasswordString), act: (cubit) => cubit.passwordChanged(invalidPasswordString),
expect: () => <SignUpState>[ expect: () => <SignUpState>[
const SignUpState( SignUpState(
password: invalidPassword, form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.pure(),
),
FormInput(
AuthFormField.password,
const Password.dirty(invalidPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid, status: FormStatus.invalid,
data: FormData.empty(),
), ),
], ],
); );
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [valid] when email/password are valid', 'emits [valid] when email/password are valid',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
seed: () => const SignUpState( seed: () => SignUpState(
email: validEmail, form: WyattFormImpl(
data: FormData.empty(), [
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.pure(),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.invalid,
), ),
act: (cubit) => cubit.passwordChanged(validPasswordString), act: (cubit) => cubit.passwordChanged(validPasswordString),
expect: () => <SignUpState>[ expect: () => <SignUpState>[
const SignUpState( SignUpState(
email: validEmail, form: WyattFormImpl(
password: validPassword, [
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid, status: FormStatus.valid,
data: FormData.empty(),
), ),
], ],
); );
}); });
group('signUpFormSubmitted', () { group('submit', () {
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'does nothing when status is not validated', 'does nothing when status is not validated',
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
act: (cubit) => cubit.signUpFormSubmitted(), act: (cubit) => cubit.submit(),
expect: () => const <SignUpState>[], expect: () => const <SignUpState>[],
); );
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'calls signUp with correct email/password/confirmedPassword', 'calls signUp with correct email/password',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
seed: () => const SignUpState( seed: () => SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.valid, status: FormStatus.valid,
email: validEmail,
password: validPassword,
data: FormData.empty(),
), ),
act: (cubit) => cubit.signUpFormSubmitted(), act: (cubit) => cubit.submit(),
verify: (_) { verify: (_) {
verify( verify(
() => authenticationRepository.signUp( () => authenticationRepository.signUp(
@@ -184,37 +319,85 @@ void main() {
}, },
); );
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [submissionInProgress, submissionSuccess] ' 'emits [submissionInProgress, submissionSuccess] '
'when signUp succeeds', 'when signUp succeeds',
setUp: () {
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
},
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
seed: () => const SignUpState( seed: () => SignUpState(
status: FormStatus.valid, form: WyattFormImpl(
email: validEmail, [
password: validPassword, FormInput(
data: FormData.empty(), AuthFormField.email,
), const Email.dirty(validEmailString),
act: (cubit) => cubit.signUpFormSubmitted(), ),
expect: () => <SignUpState>[ FormInput(
const SignUpState( AuthFormField.password,
status: FormStatus.submissionInProgress, const Password.dirty(validPasswordString),
email: validEmail, )
password: validPassword, ],
data: FormData.empty(), name: AuthFormName.signUpForm,
), ),
const SignUpState( status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <SignUpState>[
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionInProgress,
),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionSuccess, status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
data: FormData.empty(),
) )
], ],
); );
blocTest<SignUpCubit, SignUpState>( blocTest<SignUpCubit<int>, SignUpState>(
'emits [submissionInProgress, submissionFailure] ' 'emits [submissionInProgress, submissionFailure] '
'when signUp fails', 'when signUp fails',
setUp: () { setUp: () {
@@ -223,31 +406,77 @@ void main() {
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),
).thenThrow(Exception('oops')); ).thenAnswer((_) async => Err(ServerException()));
when(
() => formRepository.accessForm(AuthFormName.signUpForm),
).thenAnswer(
(_) => WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
);
}, },
build: () => SignUpCubit( build: () => SignUpCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
formData: const FormData.empty(),
), ),
seed: () => const SignUpState( seed: () => SignUpState(
status: FormStatus.valid, form: WyattFormImpl(
email: validEmail, [
password: validPassword, FormInput(
data: FormData.empty(), AuthFormField.email,
), const Email.dirty(validEmailString),
act: (cubit) => cubit.signUpFormSubmitted(), ),
expect: () => <SignUpState>[ FormInput(
const SignUpState( AuthFormField.password,
status: FormStatus.submissionInProgress, const Password.dirty(validPasswordString),
email: validEmail, )
password: validPassword, ],
data: FormData.empty(), name: AuthFormName.signUpForm,
), ),
const SignUpState( status: FormStatus.valid,
),
act: (cubit) => cubit.submit(),
expect: () => <SignUpState>[
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionInProgress,
),
SignUpState(
form: WyattFormImpl(
[
FormInput(
AuthFormField.email,
const Email.dirty(validEmailString),
),
FormInput(
AuthFormField.password,
const Password.dirty(validPasswordString),
)
],
name: AuthFormName.signUpForm,
),
status: FormStatus.submissionFailure, status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
data: FormData.empty(),
) )
], ],
); );
@@ -19,93 +19,37 @@ import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
void main() { void main() {
const Email email = Email.dirty('email'); final WyattForm form = WyattFormImpl(
const String passwordString = 'password'; [
const Password password = Password.dirty(passwordString); FormInput(AuthFormField.email, const Email.pure()),
FormInput(AuthFormField.password, const Password.pure())
],
name: AuthFormName.signInForm,
);
group('SignUpState', () { group('SignUpState', () {
test('supports value comparisons', () { test('supports value comparisons', () {
expect( expect(
const SignUpState( SignUpState(
data: FormData.empty(), form: form,
),
const SignUpState(
data: FormData.empty(),
), ),
SignUpState(form: form),
); );
}); });
test('returns same object when no properties are passed', () { test('returns same object when no properties are passed', () {
expect( expect(
const SignUpState( SignUpState(form: form).copyWith(),
data: FormData.empty(), SignUpState(form: form),
).copyWith(),
const SignUpState(
data: FormData.empty(),
),
); );
}); });
test('returns object with updated status when status is passed', () { test('returns object with updated status when status is passed', () {
expect( expect(
const SignUpState( SignUpState(form: form).copyWith(status: FormStatus.invalid),
data: FormData.empty(), SignUpState(
).copyWith(status: FormStatus.pure), form: form,
const SignUpState( status: FormStatus.invalid,
data: FormData.empty(),
),
);
});
test('returns object with updated email when email is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(email: email),
const SignUpState(
email: email,
data: FormData.empty(),
),
);
});
test('returns object with updated password when password is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(password: password),
const SignUpState(
password: password,
data: FormData.empty(),
),
);
});
test(
'returns object with updated data'
' when data is passed', () {
expect(
const SignUpState(
data: FormData.empty(),
).copyWith(
data: const FormData(
[
FormInput(
'field',
Name.pure(),
),
],
),
),
const SignUpState(
data: FormData(
[
FormInput(
'field',
Name.pure(),
),
],
),
), ),
); );
}); });
+4
View File
@@ -1,3 +1,7 @@
## 0.1.0+1
- Update a dependency to the latest release.
## 0.1.0 ## 0.1.0
- **REFACTOR**: refactor simple example. - **REFACTOR**: refactor simple example.
+5 -7
View File
@@ -1,7 +1,7 @@
name: wyatt_form_bloc name: wyatt_form_bloc
description: Manage forms in Dart & Flutter with Bloc description: Manage forms in Dart & Flutter with Bloc
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc
version: 0.1.0 version: 0.1.0+1
publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub publish_to: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub
@@ -16,14 +16,12 @@ dependencies:
equatable: ^2.0.5 equatable: ^2.0.5
wyatt_architecture: wyatt_architecture:
git: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages version: ^0.0.2
ref: wyatt_architecture-v0.0.2-dev.0
path: packages/wyatt_architecture
wyatt_type_utils: wyatt_type_utils:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 0.0.3+1 version: ^0.0.3+1
dev_dependencies: dev_dependencies:
@@ -32,4 +30,4 @@ dev_dependencies:
wyatt_analysis: wyatt_analysis:
hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/
version: 2.2.2 version: ^2.2.2