Flutter - Authentication BLoC
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 changesstop()
to cancel listener
- Tracks every user changes
- 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 aFormData
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)
- Use
- 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()
ANDdataChanged()
.
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
andexport
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.