<!--
 * 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.