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. * Please see the AUTHORS file for details.
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -7,7 +7,7 @@
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* any later version. * any later version.
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
@ -19,9 +19,7 @@
# Flutter - Authentication BLoC # Flutter - Authentication BLoC
<p align="left"> <p align="left">
<a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis"> <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/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" /> <img src="https://img.shields.io/badge/SDK-Flutter-blue?style=flat-square" alt="SDK: Flutter" />
</p> </p>
@ -29,37 +27,195 @@ Authentication Bloc for Flutter.
## Features ## Features
- Wyatt Architecture * 🧐 Wyatt Architecture
- Entities: * 🧱 Entities
- Account : AccountModel -> Contains account information from provider - Account -> Contains account information from provider.
- AccountWrapper : AccountWrapperModel -> Contains account and extra data. - Session -> Contains account and associated data retrieved from an external source.
- Data Sources: - AuthenticationChangeEvent -> Describes an event in authentication change (sign in, sign up, sign out, etc...)
- Local: - SessionWrapper -> Contains latest authentication change event and session.
- Cached Authentication Data : AuthenticationCacheDataSourceImpl -> Provides a cache implementation * 🔑 Powerful and secured authentication repository
- Remote: * 🔥 Multiple data sources
- Remote Authentication Data : AuthenticationFirebaseDataSourceImpl -> Provides a proxy to FirebaseAuth - Mock
- Repositories: - Firebase
- AuthenticationRepository : AuthenticationRepositoryImpl -> Provides all authentication methods * 🧊 Cubits, why make it complicated when you can make it simple?
- Features: - Goes to the essential.
- Authentication: * 📐 Consistent
- AuthenticationBuilder : widget to build reactive view from authentication state - Every class have same naming convention
- AuthenticationCubit : tracks every auth changes, have sign out capability. * 🧪 Tested
- SignUp: * 📚 Documented: [available here](./doc/api/index.md)
- 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
## Getting started ## Getting started
Simply add `wyatt_authentication_bloc` in `pubspec.yaml`, then Simply add `wyatt_authentication_bloc` in `pubspec.yaml` , then
```dart ```dart
import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart'; import 'package:wyatt_authentication_bloc/wyatt_authentication_bloc.dart';
``` ```
## Usage ### Data source
// TODO 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-auto-include-dependencies \
--no-validate-links \ --no-validate-links \
--show-progress \ --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: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 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 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() { void main() {
group('AuthenticationCubit<T>', () { group('AuthenticationCubit<T>', () {
final MockAccount account = MockAccount(); 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; late AuthenticationRepository<int> authenticationRepository;
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository(); authenticationRepository = MockAuthenticationRepository();
when(() => authenticationRepository.streamAccount()).thenAnswer( when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => const Stream.empty(), (_) => const Stream.empty(),
); );
when(
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account));
}); });
test('initial auth state is `unknown`', () { test('initial auth state is `unknown`', () {
expect( expect(
AuthenticationCubit<int>( TestAuthenticationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
).state, ).state,
const AuthenticationState<Never>.unknown(), const AuthenticationState<Never>.unknown(),
@ -53,17 +77,13 @@ void main() {
group('ListenForAuthenticationChanges', () { group('ListenForAuthenticationChanges', () {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>( blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits authenticated when stream contains account', 'emits authenticated when stream contains session',
setUp: () { setUp: () {
when(() => authenticationRepository.streamAccount()).thenAnswer( when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => Stream.fromIterable([ (_) => Stream.fromIterable([wrapper]),
Future.value(
Ok(wrapper),
)
]),
); );
}, },
build: () => AuthenticationCubit( build: () => TestAuthenticationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const AuthenticationState.unknown(), seed: () => const AuthenticationState.unknown(),
@ -73,39 +93,12 @@ void main() {
blocTest<AuthenticationCubit<int>, AuthenticationState<int>>( blocTest<AuthenticationCubit<int>, AuthenticationState<int>>(
'emits unauthenticated when account stream is empty', 'emits unauthenticated when account stream is empty',
setUp: () { setUp: () {
when( when(() => authenticationRepository.sessionStream()).thenAnswer(
() => authenticationRepository.destroyCache(), (_) => Stream.fromIterable(
).thenAnswer((_) async => const Ok(null)); [const SessionWrapper(event: SignedOutEvent())],),
when(() => authenticationRepository.streamAccount()).thenAnswer(
(_) => Stream.fromIterable([
Future.value(
Ok(AccountWrapperModel(null, 1)),
)
]),
); );
}, },
build: () => AuthenticationCubit( build: () => TestAuthenticationCubit(
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(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
seed: () => const AuthenticationState.unknown(), seed: () => const AuthenticationState.unknown(),
@ -121,7 +114,7 @@ void main() {
() => authenticationRepository.signOut(), () => authenticationRepository.signOut(),
).thenAnswer((_) async => const Ok(null)); ).thenAnswer((_) async => const Ok(null));
}, },
build: () => AuthenticationCubit( build: () => TestAuthenticationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
), ),
act: (cubit) => cubit.signOut(), act: (cubit) => cubit.signOut(),

View File

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

View File

@ -39,15 +39,21 @@ void main() {
setUp(() { setUp(() {
authenticationRepository = MockAuthenticationRepository(); authenticationRepository = MockAuthenticationRepository();
when( account = MockAccount();
() => authenticationRepository.getAccount(),
).thenAnswer((_) async => Ok(account)); when(() => authenticationRepository.sessionStream()).thenAnswer(
(_) => Stream.fromIterable([
SessionWrapper<int>(
event: SignedInFromCacheEvent(account: account),
session: Session<int>(account: account, data: 10),
)
]),
);
when( when(
() => authenticationRepository.refresh(), () => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null)); ).thenAnswer((_) async => Ok(account));
account = MockAccount();
when( when(
() => account.emailVerified, () => account.emailVerified,
).thenAnswer((_) => true); ).thenAnswer((_) => true);
@ -129,7 +135,7 @@ void main() {
setUp: () { setUp: () {
when( when(
() => authenticationRepository.refresh(), () => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null)); ).thenAnswer((_) async => Ok(account));
}, },
build: () => EmailVerificationCubit( build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
@ -145,7 +151,7 @@ void main() {
setUp: () { setUp: () {
when( when(
() => authenticationRepository.refresh(), () => authenticationRepository.refresh(),
).thenAnswer((_) async => const Ok(null)); ).thenAnswer((_) async => Ok(account));
when(() => account.emailVerified).thenAnswer((_) => false); when(() => account.emailVerified).thenAnswer((_) => false);
}, },
build: () => EmailVerificationCubit( build: () => EmailVerificationCubit(
@ -161,7 +167,7 @@ void main() {
'emits success with true if verified', 'emits success with true if verified',
setUp: () { setUp: () {
when(() => authenticationRepository.refresh()) when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null)); .thenAnswer((_) async => Ok(account));
}, },
build: () => EmailVerificationCubit( build: () => EmailVerificationCubit(
authenticationRepository: authenticationRepository, authenticationRepository: authenticationRepository,
@ -183,7 +189,7 @@ void main() {
'emits success with false if not verified', 'emits success with false if not verified',
setUp: () { setUp: () {
when(() => authenticationRepository.refresh()) when(() => authenticationRepository.refresh())
.thenAnswer((_) async => const Ok(null)); .thenAnswer((_) async => Ok(account));
when(() => account.emailVerified).thenAnswer((_) => false); when(() => account.emailVerified).thenAnswer((_) => false);
}, },
build: () => EmailVerificationCubit( 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(); formRepository = MockFormRepository();
when( when(
() => authenticationRepository.signUp( () => authenticationRepository.signUpWithEmailAndPassword(
email: any(named: 'email'),
password: any(named: 'password'),
),
).thenAnswer((_) async => Ok(account));
when(
() => authenticationRepository.signInWithEmailAndPassword(
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),
@ -318,7 +311,7 @@ void main() {
act: (cubit) => cubit.signUpWithEmailPassword(), act: (cubit) => cubit.signUpWithEmailPassword(),
verify: (_) { verify: (_) {
verify( verify(
() => authenticationRepository.signUp( () => authenticationRepository.signUpWithEmailAndPassword(
email: validEmailString, email: validEmailString,
password: validPasswordString, password: validPasswordString,
), ),
@ -409,7 +402,7 @@ void main() {
'when signUp fails', 'when signUp fails',
setUp: () { setUp: () {
when( when(
() => authenticationRepository.signUp( () => authenticationRepository.signUpWithEmailAndPassword(
email: any(named: 'email'), email: any(named: 'email'),
password: any(named: 'password'), password: any(named: 'password'),
), ),