wyatt-packages/packages/wyatt_authentication_bloc

Flutter - Authentication BLoC

Style: Wyatt Analysis SDK: Flutter

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

import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';

Usage

Create an authentication repository:

final AuthenticationRepositoryInterface _authenticationRepository = AuthenticationRepositoryFirebase();

Create an authentication cubit:

final AuthenticationCubit _authenticationCubit = AuthenticationCubit(
    authenticationRepository: _authenticationRepository,
);

Create a sign up cubit:

final SignUpCubit _signUpCubit = SignUpCubit(
    authenticationRepository: _authenticationRepository,
    authenticationCubit: _authenticationCubit,
);

You can use AuthenticationBloc to route your app.

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:

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:

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:

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

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:

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:

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:

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:

context.read<SignUpCubit>().signUpFormSubmitted()

And voilà, a document with uid as id, and fields email, name, phone, uid will be create in users collection.