diff --git a/lib/core/di/di.dart b/lib/core/di/di.dart index 67ec86cc..115de91c 100644 --- a/lib/core/di/di.dart +++ b/lib/core/di/di.dart @@ -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'; @@ -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'; @@ -63,6 +70,13 @@ Future setupDI() async { final fitnessStartProgressBox = await Hive.openBox( HiveFitnessStartProgressStorage.boxName, ); + final supportDirectory = await getApplicationSupportDirectory(); + final cookiesDirectory = Directory( + '${supportDirectory.path}/guest_cookies', + ); + final cookieJar = PersistCookieJar( + storage: FileStorage(cookiesDirectory.path), + ); // Logger di.registerLazySingleton(() => createLogger()); @@ -90,12 +104,20 @@ Future setupDI() async { () => HiveFitnessStartProgressStorage(fitnessStartProgressBox), dispose: (_) => fitnessStartProgressBox.close(), ); + di.registerLazySingleton(() => cookieJar); + di.registerLazySingleton( + () => CookieJarGuestSessionStorage( + di(), + Uri.parse(ApiPaths.baseUrl), + ), + ); // Authentication di.registerLazySingleton( () => createDioClient( logger: di(), tokenStorage: di(), + cookieJar: di(), ), ); di.registerLazySingleton(() => AuthApiClient(di())); @@ -167,6 +189,7 @@ Future setupDI() async { di(), di(), di(), + di(), di(), ), dispose: (cubit) => cubit.close(), diff --git a/lib/core/network/dio_setup.dart b/lib/core/network/dio_setup.dart index f7a24dc0..3a4bcf53 100644 --- a/lib/core/network/dio_setup.dart +++ b/lib/core/network/dio_setup.dart @@ -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'; @@ -11,6 +13,7 @@ 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()); @@ -18,6 +21,8 @@ Dio createDioClient({ /// 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, diff --git a/lib/core/services/guest_session_storage/cookie_jar_guest_session_storage.dart b/lib/core/services/guest_session_storage/cookie_jar_guest_session_storage.dart new file mode 100644 index 00000000..f5ab414b --- /dev/null +++ b/lib/core/services/guest_session_storage/cookie_jar_guest_session_storage.dart @@ -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 clear() async { + await _cookieJar.delete(_baseUri, true); + } +} diff --git a/lib/core/services/guest_session_storage/guest_session_storage.dart b/lib/core/services/guest_session_storage/guest_session_storage.dart new file mode 100644 index 00000000..946004f2 --- /dev/null +++ b/lib/core/services/guest_session_storage/guest_session_storage.dart @@ -0,0 +1,5 @@ +/// Abstraction for clearing persisted guest backend session data. +abstract interface class GuestSessionStorage { + /// Clears any guest session data persisted locally. + Future clear(); +} diff --git a/lib/features/auth/presentation/cubits/auth_session_cubit.dart b/lib/features/auth/presentation/cubits/auth_session_cubit.dart index 6abd5230..5998aa71 100644 --- a/lib/features/auth/presentation/cubits/auth_session_cubit.dart +++ b/lib/features/auth/presentation/cubits/auth_session_cubit.dart @@ -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'; @@ -25,6 +26,9 @@ final class AuthSessionCubit extends Cubit { /// 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; @@ -33,6 +37,7 @@ final class AuthSessionCubit extends Cubit { this._repository, this._tokenStorage, this._fitnessStartProgressStorage, + this._guestSessionStorage, this._logger, ) : super(const AuthSessionState.initial()); @@ -122,8 +127,22 @@ final class AuthSessionCubit extends Cubit { } } + Future _clearGuestSessionSafely() async { + try { + await _guestSessionStorage.clear(); + return true; + } catch (e, s) { + _logger.e('Failed to clear guest session cookies.', e, s); + return false; + } + } + Future _clearGuestDataSafely() async { - return _clearGuestProgressSafely(); + final results = await Future.wait([ + _clearGuestProgressSafely(), + _clearGuestSessionSafely(), + ]); + return results.every((isSuccess) => isSuccess); } Future _clearGuestDataAfterAuthSuccess() async { diff --git a/pubspec.lock b/pubspec.lock index fc5f1746..a6d26b23 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: @@ -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: @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index c6f3e28e..06419637 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/features/auth/presentation/cubits/auth_session_cubit_test.dart b/test/features/auth/presentation/cubits/auth_session_cubit_test.dart index 5158908d..41d755c2 100644 --- a/test/features/auth/presentation/cubits/auth_session_cubit_test.dart +++ b/test/features/auth/presentation/cubits/auth_session_cubit_test.dart @@ -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'; @@ -17,12 +18,14 @@ import 'auth_session_cubit_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), ]) void main() { late MockAuthRepository repository; late MockTokenStorage tokenStorage; late MockFitnessStartProgressStorage progressStorage; + late MockGuestSessionStorage guestSessionStorage; late MockAppLogger logger; late AuthSessionCubit authSessionCubit; @@ -36,17 +39,20 @@ void main() { repository = MockAuthRepository(); tokenStorage = MockTokenStorage(); progressStorage = MockFitnessStartProgressStorage(); + guestSessionStorage = MockGuestSessionStorage(); logger = MockAppLogger(); authSessionCubit = AuthSessionCubit( repository, tokenStorage, progressStorage, + guestSessionStorage, logger, ); provideDummy>(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', () { @@ -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()); }, ); @@ -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()); }, ); @@ -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()); }, ); @@ -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()); }, @@ -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); }, ); @@ -262,6 +273,7 @@ void main() { expect: () => const [AuthSessionState.guest()], verify: (_) { verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); }, ); @@ -276,6 +288,7 @@ void main() { expect: () => const [], verify: (_) { verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); verify(logger.e(any, any, any)).called(1); }, ); @@ -288,6 +301,7 @@ void main() { expect: () => const [], verify: (_) { verifyNever(progressStorage.clear()); + verifyNever(guestSessionStorage.clear()); }, ); @@ -337,13 +351,14 @@ void main() { expect: () => const [AuthSessionState.unauthenticated()], verify: (_) { verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); }, ); blocTest( - '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(), @@ -351,6 +366,7 @@ void main() { expect: () => const [], verify: (_) { verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); verify(logger.e(any, any, any)).called(1); }, ); @@ -365,6 +381,7 @@ void main() { expect: () => const [AuthSessionState.authenticated(user)], verify: (_) { verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); }, ); @@ -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()); }, @@ -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); }, @@ -406,6 +425,7 @@ void main() { verify: (_) { verify(tokenStorage.deleteAccessToken()).called(1); verify(progressStorage.clear()).called(1); + verify(guestSessionStorage.clear()).called(1); }, ); @@ -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); }, );