From ec8f80d9da8cfcfd1bf0dd3bd6e5b0d8545395d1 Mon Sep 17 00:00:00 2001 From: fremartini Date: Thu, 23 Nov 2023 19:20:44 +0100 Subject: [PATCH] and a comma --- .../authentication_local_data_source.dart | 2 - .../data/models/authenticated_user_model.dart | 52 ---------- .../domain/entities/authenticated_user.dart | 7 -- .../usecases/save_authenticated_user.dart | 5 - .../cubits/authentication_cubit.dart | 72 +++++++------- .../cubits/authentication_state.dart | 8 -- .../account_remote_data_source.dart | 2 - .../presentation/pages/re_login_page.dart | 97 ------------------- .../redirection/redirection_router.dart | 26 +---- .../session_local_data_source.dart | 43 ++++++++ .../data/models/session_details_model.dart | 60 ++++++++++++ .../domain/entities/session_details.dart | 12 +++ .../domain/usecases/get_session_details.dart | 13 +++ .../domain/usecases/save_session_details.dart | 18 ++++ .../cubit/session_timeout_cubit.dart | 51 ++++++++++ .../cubit/session_timeout_state.dart | 0 .../widgets/session_timeout_dropdown.dart | 5 +- .../cubit/session_timeout_cubit.dart | 27 ------ .../widgets/sections/account_section.dart | 2 +- lib/service_locator.dart | 29 ++++++ ...authentication_local_data_source_test.dart | 8 +- .../authentication_interceptor_test.dart | 4 +- .../models/authenticated_user_model_test.dart | 45 +-------- .../cubit/authentication_cubit_test.dart | 50 ++++------ .../account_remote_data_source_test.dart | 4 +- .../data/reactivation_authenticator_test.dart | 26 ++--- .../authenticated_user.json | 4 +- 27 files changed, 301 insertions(+), 371 deletions(-) delete mode 100644 lib/features/login/presentation/pages/re_login_page.dart create mode 100644 lib/features/session/data/datasources/session_local_data_source.dart create mode 100644 lib/features/session/data/models/session_details_model.dart create mode 100644 lib/features/session/domain/entities/session_details.dart create mode 100644 lib/features/session/domain/usecases/get_session_details.dart create mode 100644 lib/features/session/domain/usecases/save_session_details.dart create mode 100644 lib/features/session/presentation/cubit/session_timeout_cubit.dart rename lib/features/{settings => session}/presentation/cubit/session_timeout_state.dart (100%) rename lib/features/{settings => session}/presentation/widgets/session_timeout_dropdown.dart (85%) delete mode 100644 lib/features/settings/presentation/cubit/session_timeout_cubit.dart diff --git a/lib/features/authentication/data/datasources/authentication_local_data_source.dart b/lib/features/authentication/data/datasources/authentication_local_data_source.dart index 641b01aa1..e7a27a334 100644 --- a/lib/features/authentication/data/datasources/authentication_local_data_source.dart +++ b/lib/features/authentication/data/datasources/authentication_local_data_source.dart @@ -55,8 +55,6 @@ class AuthenticationLocalDataSource { email: user.email, token: token, encodedPasscode: user.encodedPasscode, - lastLogin: user.lastLogin, - sessionTimeout: user.sessionTimeout, ); await saveAuthenticatedUser(model); diff --git a/lib/features/authentication/data/models/authenticated_user_model.dart b/lib/features/authentication/data/models/authenticated_user_model.dart index d4834d092..f7f257976 100644 --- a/lib/features/authentication/data/models/authenticated_user_model.dart +++ b/lib/features/authentication/data/models/authenticated_user_model.dart @@ -1,77 +1,25 @@ import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart'; -import 'package:fpdart/fpdart.dart'; class AuthenticatedUserModel extends AuthenticatedUser { const AuthenticatedUserModel({ required super.email, required super.token, required super.encodedPasscode, - required super.lastLogin, - required super.sessionTimeout, }); factory AuthenticatedUserModel.fromJson(Map json) { - final lastLogin = - _nullOrValue(json, 'last_login', DateTime.parse); - final sessionTimeout = _nullOrValue( - json, - 'session_timeout', - _parseDuration, - ); - return AuthenticatedUserModel( email: json['email'] as String, token: json['token'] as String, encodedPasscode: json['passcode'] as String, - lastLogin: lastLogin, - sessionTimeout: sessionTimeout, ); } - static String? _optionToString(Option option) { - return option.match(() => 'null', (value) => value.toString()); - } - Map toJson() { return { 'email': email, 'token': token, 'passcode': encodedPasscode, - 'last_login': _optionToString(lastLogin), - 'session_timeout': _optionToString(sessionTimeout), }; } - - static Option _nullOrValue( - Map m, - String key, - T Function(String) callback, - ) { - if (!m.containsKey(key)) { - return none(); - } - - final val = m[key] as String; - - if (val == 'null') { - return none(); - } - - return Some(callback(val)); - } - - static Duration _parseDuration(String s) { - int hours = 0; - int minutes = 0; - - final parts = s.split(':'); - if (parts.length > 2) { - hours = int.parse(parts[parts.length - 3]); - } - if (parts.length > 1) { - minutes = int.parse(parts[parts.length - 2]); - } - - return Duration(hours: hours, minutes: minutes); - } } diff --git a/lib/features/authentication/domain/entities/authenticated_user.dart b/lib/features/authentication/domain/entities/authenticated_user.dart index d4293f77e..a9b0ed931 100644 --- a/lib/features/authentication/domain/entities/authenticated_user.dart +++ b/lib/features/authentication/domain/entities/authenticated_user.dart @@ -1,19 +1,14 @@ import 'package:equatable/equatable.dart'; -import 'package:fpdart/fpdart.dart'; class AuthenticatedUser extends Equatable { final String email; final String token; final String encodedPasscode; - final Option lastLogin; - final Option sessionTimeout; const AuthenticatedUser({ required this.email, required this.token, required this.encodedPasscode, - required this.lastLogin, - required this.sessionTimeout, }); @override @@ -21,7 +16,5 @@ class AuthenticatedUser extends Equatable { email, token, encodedPasscode, - lastLogin, - sessionTimeout, ]; } diff --git a/lib/features/authentication/domain/usecases/save_authenticated_user.dart b/lib/features/authentication/domain/usecases/save_authenticated_user.dart index e17049317..7cb3e71a5 100644 --- a/lib/features/authentication/domain/usecases/save_authenticated_user.dart +++ b/lib/features/authentication/domain/usecases/save_authenticated_user.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/features/authentication/data/datasources/authentication_local_data_source.dart'; import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; -import 'package:fpdart/fpdart.dart'; class SaveAuthenticatedUser { final AuthenticationLocalDataSource dataSource; @@ -11,16 +10,12 @@ class SaveAuthenticatedUser { required String email, required String token, required String encodedPasscode, - required Option lastLogin, - required Option sessionTimeout, }) async { return dataSource.saveAuthenticatedUser( AuthenticatedUserModel( email: email, token: token, encodedPasscode: encodedPasscode, - lastLogin: lastLogin, - sessionTimeout: sessionTimeout, ), ); } diff --git a/lib/features/authentication/presentation/cubits/authentication_cubit.dart b/lib/features/authentication/presentation/cubits/authentication_cubit.dart index cfe87b528..4de612ffa 100644 --- a/lib/features/authentication/presentation/cubits/authentication_cubit.dart +++ b/lib/features/authentication/presentation/cubits/authentication_cubit.dart @@ -3,6 +3,8 @@ import 'package:coffeecard/features/authentication/domain/entities/authenticated import 'package:coffeecard/features/authentication/domain/usecases/clear_authenticated_user.dart'; import 'package:coffeecard/features/authentication/domain/usecases/get_authenticated_user.dart'; import 'package:coffeecard/features/authentication/domain/usecases/save_authenticated_user.dart'; +import 'package:coffeecard/features/session/domain/usecases/get_session_details.dart'; +import 'package:coffeecard/features/session/domain/usecases/save_session_details.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fpdart/fpdart.dart'; @@ -16,30 +18,39 @@ class AuthenticationCubit extends Cubit { final ClearAuthenticatedUser clearAuthenticatedUser; final SaveAuthenticatedUser saveAuthenticatedUser; final GetAuthenticatedUser getAuthenticatedUser; + final GetSessionDetails getSessionDetails; + final SaveSessionDetails saveSessionDetails; final DateService dateService; AuthenticationCubit({ required this.clearAuthenticatedUser, required this.saveAuthenticatedUser, required this.getAuthenticatedUser, + required this.getSessionDetails, + required this.saveSessionDetails, required this.dateService, }) : super(const AuthenticationState._()); Future appStarted() async { final authenticatedUser = await getAuthenticatedUser(); + final sessionDetails = await getSessionDetails(); authenticatedUser.match( () => emit(const AuthenticationState.unauthenticated()), - (authenticatedUser) { - final sessionExpired = _isSessionExpired( - authenticatedUser.lastLogin, - authenticatedUser.sessionTimeout, - ); + (authenticatedUser) async { + sessionDetails.map( + (sessionDetails) async { + final sessionExpired = _isSessionExpired( + sessionDetails.lastLogin, + sessionDetails.sessionTimeout, + ); - if (sessionExpired) { - emit(AuthenticationState.reauthenticated(authenticatedUser)); - return; - } + if (sessionExpired) { + await unauthenticated(); + return; + } + }, + ); emit(AuthenticationState.authenticated(authenticatedUser)); }, @@ -48,13 +59,17 @@ class AuthenticationCubit extends Cubit { bool _isSessionExpired( Option lastLogin, - Option sessionTimeout, + Option sessionTimeout, ) { return lastLogin.match( () => false, (lastLogin) => sessionTimeout.match( () => false, (sessionTimeout) { + if (sessionTimeout == null) { + return false; + } + final now = dateService.currentDateTime; final difference = now.difference(lastLogin); return difference > sessionTimeout; @@ -68,14 +83,24 @@ class AuthenticationCubit extends Cubit { String encodedPasscode, String token, ) async { - final now = dateService.currentDateTime; - await saveAuthenticatedUser( email: email, token: token, encodedPasscode: encodedPasscode, - lastLogin: some(now), - sessionTimeout: none(), + ); + + final sessionDetails = await getSessionDetails(); + + final now = some(dateService.currentDateTime); + sessionDetails.match( + () async => await saveSessionDetails( + lastLogin: now, + sessionTimeout: none(), + ), + (sessionDetails) async => await saveSessionDetails( + lastLogin: now, + sessionTimeout: sessionDetails.sessionTimeout, + ), ); emit( @@ -84,8 +109,6 @@ class AuthenticationCubit extends Cubit { token: token, email: email, encodedPasscode: encodedPasscode, - lastLogin: some(now), - sessionTimeout: none(), ), ), ); @@ -95,21 +118,4 @@ class AuthenticationCubit extends Cubit { await clearAuthenticatedUser(); emit(const AuthenticationState.unauthenticated()); } - - Future saveSessionTimeout(Duration? duration) async { - final user = await getAuthenticatedUser(); - - user.match( - () => null, - (user) async { - await saveAuthenticatedUser( - email: user.email, - token: user.token, - encodedPasscode: user.encodedPasscode, - lastLogin: user.lastLogin, - sessionTimeout: optionOf(duration), - ); - }, - ); - } } diff --git a/lib/features/authentication/presentation/cubits/authentication_state.dart b/lib/features/authentication/presentation/cubits/authentication_state.dart index f4aa5fd71..ddfbed8b7 100644 --- a/lib/features/authentication/presentation/cubits/authentication_state.dart +++ b/lib/features/authentication/presentation/cubits/authentication_state.dart @@ -17,12 +17,6 @@ class AuthenticationState extends Equatable { authenticatedUser: authenticatedUser, ); - const AuthenticationState.reauthenticated(AuthenticatedUser authenticatedUser) - : this._( - status: AuthenticationStatus.reauthenticated, - authenticatedUser: authenticatedUser, - ); - const AuthenticationState.unauthenticated() : this._(status: AuthenticationStatus.unauthenticated); @@ -42,12 +36,10 @@ enum AuthenticationStatus { unknown, authenticated, unauthenticated, - reauthenticated } extension AuthenticationStatusIs on AuthenticationStatus { bool get isUnknown => this == AuthenticationStatus.unknown; bool get isAuthenticated => this == AuthenticationStatus.authenticated; bool get isUnauthenticated => this == AuthenticationStatus.unauthenticated; - bool get isReauthenticated => this == AuthenticationStatus.reauthenticated; } diff --git a/lib/features/login/data/datasources/account_remote_data_source.dart b/lib/features/login/data/datasources/account_remote_data_source.dart index 1c9145f1d..3b08b0a84 100644 --- a/lib/features/login/data/datasources/account_remote_data_source.dart +++ b/lib/features/login/data/datasources/account_remote_data_source.dart @@ -49,8 +49,6 @@ class AccountRemoteDataSource { email: email, encodedPasscode: encodedPasscode, token: result.token!, - lastLogin: none(), - sessionTimeout: none(), ), ), ); diff --git a/lib/features/login/presentation/pages/re_login_page.dart b/lib/features/login/presentation/pages/re_login_page.dart deleted file mode 100644 index 6b2665f90..000000000 --- a/lib/features/login/presentation/pages/re_login_page.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:coffeecard/core/ignore_value.dart'; -import 'package:coffeecard/core/widgets/components/loading_overlay.dart'; -import 'package:coffeecard/core/widgets/components/rounded_button.dart'; -import 'package:coffeecard/core/widgets/fast_slide_transition.dart'; -import 'package:coffeecard/features/login/presentation/cubit/login_cubit.dart'; -import 'package:coffeecard/features/login/presentation/pages/forgot_passcode_page.dart'; -import 'package:coffeecard/features/login/presentation/pages/login_page_base.dart'; -import 'package:coffeecard/features/login/presentation/pages/login_page_email.dart'; -import 'package:coffeecard/features/login/presentation/widgets/login_passcode_dots.dart'; -import 'package:coffeecard/features/login/presentation/widgets/numpad/numpad.dart'; -import 'package:coffeecard/service_locator.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ReLoginPage extends StatefulWidget { - const ReLoginPage({required this.email, required this.navigatorKey}); - - final String email; - final GlobalKey navigatorKey; - - static Route routeWith({ - required String email, - required GlobalKey navigatorKey, - }) { - return FastSlideTransition( - child: ReLoginPage( - email: email, - navigatorKey: navigatorKey, - ), - ); - } - - @override - State createState() => _ReLoginPageState(); -} - -class _ReLoginPageState extends State { - void _forgotPasscode(BuildContext context) { - final _ = Navigator.push( - context, - ForgotPasscodePage.routeWith(initialValue: widget.email), - ); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => LoginCubit( - email: widget.email, - loginUser: sl(), - resendEmail: sl(), - authenticationCubit: sl(), - firebaseAnalyticsEventLogging: sl(), - ), - child: BlocConsumer( - listenWhen: (previous, current) => - previous is LoginLoading || current is LoginLoading, - listener: (context, state) { - if (state is LoginLoading) { - ignoreValue(LoadingOverlay.show(context)); - } else { - LoadingOverlay.hide(context); - } - }, - buildWhen: (_, current) => current is! LoginLoading, - builder: (context, state) { - final passcode = (state is LoginTypingPasscode) ? state.passcode : ''; - - return LoginPageBase( - resizeToAvoidBottomInset: false, - title: widget.email, - inputWidget: LoginPasscodeDots( - passcodeLength: passcode.length, - hasError: state is LoginError, - ), - defaultHint: 'Session expired. Please sign in again', - error: state is LoginError ? state.errorMessage : null, - ctaChildren: [ - RoundedButton( - text: 'Sign out', - onTap: () { - widget.navigatorKey.currentState! - .pushAndRemoveUntil( - LoginPageEmail.routeFromLogout, - (_) => false, - ) - .ignore(); - }, - ), - ], - bottomWidget: Numpad(forgotPasscodeAction: _forgotPasscode), - ); - }, - ), - ); - } -} diff --git a/lib/features/redirection/redirection_router.dart b/lib/features/redirection/redirection_router.dart index 32a55e2c0..bafbdae9b 100644 --- a/lib/features/redirection/redirection_router.dart +++ b/lib/features/redirection/redirection_router.dart @@ -3,7 +3,6 @@ import 'package:coffeecard/core/widgets/pages/home_page.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/login/presentation/pages/login_page_email.dart'; -import 'package:coffeecard/features/login/presentation/pages/re_login_page.dart'; import 'package:coffeecard/features/product/domain/entities/purchasable_products.dart'; import 'package:coffeecard/features/product/presentation/cubit/product_cubit.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; @@ -49,21 +48,11 @@ class _MainRedirectionRouterState extends State { final authenticationLoaded = !authenticationStatus.isUnknown; final environmentLoaded = environmentState is EnvironmentLoaded; if (authenticationLoaded && environmentLoaded) { - handleAuthentication( - authenticationStatus, - authenticationCubit.state.authenticatedUser?.email, - ); + handleAuthentication(authenticationStatus); } } - Future handleAuthentication( - AuthenticationStatus status, - String? email, - ) async { - if (status.isReauthenticated) { - return promptRelogin(email!); - } - + Future handleAuthentication(AuthenticationStatus status) async { // If no user credentials are stored, redirect to login page. if (!status.isAuthenticated) { return redirectToLogin(); @@ -111,17 +100,6 @@ class _MainRedirectionRouterState extends State { }); } - void promptRelogin(String email) { - final route = ReLoginPage.routeWith( - email: email, - navigatorKey: widget.navigatorKey, - ); - - widget.navigatorKey.currentState! - .pushAndRemoveUntil(route, (_) => false) - .ignore(); - } - /// Redirects the user to the login page based. /// The route (animation) is determined by the [firstNavigation] flag. void redirectToLogin() { diff --git a/lib/features/session/data/datasources/session_local_data_source.dart b/lib/features/session/data/datasources/session_local_data_source.dart new file mode 100644 index 000000000..4fa4ad62c --- /dev/null +++ b/lib/features/session/data/datasources/session_local_data_source.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:coffeecard/features/session/data/models/session_details_model.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:logger/logger.dart'; + +class SessionLocalDataSource { + static const _sessionKey = 'session'; + + final FlutterSecureStorage storage; + final Logger logger; + + const SessionLocalDataSource({ + required this.storage, + required this.logger, + }); + + Future saveSessionDetails( + SessionDetailsModel sessionDetails, + ) async { + await storage.write( + key: _sessionKey, + value: json.encode(sessionDetails), + ); + + logger.d('$sessionDetails added to storage'); + } + + Future> getSessionDetails() async { + final jsonString = await storage.read(key: _sessionKey); + + if (jsonString == null) { + return const None(); + } + + final sessionDetails = SessionDetailsModel.fromJson( + json.decode(jsonString) as Map, + ); + + return Some(sessionDetails); + } +} diff --git a/lib/features/session/data/models/session_details_model.dart b/lib/features/session/data/models/session_details_model.dart new file mode 100644 index 000000000..5c504227a --- /dev/null +++ b/lib/features/session/data/models/session_details_model.dart @@ -0,0 +1,60 @@ +import 'package:coffeecard/features/session/domain/entities/session_details.dart'; +import 'package:fpdart/fpdart.dart'; + +class SessionDetailsModel extends SessionDetails { + const SessionDetailsModel({ + required super.sessionTimeout, + required super.lastLogin, + }); + + factory SessionDetailsModel.fromJson(Map json) { + return SessionDetailsModel( + sessionTimeout: _parseOption(json, 'session_timeout', _parseDuration), + lastLogin: _parseOption(json, 'last_login', DateTime.parse), + ); + } + + Map toJson() { + return { + 'last_login': _optionToString(lastLogin), + 'session_timeout': _optionToString(sessionTimeout), + }; + } + + static String? _optionToString(Option option) { + return option.match(() => 'null', (value) => value.toString()); + } + + static Option _parseOption( + Map m, + String key, + T Function(String) callback, + ) { + if (!m.containsKey(key)) { + return none(); + } + + final val = m[key] as String; + + if (val == 'null') { + return none(); + } + + return Some(callback(val)); + } + + static Duration _parseDuration(String s) { + int hours = 0; + int minutes = 0; + + final parts = s.split(':'); + if (parts.length > 2) { + hours = int.parse(parts[parts.length - 3]); + } + if (parts.length > 1) { + minutes = int.parse(parts[parts.length - 2]); + } + + return Duration(hours: hours, minutes: minutes); + } +} diff --git a/lib/features/session/domain/entities/session_details.dart b/lib/features/session/domain/entities/session_details.dart new file mode 100644 index 000000000..f3ca49251 --- /dev/null +++ b/lib/features/session/domain/entities/session_details.dart @@ -0,0 +1,12 @@ +import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; + +class SessionDetails extends Equatable { + final Option sessionTimeout; + final Option lastLogin; + + const SessionDetails({required this.sessionTimeout, required this.lastLogin}); + + @override + List get props => [sessionTimeout, lastLogin]; +} diff --git a/lib/features/session/domain/usecases/get_session_details.dart b/lib/features/session/domain/usecases/get_session_details.dart new file mode 100644 index 000000000..7b5336797 --- /dev/null +++ b/lib/features/session/domain/usecases/get_session_details.dart @@ -0,0 +1,13 @@ +import 'package:coffeecard/features/session/data/datasources/session_local_data_source.dart'; +import 'package:coffeecard/features/session/domain/entities/session_details.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetSessionDetails { + final SessionLocalDataSource dataSource; + + GetSessionDetails({required this.dataSource}); + + Future> call() { + return dataSource.getSessionDetails(); + } +} diff --git a/lib/features/session/domain/usecases/save_session_details.dart b/lib/features/session/domain/usecases/save_session_details.dart new file mode 100644 index 000000000..907c595a7 --- /dev/null +++ b/lib/features/session/domain/usecases/save_session_details.dart @@ -0,0 +1,18 @@ +import 'package:coffeecard/features/session/data/datasources/session_local_data_source.dart'; +import 'package:coffeecard/features/session/data/models/session_details_model.dart'; +import 'package:fpdart/fpdart.dart'; + +class SaveSessionDetails { + final SessionLocalDataSource dataSource; + + SaveSessionDetails({required this.dataSource}); + + Future call({ + required Option sessionTimeout, + required Option lastLogin, + }) async { + return dataSource.saveSessionDetails( + SessionDetailsModel(sessionTimeout: sessionTimeout, lastLogin: lastLogin), + ); + } +} diff --git a/lib/features/session/presentation/cubit/session_timeout_cubit.dart b/lib/features/session/presentation/cubit/session_timeout_cubit.dart new file mode 100644 index 000000000..fe53d2fbf --- /dev/null +++ b/lib/features/session/presentation/cubit/session_timeout_cubit.dart @@ -0,0 +1,51 @@ +import 'package:bloc/bloc.dart'; +import 'package:coffeecard/features/session/domain/usecases/get_session_details.dart'; +import 'package:coffeecard/features/session/domain/usecases/save_session_details.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; + +part 'session_timeout_state.dart'; + +typedef SessionTimeout = (String, Duration?); + +const List entries = [ + ('2 seconds', Duration(seconds: 2)), + ('2 hours', Duration(hours: 2)), + ('Never', null), +]; + +class SessionTimeoutCubit extends Cubit { + final GetSessionDetails getSessionDetails; + final SaveSessionDetails saveSessionDetails; + + SessionTimeoutCubit({ + required this.getSessionDetails, + required this.saveSessionDetails, + }) : super( + SessionTimeoutState( + entries: entries, + selected: entries.first, + ), + ); + + SessionTimeout selected() => state.selected; + + Future setSelected(SessionTimeout sessionTimeout) async { + final e = entries.firstWhere((element) => element == sessionTimeout); + + final sessionDetails = await getSessionDetails(); + + sessionDetails.map( + (sessionDetails) async => await saveSessionDetails( + lastLogin: sessionDetails.lastLogin, + sessionTimeout: some(e.$2), + ), + ); + + emit( + state.copyWith( + selected: e, + ), + ); + } +} diff --git a/lib/features/settings/presentation/cubit/session_timeout_state.dart b/lib/features/session/presentation/cubit/session_timeout_state.dart similarity index 100% rename from lib/features/settings/presentation/cubit/session_timeout_state.dart rename to lib/features/session/presentation/cubit/session_timeout_state.dart diff --git a/lib/features/settings/presentation/widgets/session_timeout_dropdown.dart b/lib/features/session/presentation/widgets/session_timeout_dropdown.dart similarity index 85% rename from lib/features/settings/presentation/widgets/session_timeout_dropdown.dart rename to lib/features/session/presentation/widgets/session_timeout_dropdown.dart index b9e976210..4ad4ac388 100644 --- a/lib/features/settings/presentation/widgets/session_timeout_dropdown.dart +++ b/lib/features/session/presentation/widgets/session_timeout_dropdown.dart @@ -1,7 +1,8 @@ import 'package:coffeecard/core/styles/app_colors.dart'; import 'package:coffeecard/core/styles/app_text_styles.dart'; import 'package:coffeecard/core/widgets/components/dropdown.dart'; -import 'package:coffeecard/features/settings/presentation/cubit/session_timeout_cubit.dart'; +import 'package:coffeecard/features/session/presentation/cubit/session_timeout_cubit.dart'; +import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,7 +12,7 @@ class SessionTimeoutDropdown extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SessionTimeoutCubit(), + create: (_) => sl(), child: BlocBuilder( builder: (context, state) => Dropdown( loading: false, diff --git a/lib/features/settings/presentation/cubit/session_timeout_cubit.dart b/lib/features/settings/presentation/cubit/session_timeout_cubit.dart deleted file mode 100644 index e2109383c..000000000 --- a/lib/features/settings/presentation/cubit/session_timeout_cubit.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -part 'session_timeout_state.dart'; - -typedef SessionTimeout = (String, Duration?); - -const List entries = [ - ('2 seconds', Duration(seconds: 2)), - ('2 hours', Duration(hours: 2)), - ('Never', null), -]; - -class SessionTimeoutCubit extends Cubit { - SessionTimeoutCubit() - : super(SessionTimeoutState(entries: entries, selected: entries.first)); - - SessionTimeout selected() => state.selected; - - Future setSelected(SessionTimeout sessionTimeout) async { - emit( - state.copyWith( - selected: entries.firstWhere((element) => element == sessionTimeout), - ), - ); - } -} diff --git a/lib/features/settings/presentation/widgets/sections/account_section.dart b/lib/features/settings/presentation/widgets/sections/account_section.dart index 6879192e4..926068748 100644 --- a/lib/features/settings/presentation/widgets/sections/account_section.dart +++ b/lib/features/settings/presentation/widgets/sections/account_section.dart @@ -3,9 +3,9 @@ import 'package:coffeecard/core/styles/app_colors.dart'; import 'package:coffeecard/core/widgets/components/dialog.dart'; import 'package:coffeecard/core/widgets/components/helpers/shimmer_builder.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; +import 'package:coffeecard/features/session/presentation/widgets/session_timeout_dropdown.dart'; import 'package:coffeecard/features/settings/presentation/pages/change_email_page.dart'; import 'package:coffeecard/features/settings/presentation/widgets/change_passcode_flow.dart'; -import 'package:coffeecard/features/settings/presentation/widgets/session_timeout_dropdown.dart'; import 'package:coffeecard/features/settings/presentation/widgets/setting_value_text.dart'; import 'package:coffeecard/features/settings/presentation/widgets/settings_group.dart'; import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 27d184aee..251886fbd 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -46,6 +46,10 @@ import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dar import 'package:coffeecard/features/register/data/datasources/register_remote_data_source.dart'; import 'package:coffeecard/features/register/domain/usecases/register_user.dart'; import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; +import 'package:coffeecard/features/session/data/datasources/session_local_data_source.dart'; +import 'package:coffeecard/features/session/domain/usecases/get_session_details.dart'; +import 'package:coffeecard/features/session/domain/usecases/save_session_details.dart'; +import 'package:coffeecard/features/session/presentation/cubit/session_timeout_cubit.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/features/ticket/domain/usecases/consume_ticket.dart'; import 'package:coffeecard/features/ticket/domain/usecases/load_tickets.dart'; @@ -105,6 +109,7 @@ void initExternal() { } void initFeatures() { + initSession(); initAuthentication(); initOpeningHours(); initOccupation(); @@ -121,6 +126,28 @@ void initFeatures() { initRegister(); } +void initSession() { + // bloc + sl.registerFactory( + () => SessionTimeoutCubit( + getSessionDetails: sl(), + saveSessionDetails: sl(), + ), + ); + + // use case + sl.registerFactory(() => GetSessionDetails(dataSource: sl())); + sl.registerFactory(() => SaveSessionDetails(dataSource: sl())); + + // repository + sl.registerLazySingleton( + () => SessionLocalDataSource( + storage: sl(), + logger: sl(), + ), + ); +} + void initAuthentication() { // bloc sl.registerLazySingleton( @@ -128,6 +155,8 @@ void initAuthentication() { clearAuthenticatedUser: sl(), saveAuthenticatedUser: sl(), getAuthenticatedUser: sl(), + getSessionDetails: sl(), + saveSessionDetails: sl(), dateService: sl(), ), ); diff --git a/test/features/authentication/data/datasources/authentication_local_data_source_test.dart b/test/features/authentication/data/datasources/authentication_local_data_source_test.dart index 85f0c7195..a5796bb66 100644 --- a/test/features/authentication/data/datasources/authentication_local_data_source_test.dart +++ b/test/features/authentication/data/datasources/authentication_local_data_source_test.dart @@ -24,12 +24,10 @@ void main() { AuthenticationLocalDataSource(storage: storage, logger: logger); }); - final user = AuthenticatedUserModel( + const user = AuthenticatedUserModel( email: 'email', token: 'token', encodedPasscode: 'encodedPasscode', - sessionTimeout: none(), - lastLogin: none(), ); group('saveAuthenticatedUser', () { @@ -114,12 +112,10 @@ void main() { await dataSource.updateToken(token); // assert - final expected = AuthenticatedUserModel( + const expected = AuthenticatedUserModel( email: 'email', token: token, encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ); verify( diff --git a/test/features/authentication/data/intercepters/authentication_interceptor_test.dart b/test/features/authentication/data/intercepters/authentication_interceptor_test.dart index 6647d6d84..c6cd76848 100644 --- a/test/features/authentication/data/intercepters/authentication_interceptor_test.dart +++ b/test/features/authentication/data/intercepters/authentication_interceptor_test.dart @@ -23,13 +23,11 @@ void main() { final mockSecureStorage = MockAuthenticationLocalDataSource(); when(mockSecureStorage.getAuthenticatedUser()).thenAnswer( - (_) async => Some( + (_) async => const Some( AuthenticatedUserModel( email: 'email', token: token, encodedPasscode: 'encodedPasscode', - sessionTimeout: none(), - lastLogin: none(), ), ), ); diff --git a/test/features/authentication/data/models/authenticated_user_model_test.dart b/test/features/authentication/data/models/authenticated_user_model_test.dart index 7b50d5904..5d4096b6d 100644 --- a/test/features/authentication/data/models/authenticated_user_model_test.dart +++ b/test/features/authentication/data/models/authenticated_user_model_test.dart @@ -2,25 +2,14 @@ import 'dart:convert'; import 'package:coffeecard/features/authentication/data/models/authenticated_user_model.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; import '../../../../fixtures/fixture_reader.dart'; void main() { - final model = AuthenticatedUserModel( + const model = AuthenticatedUserModel( email: 'email', token: 'token', encodedPasscode: 'passcode', - sessionTimeout: some(const Duration(hours: 2)), - lastLogin: some(DateTime.parse('2012-02-27')), - ); - - final modelNullFields = AuthenticatedUserModel( - email: 'email', - token: 'token', - encodedPasscode: 'passcode', - lastLogin: none(), - sessionTimeout: none(), ); group('fromJson', () { @@ -36,20 +25,6 @@ void main() { // assert expect(actual, model); }); - - test('should return model without lastLogin and sessionTimeout', () { - // arrange - final jsonString = - fixture('authenticated_user/authenticated_user_null_fields.json'); - - // act - final actual = AuthenticatedUserModel.fromJson( - json.decode(jsonString) as Map, - ); - - // assert - expect(actual, modelNullFields); - }); }); group('toJson', () { test('should return map', () { @@ -61,24 +36,6 @@ void main() { 'email': 'email', 'token': 'token', 'passcode': 'passcode', - 'last_login': '2012-02-27 00:00:00.000', - 'session_timeout': '2:00:00.000000', - }; - - expect(actual, expected); - }); - - test('should return map with null fields', () { - // act - final actual = modelNullFields.toJson(); - - // assert - final expected = { - 'email': 'email', - 'token': 'token', - 'passcode': 'passcode', - 'last_login': 'null', - 'session_timeout': 'null', }; expect(actual, expected); diff --git a/test/features/authentication/presentation/cubit/authentication_cubit_test.dart b/test/features/authentication/presentation/cubit/authentication_cubit_test.dart index 378a94b98..309592fcb 100644 --- a/test/features/authentication/presentation/cubit/authentication_cubit_test.dart +++ b/test/features/authentication/presentation/cubit/authentication_cubit_test.dart @@ -5,6 +5,8 @@ import 'package:coffeecard/features/authentication/domain/usecases/clear_authent import 'package:coffeecard/features/authentication/domain/usecases/get_authenticated_user.dart'; import 'package:coffeecard/features/authentication/domain/usecases/save_authenticated_user.dart'; import 'package:coffeecard/features/authentication/presentation/cubits/authentication_cubit.dart'; +import 'package:coffeecard/features/session/domain/usecases/get_session_details.dart'; +import 'package:coffeecard/features/session/domain/usecases/save_session_details.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; @@ -17,6 +19,8 @@ import 'authentication_cubit_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), MockSpec(), ], ) @@ -25,27 +29,31 @@ void main() { late MockGetAuthenticatedUser getAuthenticatedUser; late MockClearAuthenticatedUser clearAuthenticatedUser; late MockSaveAuthenticatedUser saveAuthenticatedUser; + late MockGetSessionDetails getSessionDetails; + late MockSaveSessionDetails saveSessionDetails; late MockDateService dateService; setUp(() { getAuthenticatedUser = MockGetAuthenticatedUser(); clearAuthenticatedUser = MockClearAuthenticatedUser(); saveAuthenticatedUser = MockSaveAuthenticatedUser(); + getSessionDetails = MockGetSessionDetails(); + saveSessionDetails = MockSaveSessionDetails(); dateService = MockDateService(); cubit = AuthenticationCubit( getAuthenticatedUser: getAuthenticatedUser, clearAuthenticatedUser: clearAuthenticatedUser, saveAuthenticatedUser: saveAuthenticatedUser, + getSessionDetails: getSessionDetails, + saveSessionDetails: saveSessionDetails, dateService: dateService, ); }); - final testUser = AuthenticatedUser( + const testUser = AuthenticatedUser( email: 'email', token: 'token', encodedPasscode: 'encodedPasscode', - lastLogin: some(DateTime.parse('2012-02-27')), - sessionTimeout: none(), ); test('initial state is AuthenticationState.unknown', () { @@ -64,10 +72,10 @@ void main() { blocTest( 'should emit [Authenticated] when a user is stored', build: () => cubit, - setUp: () => - when(getAuthenticatedUser()).thenAnswer((_) async => Some(testUser)), + setUp: () => when(getAuthenticatedUser()) + .thenAnswer((_) async => const Some(testUser)), act: (_) => cubit.appStarted(), - expect: () => [AuthenticationState.authenticated(testUser)], + expect: () => [const AuthenticationState.authenticated(testUser)], ); }); @@ -79,15 +87,14 @@ void main() { when(dateService.currentDateTime) .thenReturn(DateTime.parse('2012-02-27')); - final testUser = AuthenticatedUser( + const testUser = AuthenticatedUser( email: 'email', token: 'token', encodedPasscode: 'encodedPasscode', - lastLogin: some(DateTime.parse('2012-02-20')), - sessionTimeout: some(const Duration(hours: 2)), ); - when(getAuthenticatedUser()).thenAnswer((_) async => Some(testUser)); + when(getAuthenticatedUser()) + .thenAnswer((_) async => const Some(testUser)); }, act: (_) => cubit.appStarted(), expect: () => [const AuthenticationState.unauthenticated()], @@ -105,14 +112,12 @@ void main() { ), setUp: () => when(dateService.currentDateTime) .thenReturn(DateTime.parse('2012-02-27')), - expect: () => [AuthenticationState.authenticated(testUser)], + expect: () => [const AuthenticationState.authenticated(testUser)], verify: (_) => verify( saveAuthenticatedUser( email: testUser.email, token: testUser.token, encodedPasscode: testUser.encodedPasscode, - lastLogin: some(DateTime.parse('2012-02-27')), - sessionTimeout: none(), ), ), ); @@ -127,23 +132,4 @@ void main() { verify: (_) => verify(clearAuthenticatedUser()), ); }); - - group('saveSessionTimeout', () { - blocTest( - 'should update sessionTimeout for user', - build: () => cubit, - act: (_) => cubit.saveSessionTimeout(const Duration(hours: 2)), - setUp: () => - when(getAuthenticatedUser()).thenAnswer((_) async => Some(testUser)), - verify: (_) => verify( - saveAuthenticatedUser( - email: testUser.email, - token: testUser.token, - encodedPasscode: testUser.encodedPasscode, - lastLogin: testUser.lastLogin, - sessionTimeout: some(const Duration(hours: 2)), - ), - ), - ); - }); } diff --git a/test/features/login/data/datasources/account_remote_data_source_test.dart b/test/features/login/data/datasources/account_remote_data_source_test.dart index 2b47639dd..db7624160 100644 --- a/test/features/login/data/datasources/account_remote_data_source_test.dart +++ b/test/features/login/data/datasources/account_remote_data_source_test.dart @@ -67,13 +67,11 @@ void main() { // assert expect( actual, - Right( + const Right( AuthenticatedUser( email: 'email', token: 'token', encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ), ), ); diff --git a/test/features/reactivation/data/reactivation_authenticator_test.dart b/test/features/reactivation/data/reactivation_authenticator_test.dart index 4c58c4753..777271c08 100644 --- a/test/features/reactivation/data/reactivation_authenticator_test.dart +++ b/test/features/reactivation/data/reactivation_authenticator_test.dart @@ -124,12 +124,10 @@ void main() { when(secureStorage.getAuthenticatedUser()).thenAnswer( (_) async => some( - AuthenticatedUserModel( + const AuthenticatedUserModel( email: email, token: token, encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ), ), ); @@ -178,24 +176,20 @@ void main() { when(secureStorage.getAuthenticatedUser()).thenAnswer( (_) async => some( - AuthenticatedUserModel( + const AuthenticatedUserModel( email: email, token: oldToken, encodedPasscode: encodedPasscode, - lastLogin: none(), - sessionTimeout: none(), ), ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( (_) async => right( - AuthenticatedUser( + const AuthenticatedUser( email: email, token: newToken, encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ), ), ); @@ -237,12 +231,10 @@ void main() { when(secureStorage.getAuthenticatedUser()).thenAnswer( (_) async => some( - AuthenticatedUserModel( + const AuthenticatedUserModel( email: email, token: oldToken, encodedPasscode: encodedPasscode, - lastLogin: none(), - sessionTimeout: none(), ), ), ); @@ -253,8 +245,6 @@ void main() { email: email, token: getNewToken(), encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ), ), ); @@ -293,24 +283,20 @@ void main() { when(secureStorage.getAuthenticatedUser()).thenAnswer( (_) async => some( - AuthenticatedUserModel( + const AuthenticatedUserModel( email: email, token: newToken, encodedPasscode: encodedPasscode, - lastLogin: none(), - sessionTimeout: none(), ), ), ); when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( (_) async => right( - AuthenticatedUser( + const AuthenticatedUser( email: email, token: newToken, encodedPasscode: 'encodedPasscode', - lastLogin: none(), - sessionTimeout: none(), ), ), ); diff --git a/test/fixtures/authenticated_user/authenticated_user.json b/test/fixtures/authenticated_user/authenticated_user.json index 22bfb3a23..66e9f9975 100644 --- a/test/fixtures/authenticated_user/authenticated_user.json +++ b/test/fixtures/authenticated_user/authenticated_user.json @@ -1,7 +1,5 @@ { "email": "email", "token": "token", - "passcode": "passcode", - "last_login": "2012-02-27 00:00:00.000", - "session_timeout": "2:00:00.000000" + "passcode": "passcode" } \ No newline at end of file