master #81

Closed
malo wants to merge 322 commits from master into feat/bloc_layout/new-package
6 changed files with 255 additions and 124 deletions
Showing only changes of commit 8ed9e86db2 - Show all commits

View File

@ -1,5 +1,5 @@
<!--
* Copyright (C) 2022 WYATT GROUP
* Copyright (C) 2023 WYATT GROUP
* Please see the AUTHORS file for details.
* This program is free software: you can redistribute it and/or modify
@ -19,9 +19,7 @@
# 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>
<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>
@ -29,28 +27,22 @@ Authentication Bloc for Flutter.
## Features
- Wyatt Architecture
- Entities:
- Account : AccountModel -> Contains account information from provider
- AccountWrapper : AccountWrapperModel -> Contains account and extra data.
- Data Sources:
- Local:
- Cached Authentication Data : AuthenticationCacheDataSourceImpl -> Provides a cache implementation
- Remote:
- Remote Authentication Data : AuthenticationFirebaseDataSourceImpl -> Provides a proxy to FirebaseAuth
- Repositories:
- AuthenticationRepository : AuthenticationRepositoryImpl -> Provides all authentication methods
- Features:
- Authentication:
- AuthenticationBuilder : widget to build reactive view from authentication state
- AuthenticationCubit : tracks every auth changes, have sign out capability.
- SignUp:
- SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign up
- SignIn:
- SignUpCubit: implementation of a FormDataCubit from `wyatt_form_bloc` for the sign in
- Consistent
* Every class have same naming convention
- Tested
* 🧐 Wyatt Architecture
* 🧱 Entities
- Account -> Contains account information from provider.
- Session -> Contains account and associated data retrieved from an external source.
- AuthenticationChangeEvent -> Describes an event in authentication change (sign in, sign up, sign out, etc...)
- SessionWrapper -> Contains latest authentication change event and session.
* 🔑 Powerful and secured authentication repository
* 🔥 Multiple data sources
- Mock
- Firebase
* 🧊 Cubits, why make it complicated when you can make it simple?
- Goes to the essential.
* 📐 Consistent
- Every class have same naming convention
* 🧪 Tested
* 📚 Documented: [available here](./doc/api/index.md)
## Getting started
@ -60,6 +52,170 @@ Simply add `wyatt_authentication_bloc` in `pubspec.yaml`, then
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
```
## Usage
### Data source
The first step is to provide a data source.
```dart
getIt.registerLazySingleton<AuthenticationRemoteDataSource<int>>(
() => AuthenticationFirebaseDataSourceImpl<int>(
firebaseAuth: FirebaseAuth.instance,
googleSignIn:
GoogleSignIn(clientId: DefaultFirebaseOptions.ios.iosClientId)),
);
```
> Here we use GetIt (see example project)
### Repository
Then you can configure your repository.
```dart
final AuthenticationRepository<int> authenticationRepository = AuthenticationRepositoryImpl(
authenticationRemoteDataSource:
getIt<AuthenticationRemoteDataSource<int>>(),
customPasswordValidator: const CustomPassword.pure(),
extraSignUpInputs: [
FormInput(
AuthFormField.confirmPassword,
const ConfirmedPassword.pure(),
metadata: const FormInputMetadata<void>(export: false),
),
],
);
```
> Here we pass some extra inputs for the sign up, and a custom password validator.
### Cubits
It is necessary to implement each cubit. Don't panic, most of the work is already done 😊 you just have to customize the logic of these.
In each of these cubits it is necessary to overload the various callbacks.
> Here the associated data `Data` is a `int`
#### Authentication
In the authentication are managed, the refresh, the deletion of account or the disconnection.
```dart
class ExampleAuthenticationCubit extends AuthenticationCubit<int> {
ExampleAuthenticationCubit({required super.authenticationRepository});
@override
FutureOrResult<int?> onReauthenticate(Result<Account, AppException> result) async {
// TODO
}
@override
FutureOrResult<int?> onRefresh(Result<Account, AppException> result) {
// TODO
}
@override
FutureOrResult<int?> onSignInFromCache(SessionWrapper<int> wrapper) {
// TODO
}
@override
FutureOrResult<void> onSignOut() {
// TODO
}
@override
FutureOrResult<void> onDelete() {
// TODO
}
}
```
#### Sign Up
```dart
class ExampleSignUpCubit extends SignUpCubit<int> {
ExampleSignUpCubit({
required super.authenticationRepository,
});
@override
FutureOrResult<int?> onSignUpWithEmailAndPassword(Result<Account, AppException> result, WyattForm form) async {
// TODO
}
}
```
#### Sign In
```dart
class ExampleSignInCubit extends SignInCubit<int> {
ExampleSignInCubit({
required super.authenticationRepository,
});
@override
FutureOrResult<int?> onSignInWithEmailAndPassword(Result<Account, AppException> result, WyattForm form) {
// TODO
}
@override
FutureOrResult<int?> onSignInAnonymously(Result<Account, AppException> result, WyattForm form) {
// TODO
}
@override
FutureOrResult<int?> onSignInWithGoogle(Result<Account, AppException> result, WyattForm form) {
// TODO
}
}
```
After setting up all these cubits you can provide them in the application. And that's it!
```dart
BlocProvider<SignUpCubit<int>>(
create: (_) => ExampleSignUpCubit(
authenticationRepository: authenticationRepository,
),
),
BlocProvider<SignInCubit<int>>(
create: (_) => ExampleSignInCubit(
authenticationRepository: authenticationRepository,
),
),
```
### Widgets
Widgets are provided to make your life easier. Starting with the `AuthenticationBuilder` which allows you to build according to the authentication state.
```dart
AuthenticationBuilder<int>(
authenticated: (context, sessionWrapper) => Text(
'Logged as ${sessionWrapper.session?.account.email} | GeneratedId is ${sessionWrapper.session?.data}'),
unauthenticated: (context) =>
const Text('Not logged (unauthenticated)'),
unknown: (context) => const Text('Not logged (unknown)'),
),
```
A `BuildContext` extension is also available to access certain attributes more quickly.
```dart
Text('Home | ${context.account<AuthenticationCubit<int>, int>()?.email}'),
```
Listeners are used to listen to the status of the sign in and sign up forms.
```dart
return SignInListener<int>(
onError: (context, status, errorMessage) => ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(errorMessage ?? 'Sign In Failure')),
),
child: ...
);
```

View File

@ -10,7 +10,6 @@ dart pub global run dartdoc --format md \
--no-auto-include-dependencies \
--no-validate-links \
--show-progress \
--output "/Users/hpcl/Work/Wyatt/wyatt-packages/wiki/wyatt_authentication_bloc/"
sed -i -e "s/\/\/export 'package:firebase_auth\/firebase_auth.dart';/export 'package:firebase_auth\/firebase_auth.dart';/g" lib/wyatt_authentication_bloc.dart
sed -i -e "s/\/\/export 'package:google_sign_in\/google_sign_in.dart';/export 'package:google_sign_in\/google_sign_in.dart';/g" lib/wyatt_authentication_bloc.dart

View File

@ -26,25 +26,49 @@ class MockAuthenticationRepository extends Mock
class MockAccount extends Mock implements Account {}
class TestAuthenticationCubit extends AuthenticationCubit<int> {
TestAuthenticationCubit({required super.authenticationRepository});
@override
FutureOrResult<void> onDelete() async => const Ok(null);
@override
FutureOrResult<int?> onReauthenticate(
Result<Account, AppException> result,
) async =>
const Ok(null);
@override
FutureOrResult<int?> onRefresh(Result<Account, AppException> result) async =>
const Ok(null);
@override
FutureOrResult<int?> onSignInFromCache(SessionWrapper<int> wrapper) async =>
const Ok(null);
@override
FutureOrResult<void> onSignOut() async => const Ok(null);
}
void main() {
group('AuthenticationCubit<T>', () {
final MockAccount account = MockAccount();
final AccountWrapper<int> wrapper = AccountWrapperModel(account, 10);
final SessionWrapper<int> wrapper = SessionWrapper(
event: const UnknownAuthenticationEvent(),
session: Session(account: account, data: 10),
);
late AuthenticationRepository<int> authenticationRepository;
setUp(() {
authenticationRepository = MockAuthenticationRepository();
when(() => authenticationRepository.streamAccount()).thenAnswer(
when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => const Stream.empty(),
);
when(
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
});
test('initial auth state is `unknown`', () {
expect(
AuthenticationCubit<int>(
TestAuthenticationCubit(
authenticationRepository: authenticationRepository,
).state,
const AuthenticationState<Never>.unknown(),
@ -53,17 +77,13 @@ void main() {
group('ListenForAuthenticationChanges', () {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits authenticated when stream contains account',
'emits authenticated when stream contains session',
setUp: () {
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Ok(wrapper),
)
]),
when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => Stream.fromIterable([wrapper]),
);
},
build: () => AuthenticationCubit(
build: () => TestAuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
@ -73,39 +93,12 @@ void main() {
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)),
)
]),
when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => Stream.fromIterable(
[const SessionWrapper(event: SignedOutEvent())],),
);
},
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(
build: () => TestAuthenticationCubit(
authenticationRepository: authenticationRepository,
),
seed: () => const AuthenticationState.unknown(),
@ -121,7 +114,7 @@ void main() {
() => authenticationRepository.signOut(),
).thenAnswer((_) async => const Ok(null));
},
build: () => AuthenticationCubit(
build: () => TestAuthenticationCubit(
authenticationRepository: authenticationRepository,
),
act: (cubit) => cubit.signOut(),

View File

@ -27,7 +27,7 @@ void main() {
const AuthenticationState<void> state =
AuthenticationState.unauthenticated();
expect(state.status, AuthenticationStatus.unauthenticated);
expect(state.accountWrapper, null);
expect(state.wrapper, null);
});
});
@ -36,10 +36,13 @@ void main() {
final MockAccount account = MockAccount();
final AuthenticationState<void> state =
AuthenticationState.authenticated(
AccountWrapperModel<void>(account, null),
SessionWrapper<void>(
event: SignedInEvent(account: account),
session: Session(account: account),
),
);
expect(state.status, AuthenticationStatus.authenticated);
expect(state.accountWrapper?.account, account);
expect(state.wrapper?.session?.account, account);
});
});
@ -49,11 +52,14 @@ void main() {
const String extra = 'AwesomeExtraData';
final AuthenticationState<String> state =
AuthenticationState.authenticated(
AccountWrapperModel(account, extra),
SessionWrapper<String>(
event: SignedInEvent(account: account),
session: Session(account: account, data: extra),
),
);
expect(state.status, AuthenticationStatus.authenticated);
expect(state.accountWrapper?.account, account);
expect(state.accountWrapper?.data, extra);
expect(state.wrapper?.session?.account, account);
expect(state.wrapper?.session?.data, extra);
});
});
});

View File

@ -39,15 +39,21 @@ void main() {
setUp(() {
authenticationRepository = MockAuthenticationRepository();
when(
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
account = MockAccount();
when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => Stream.fromIterable([
SessionWrapper<int>(
event: SignedInFromCacheEvent(account: account),
session: Session<int>(account: account, data: 10),
)
]),
);
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
).thenAnswer((_) async => Ok(account));
account = MockAccount();
when(
() => account.emailVerified,
).thenAnswer((_) => true);
@ -129,7 +135,7 @@ void main() {
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
).thenAnswer((_) async => Ok(account));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
@ -145,7 +151,7 @@ void main() {
setUp: () {
when(
() => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null));
).thenAnswer((_) async => Ok(account));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
@ -161,7 +167,7 @@ void main() {
'emits success with true if verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
.thenAnswer((_) async => Ok(account));
},
build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository,
@ -183,7 +189,7 @@ void main() {
'emits success with false if not verified',
setUp: () {
when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null));
.thenAnswer((_) async => Ok(account));
when(() => account.emailVerified).thenAnswer((_) => false);
},
build: () => EmailVerificationCubit(
@ -222,28 +228,6 @@ void main() {
)
],
);
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,
)
],
);
});
});
}

View File

@ -59,14 +59,7 @@ void main() {
formRepository = MockFormRepository();
when(
() => authenticationRepository.signUp(
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenAnswer((_) async => Ok(account));
when(
() => authenticationRepository.signInWithEmailAndPassword(
() => authenticationRepository.signUpWithEmailAndPassword(
email: any(named: 'email'),
password: any(named: 'password'),
),
@ -318,7 +311,7 @@ void main() {
act: (cubit) => cubit.signUpWithEmailPassword(),
verify: (_) {
verify(
() => authenticationRepository.signUp(
() => authenticationRepository.signUpWithEmailAndPassword(
email: validEmailString,
password: validPasswordString,
),
@ -409,7 +402,7 @@ void main() {
'when signUp fails',
setUp: () {
when(
() => authenticationRepository.signUp(
() => authenticationRepository.signUpWithEmailAndPassword(
email: any(named: 'email'),
password: any(named: 'password'),
),