Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sign user out after 2 hours/Never #514

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/core/strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ abstract final class Strings {
static const deleteAccount = 'Delete account';
static const credits = 'Credits';
static const privacyPolicy = 'Privacy policy';
static const sessionTimeout = 'Session timeout';

static const emailShimmerText = '[email protected]';
static const openingHoursShimmerText = 'Somedays: 8-16';
Expand Down
19 changes: 12 additions & 7 deletions lib/core/widgets/components/dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ import 'package:coffeecard/core/styles/app_colors.dart';
import 'package:coffeecard/core/styles/app_text_styles.dart';
import 'package:flutter/material.dart';

class Dropdown<FilterCategory> extends StatelessWidget {
class Dropdown<T> extends StatelessWidget {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change the intended use case for this widget.

final bool loading;
final FilterCategory value;
final List<DropdownMenuItem<FilterCategory>>? items;
final void Function(FilterCategory?) onChanged;
final T value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give a more descriptive name for the type T

Suggested change
final T value;
final DropdownItem value;

final List<DropdownMenuItem<T>> items;
final void Function(T?) onChanged;
final TextStyle? textStyle;
final Color? dropdownColor;
Comment on lines +10 to +11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make required with no default value. What I suspect you really want is a way to change the colour of the text, in which case you can also change the TextStyle field into a Color field instead.

Suggested change
final TextStyle? textStyle;
final Color? dropdownColor;
final Color textColor;
final Color dropdownColor;


const Dropdown({
required this.loading,
required this.value,
required this.items,
required this.onChanged,
this.textStyle,
this.dropdownColor,
});

Container get _underline {
Expand All @@ -29,17 +33,18 @@ class Dropdown<FilterCategory> extends StatelessWidget {
);
}

Color get _color => loading ? AppColors.lightGray : AppColors.white;
Color get _color =>
loading ? AppColors.lightGray : (dropdownColor ?? AppColors.white);

@override
Widget build(BuildContext context) {
return Theme(
data: ThemeData(disabledColor: AppColors.lightGray),
child: DropdownButton(
dropdownColor: AppColors.secondary,
dropdownColor: dropdownColor ?? AppColors.secondary,
underline: _underline,
icon: Icon(Icons.arrow_drop_down, color: _color),
style: AppTextStyle.buttonText,
style: textStyle ?? AppTextStyle.buttonText,
value: value,
items: items,
onChanged: loading ? null : onChanged,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'package:coffeecard/core/external/date_service.dart';
import 'package:coffeecard/features/authentication/domain/entities/authenticated_user.dart';
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';

part 'authentication_state.dart';

Expand All @@ -14,20 +18,65 @@
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<void> appStarted() async {
final authenticatedUser = await getAuthenticatedUser();

authenticatedUser.match(
() => emit(const AuthenticationState.unauthenticated()),
(authenticatedUser) =>
emit(AuthenticationState.authenticated(authenticatedUser)),
(authenticatedUser) async {
final sessionDetails = await getSessionDetails();

sessionDetails.match(
() => emit(AuthenticationState.authenticated(authenticatedUser)),
(sessionDetails) async {
final sessionExpired = _isSessionExpired(
sessionDetails.lastLogin,
sessionDetails.sessionTimeout,
);

if (sessionExpired) {
await unauthenticated();
return;
}

emit(AuthenticationState.authenticated(authenticatedUser));
},
);
},
);
}

bool _isSessionExpired(
Option<DateTime> lastLogin,
Option<Duration?> sessionTimeout,
) {
return lastLogin.match(
() => false,

Check warning on line 67 in lib/features/authentication/presentation/cubits/authentication_cubit.dart

View check run for this annotation

Codecov / codecov/patch

lib/features/authentication/presentation/cubits/authentication_cubit.dart#L67

Added line #L67 was not covered by tests
(lastLogin) => sessionTimeout.match(
() => false,

Check warning on line 69 in lib/features/authentication/presentation/cubits/authentication_cubit.dart

View check run for this annotation

Codecov / codecov/patch

lib/features/authentication/presentation/cubits/authentication_cubit.dart#L69

Added line #L69 was not covered by tests
(sessionTimeout) {
if (sessionTimeout == null) {
return false;
}

final now = dateService.currentDateTime;
final difference = now.difference(lastLogin);
return difference > sessionTimeout;
},
),
);
}

Expand All @@ -42,6 +91,20 @@
encodedPasscode: encodedPasscode,
);

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(
AuthenticationState.authenticated(
AuthenticatedUser(
Expand All @@ -55,6 +118,20 @@

Future<void> unauthenticated() async {
await clearAuthenticatedUser();

final sessionDetails = await getSessionDetails();

sessionDetails.match(
() async => await saveSessionDetails(
lastLogin: none(),
sessionTimeout: none(),
),
(sessionDetails) async => await saveSessionDetails(
lastLogin: none(),
sessionTimeout: sessionDetails.sessionTimeout,
),
);

emit(const AuthenticationState.unauthenticated());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ class AuthenticationState extends Equatable {
}
}

enum AuthenticationStatus { unknown, authenticated, unauthenticated }
enum AuthenticationStatus {
unknown,
authenticated,
unauthenticated,
}

extension AuthenticationStatusIs on AuthenticationStatus {
bool get isUnknown => this == AuthenticationStatus.unknown;
Expand Down
3 changes: 1 addition & 2 deletions lib/features/redirection/redirection_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ class _MainRedirectionRouterState extends State<MainRedirectionRouter> {
/// Redirects the user to the login page based.
/// The route (animation) is determined by the [firstNavigation] flag.
void redirectToLogin() {
final Route route;
route = firstNavigation
final route = firstNavigation
? LoginPageEmail.routeFromSplash
: LoginPageEmail.routeFromLogout;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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<void> saveSessionDetails(SessionDetailsModel sessionDetails) async {
await storage.write(
key: _sessionKey,
value: json.encode(sessionDetails),
);

logger.d('$sessionDetails added to storage');
}

Future<Option<SessionDetailsModel>> getSessionDetails() async {
final jsonString = await storage.read(key: _sessionKey);

if (jsonString == null) {
return none();
}

final sessionDetails = SessionDetailsModel.fromJson(
json.decode(jsonString) as Map<String, dynamic>,
);

return some(sessionDetails);
}
}
55 changes: 55 additions & 0 deletions lib/features/session/data/models/session_details_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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<String, dynamic> json) {
return SessionDetailsModel(
sessionTimeout: _parseOption(json, 'session_timeout', _parseDuration),
lastLogin: _parseOption(json, 'last_login', DateTime.parse),
);
}

Map<String, dynamic> toJson() {
return {
'last_login': _optionToString(lastLogin),
'session_timeout': _optionToString(sessionTimeout),
};
}

static String? _optionToString<T>(Option<T> option) {
return option.match(() => 'null', (value) => value.toString());
}

static Option<T> _parseOption<T>(
Map<String, dynamic> jsonMap,
String key,
T Function(String) callback,
) {
if (!jsonMap.containsKey(key)) {
return none();

Check warning on line 34 in lib/features/session/data/models/session_details_model.dart

View check run for this annotation

Codecov / codecov/patch

lib/features/session/data/models/session_details_model.dart#L34

Added line #L34 was not covered by tests
}

final val = jsonMap[key] as String;

if (val == 'null') {
return none();
}

return Some(callback(val));
}

static Duration _parseDuration(String duration) {
final parts = duration.split(':');

final hours = int.parse(parts[0]);
final minutes = int.parse(parts[1]);
final seconds = int.parse(parts[2].split('.')[0]);

return Duration(hours: hours, minutes: minutes, seconds: seconds);
}
}
12 changes: 12 additions & 0 deletions lib/features/session/domain/entities/session_details.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:equatable/equatable.dart';
import 'package:fpdart/fpdart.dart';

class SessionDetails extends Equatable {
final Option<Duration?> sessionTimeout;
final Option<DateTime> lastLogin;

const SessionDetails({required this.sessionTimeout, required this.lastLogin});

@override
List<Object?> get props => [sessionTimeout, lastLogin];
}
13 changes: 13 additions & 0 deletions lib/features/session/domain/usecases/get_session_details.dart
Original file line number Diff line number Diff line change
@@ -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<Option<SessionDetails>> call() {
return dataSource.getSessionDetails();
}
}
18 changes: 18 additions & 0 deletions lib/features/session/domain/usecases/save_session_details.dart
Original file line number Diff line number Diff line change
@@ -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<void> call({
required Option<Duration?> sessionTimeout,
required Option<DateTime> lastLogin,
}) async {
return dataSource.saveSessionDetails(
SessionDetailsModel(sessionTimeout: sessionTimeout, lastLogin: lastLogin),
);
}
}
Loading
Loading