303 lines
9.3 KiB
Markdown
303 lines
9.3 KiB
Markdown
<!--
|
|
* 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/>.
|
|
-->
|
|
|
|
# Flutter - Authentication BLoC
|
|
|
|
<p align="left">
|
|
<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" />
|
|
</a>
|
|
<img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
|
|
</p>
|
|
|
|
Authentication Bloc for Flutter.
|
|
|
|
## Features
|
|
|
|
- UserInterface
|
|
* UserFirebase : FirebaseAuth user implementation
|
|
- AuthenticationRepositoryInterface
|
|
* AuthenticationRepositoryFirebase : FirebaseAuth implementation
|
|
- ExceptionsInterface
|
|
* ExceptionsFirebase : FirebaseAuth Exception parsing implementation
|
|
- AuthenticationBloc
|
|
* Tracks every user changes
|
|
- Right after the listener has been registered.
|
|
- When a user is signed in.
|
|
- When the current user is signed out.
|
|
- When there is a change in the current user's token.
|
|
- On `refresh()`
|
|
* Start/Stop listening on demand
|
|
- `start()` to listen to user changes
|
|
- `stop()` to cancel listener
|
|
- SignUpCubit
|
|
* Handles email/password validation and password confirmation
|
|
* Handles register with email/password
|
|
* 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
|
|
* Every class have same naming convention
|
|
- Tested
|
|
* Partially tested with *bloc_test*
|
|
|
|
## Getting started
|
|
|
|
Simply add `wyatt_authentication_bloc` in `pubspec.yaml`, then
|
|
|
|
```dart
|
|
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
|
|
```
|
|
|
|
## Usage
|
|
|
|
Create an authentication repository:
|
|
|
|
```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. |