Skip to content
Merged
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
23 changes: 23 additions & 0 deletions lib/core/di/di.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';

import '../../features/auth/data/remote/auth_api_client.dart';
import '../../features/auth/data/repositories/auth_repository_impl.dart';
Expand Down Expand Up @@ -42,9 +46,12 @@ import '../../features/workouts/execution/data/repositories/workout_execution_re
import '../../features/workouts/execution/domain/repositories/workout_execution_repository.dart';
import '../../features/workouts/overview/data/repositories/workouts_overview_repository_impl.dart';
import '../../features/workouts/overview/domain/repositories/workouts_overview_repository.dart';
import '../network/api_paths.dart';
import '../network/dio_setup.dart';
import '../services/fitness_start_progress_storage/fitness_start_progress_storage.dart';
import '../services/fitness_start_progress_storage/hive_fitness_start_progress_storage.dart';
import '../services/guest_session_storage/cookie_jar_guest_session_storage.dart';
import '../services/guest_session_storage/guest_session_storage.dart';
import '../services/network/network_service.dart';
import '../services/network/network_service_impl.dart';
import '../services/token_storage/secure_token_storage.dart';
Expand All @@ -63,6 +70,13 @@ Future<void> setupDI() async {
final fitnessStartProgressBox = await Hive.openBox<dynamic>(
HiveFitnessStartProgressStorage.boxName,
);
final supportDirectory = await getApplicationSupportDirectory();
final cookiesDirectory = Directory(
'${supportDirectory.path}/guest_cookies',
);
final cookieJar = PersistCookieJar(
storage: FileStorage(cookiesDirectory.path),
);

// Logger
di.registerLazySingleton<Logger>(() => createLogger());
Expand Down Expand Up @@ -90,12 +104,20 @@ Future<void> setupDI() async {
() => HiveFitnessStartProgressStorage(fitnessStartProgressBox),
dispose: (_) => fitnessStartProgressBox.close(),
);
di.registerLazySingleton<CookieJar>(() => cookieJar);
di.registerLazySingleton<GuestSessionStorage>(
() => CookieJarGuestSessionStorage(
di<CookieJar>(),
Uri.parse(ApiPaths.baseUrl),
),
);

// Authentication
di.registerLazySingleton<Dio>(
() => createDioClient(
logger: di<AppLogger>(),
tokenStorage: di<TokenStorage>(),
cookieJar: di<CookieJar>(),
),
);
di.registerLazySingleton<AuthApiClient>(() => AuthApiClient(di<Dio>()));
Expand Down Expand Up @@ -167,6 +189,7 @@ Future<void> setupDI() async {
di<AuthRepository>(),
di<TokenStorage>(),
di<FitnessStartProgressStorage>(),
di<GuestSessionStorage>(),
di<AppLogger>(),
),
dispose: (cubit) => cubit.close(),
Expand Down
5 changes: 5 additions & 0 deletions lib/core/network/dio_setup.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter/foundation.dart';

import '../services/token_storage/token_storage.dart';
Expand All @@ -11,13 +13,16 @@ import 'interceptors/logging_interceptor.dart';
Dio createDioClient({
required AppLogger logger,
required TokenStorage tokenStorage,
required CookieJar cookieJar,
}) {
/// Main Dio instance for all API calls.
final dio = Dio(_createDioBaseOptions());

/// Separate Dio instance for token refresh only to avoid interceptor loops.
final refreshDio = Dio(_createDioBaseOptions());

dio.interceptors.add(CookieManager(cookieJar));

dio.interceptors.add(
AuthInterceptor(
tokenStorage,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:cookie_jar/cookie_jar.dart';

import 'guest_session_storage.dart';

/// CookieJar-backed implementation of [GuestSessionStorage].
final class CookieJarGuestSessionStorage implements GuestSessionStorage {
final CookieJar _cookieJar;
final Uri _baseUri;

/// Creates an instance of [CookieJarGuestSessionStorage].
CookieJarGuestSessionStorage(this._cookieJar, this._baseUri);

@override
Future<void> clear() async {
await _cookieJar.delete(_baseUri, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// Abstraction for clearing persisted guest backend session data.
abstract interface class GuestSessionStorage {
/// Clears any guest session data persisted locally.
Future<void> clear();
}
21 changes: 20 additions & 1 deletion lib/features/auth/presentation/cubits/auth_session_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../core/failures/feature/auth/auth_failure.dart';
import '../../../../core/result/result.dart';
import '../../../../core/services/fitness_start_progress_storage/fitness_start_progress_storage.dart';
import '../../../../core/services/guest_session_storage/guest_session_storage.dart';
import '../../../../core/services/token_storage/token_storage.dart';
import '../../../../core/utils/logger/app_logger.dart';
import '../../domain/entities/user.dart';
Expand All @@ -25,6 +26,9 @@ final class AuthSessionCubit extends Cubit<AuthSessionState> {
/// Storage for persisted guest Fitness Start progress.
final FitnessStartProgressStorage _fitnessStartProgressStorage;

/// Storage for persisted guest backend session cookies.
final GuestSessionStorage _guestSessionStorage;

/// Logger for non-fatal session cleanup errors.
final AppLogger _logger;

Expand All @@ -33,6 +37,7 @@ final class AuthSessionCubit extends Cubit<AuthSessionState> {
this._repository,
this._tokenStorage,
this._fitnessStartProgressStorage,
this._guestSessionStorage,
this._logger,
) : super(const AuthSessionState.initial());

Expand Down Expand Up @@ -122,8 +127,22 @@ final class AuthSessionCubit extends Cubit<AuthSessionState> {
}
}

Future<bool> _clearGuestSessionSafely() async {
try {
await _guestSessionStorage.clear();
return true;
} catch (e, s) {
_logger.e('Failed to clear guest session cookies.', e, s);
return false;
}
}

Future<bool> _clearGuestDataSafely() async {
return _clearGuestProgressSafely();
final results = await Future.wait([
_clearGuestProgressSafely(),
_clearGuestSessionSafely(),
]);
return results.every((isSuccess) => isSuccess);
}

Future<void> _clearGuestDataAfterAuthSuccess() async {
Expand Down
24 changes: 24 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cookie_jar:
dependency: "direct main"
description:
name: cookie_jar
sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff"
url: "https://pub.dev"
source: hosted
version: "4.0.9"
coverage:
dependency: transitive
description:
Expand Down Expand Up @@ -281,6 +289,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.9.1"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
sha256: "0db1a7b997a0455e488ac35744c68eed3f2a4280d3ab531835a65641b0a08744"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
dio_web_adapter:
dependency: transitive
description:
Expand Down Expand Up @@ -1229,6 +1245,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
uri:
dependency: transitive
description:
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ dependencies:
cached_network_image: ^3.4.1
carousel_slider: ^5.1.1
connectivity_plus: ^7.0.0
cookie_jar: ^4.0.8
dio: ^5.9.1
dio_cookie_manager: ^3.3.0
envied: ^1.3.3
equatable: ^2.0.8
flutter:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:mockito/mockito.dart';
import 'package:moveup_flutter/core/failures/feature/auth/auth_failure.dart';
import 'package:moveup_flutter/core/result/result.dart';
import 'package:moveup_flutter/core/services/fitness_start_progress_storage/fitness_start_progress_storage.dart';
import 'package:moveup_flutter/core/services/guest_session_storage/guest_session_storage.dart';
import 'package:moveup_flutter/core/services/token_storage/token_storage.dart';
import 'package:moveup_flutter/core/utils/logger/app_logger.dart';
import 'package:moveup_flutter/features/auth/domain/entities/user.dart';
Expand All @@ -17,12 +18,14 @@ import 'auth_session_cubit_test.mocks.dart';
MockSpec<AuthRepository>(),
MockSpec<TokenStorage>(),
MockSpec<FitnessStartProgressStorage>(),
MockSpec<GuestSessionStorage>(),
MockSpec<AppLogger>(),
])
void main() {
late MockAuthRepository repository;
late MockTokenStorage tokenStorage;
late MockFitnessStartProgressStorage progressStorage;
late MockGuestSessionStorage guestSessionStorage;
late MockAppLogger logger;
late AuthSessionCubit authSessionCubit;

Expand All @@ -36,17 +39,20 @@ void main() {
repository = MockAuthRepository();
tokenStorage = MockTokenStorage();
progressStorage = MockFitnessStartProgressStorage();
guestSessionStorage = MockGuestSessionStorage();
logger = MockAppLogger();
authSessionCubit = AuthSessionCubit(
repository,
tokenStorage,
progressStorage,
guestSessionStorage,
logger,
);
provideDummy<Result<User, AuthFailure>>(const Success(user));
when(progressStorage.hasCompletedProgress()).thenAnswer((_) async => false);
when(progressStorage.saveCompleted()).thenAnswer((_) async {});
when(progressStorage.clear()).thenAnswer((_) async {});
when(guestSessionStorage.clear()).thenAnswer((_) async {});
});

group('AuthSessionCubit', () {
Expand All @@ -63,6 +69,7 @@ void main() {
verify(tokenStorage.getAccessToken()).called(1);
verify(progressStorage.hasCompletedProgress()).called(1);
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verifyNever(repository.getCurrentUser());
},
);
Expand All @@ -83,6 +90,7 @@ void main() {
verify(tokenStorage.getAccessToken()).called(1);
verify(progressStorage.hasCompletedProgress()).called(1);
verifyNever(progressStorage.clear());
verifyNever(guestSessionStorage.clear());
verifyNever(repository.getCurrentUser());
},
);
Expand All @@ -104,6 +112,7 @@ void main() {
verify(progressStorage.hasCompletedProgress()).called(1);
verify(logger.e(any, any, any)).called(1);
verifyNever(progressStorage.clear());
verifyNever(guestSessionStorage.clear());
verifyNever(repository.getCurrentUser());
},
);
Expand All @@ -124,6 +133,7 @@ void main() {
verify(tokenStorage.getAccessToken()).called(1);
verify(repository.getCurrentUser()).called(1);
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verifyNever(progressStorage.hasCompletedProgress());
verifyNever(tokenStorage.deleteAccessToken());
},
Expand All @@ -149,6 +159,7 @@ void main() {
verify(repository.getCurrentUser()).called(1);
verify(tokenStorage.deleteAccessToken()).called(1);
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
},
);

Expand Down Expand Up @@ -262,6 +273,7 @@ void main() {
expect: () => const [AuthSessionState.guest()],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
},
);

Expand All @@ -276,6 +288,7 @@ void main() {
expect: () => const <AuthSessionState>[],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verify(logger.e(any, any, any)).called(1);
},
);
Expand All @@ -288,6 +301,7 @@ void main() {
expect: () => const <AuthSessionState>[],
verify: (_) {
verifyNever(progressStorage.clear());
verifyNever(guestSessionStorage.clear());
},
);

Expand Down Expand Up @@ -337,20 +351,22 @@ void main() {
expect: () => const [AuthSessionState.unauthenticated()],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
},
);

blocTest<AuthSessionCubit, AuthSessionState>(
'cancelGuestFlow does not emit unauthenticated when guest progress clear fails',
'cancelGuestFlow does not emit unauthenticated when guest session clear fails',
setUp: () {
when(progressStorage.clear()).thenThrow(Exception('storage_error'));
when(guestSessionStorage.clear()).thenThrow(Exception('storage_error'));
},
build: () => authSessionCubit,
seed: () => const AuthSessionState.guest(),
act: (cubit) => cubit.cancelGuestFlow(),
expect: () => const <AuthSessionState>[],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verify(logger.e(any, any, any)).called(1);
},
);
Expand All @@ -365,6 +381,7 @@ void main() {
expect: () => const [AuthSessionState.authenticated(user)],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
},
);

Expand All @@ -375,6 +392,7 @@ void main() {
expect: () => const [AuthSessionState.unauthenticated()],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verifyNever(repository.getCurrentUser());
verifyNever(tokenStorage.deleteAccessToken());
},
Expand All @@ -390,6 +408,7 @@ void main() {
expect: () => const [AuthSessionState.unauthenticated()],
verify: (_) {
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verify(logger.e(any, any, any)).called(1);
verify(logger.w(any, any, any)).called(1);
},
Expand All @@ -406,6 +425,7 @@ void main() {
verify: (_) {
verify(tokenStorage.deleteAccessToken()).called(1);
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
},
);

Expand All @@ -420,6 +440,7 @@ void main() {
verify: (_) {
verify(tokenStorage.deleteAccessToken()).called(1);
verify(progressStorage.clear()).called(1);
verify(guestSessionStorage.clear()).called(1);
verify(logger.e(any, any, any)).called(1);
},
);
Expand Down
Loading