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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The debug route is now a static centered placeholder again and no longer owns a separate logout flow.
- `AppCard` now supports an optional fixed height, allowing specialized screens like the subscriptions catalog to match exact card mockups without introducing a forked card component.
- Subscriptions catalog cards now open a dedicated subscription details screen, and `AppInputField` now supports hidden labels for grouped payment-field layouts without requiring widget forks.
- Removed guest test attempt flow, onboarding testing routes, testing-related string constants from fitness start onboarding.

### Breaking

- Shared test-attempt transport DTOs were renamed from guest-prefixed names to neutral request/response models because the same payload shapes are now reused by both guest and authenticated flows.
- Test-attempt DI wiring now resolves separate guest and authenticated repository bindings while keeping the shared `TestAttemptCubit` and domain contract unchanged.
- Guest fitness start onboarding now completes with fitness parameters collection (goals, physical metrics, and fitness level) and proceeds directly to sign-up, removing the intermediate testing step.

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Mobile client for the moveUP fitness platform.

## 🚀 Highlights

- 🏃 **Fitness Start Onboarding**: guest quiz, onboarding test catalog, guest test attempts, and completed-progress resume support before sign-up
- 🏃 **Fitness Start Onboarding**: guest quiz and resume support before sign-up
- 🔐 **End-to-end Auth Flow**: sign in, sign up, email verification, password recovery, OTP verification, password reset, and logout
- 📶 **Offline Flow**: app-level connectivity tracking, blocking offline route, and automatic return to the auth flow after reconnect
- ✨ **Startup Splash Flow**: branded startup entry route with router-driven splash timing before auth or offline redirects
Expand Down Expand Up @@ -98,10 +98,10 @@ lib/
│ │ ├── data/ # DTOs, API client, repository implementation
│ │ ├── domain/ # Entities and repository contracts
│ │ └── presentation/ # Pages, widgets, cubits, validators
│ ├── fitness_start/ # Guest onboarding quiz, tests shell, and onboarding flow UI
│ ├── fitness_start/ # Guest onboarding quiz
│ ├── offline/ # Connectivity state and blocking offline flow
│ ├── splash/ # Branded startup splash screen
│ ├── tests/ # Shared tests catalog and guest test attempt flow
│ ├── tests/ # Tests catalog and guest attempt flow
│ └── debug/ # Internal debug screen
├── uikit/
│ ├── buttons/ # Shared buttons
Expand Down
9 changes: 3 additions & 6 deletions lib/core/constants/app_strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,13 @@ abstract final class AppStrings {
static const fitnessStartHeightRange = '140–210 см';
static const fitnessStartReferencesLoadFailed = 'Не удалось загрузить данные';
static const fitnessStartRetryButton = 'Попробовать снова';
static const fitnessStartCompletedTitle = 'Обнаружены данные';
static const fitnessStartCompletedTitle = 'Обнаружены сохраненные данные';
static const fitnessStartCompletedMessage =
'Хотите пройти тест снова или использовать ранее введенную информацию?';
static const fitnessStartRestartAction = 'Пройти тест снова';
'Хотите пройти фитнес-старт заново или использовать ранее введенную информацию?';
static const fitnessStartRestartAction = 'Пройти заново';
static const fitnessStartRegisterAction = 'Использовать введенную информацию';
static const fitnessStartValidationFailed = 'Проверьте введенные данные и попробуйте снова';
static const fitnessStartUnknown = 'Не удалось выполнить действие. Попробуйте снова';
static const fitnessStartTestsTitle = 'Персональная программа';
static const fitnessStartTestsDescription =
'Всего несколько быстрых тестов помогут подобрать безопасные и эффективные упражнения для Вашего уровня подготовки';

// Tests catalog.
static const testsCatalogTitle = 'Тесты';
Expand Down
36 changes: 3 additions & 33 deletions lib/core/di/di.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
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 All @@ -34,8 +30,7 @@ import '../../features/subscriptions/data/remote/subscription_payment_api_client
import '../../features/subscriptions/data/remote/subscriptions_api_client.dart';
import '../../features/subscriptions/data/repositories/subscriptions_repository_impl.dart';
import '../../features/subscriptions/domain/repositories/subscriptions_repository.dart';
import '../../features/tests/attempt/data/repositories/authenticated_test_attempt_repository_impl.dart';
import '../../features/tests/attempt/data/repositories/guest_test_attempt_repository_impl.dart';
import '../../features/tests/attempt/data/repositories/test_attempt_repository_impl.dart';
import '../../features/tests/attempt/domain/repositories/test_attempt_repository.dart';
import '../../features/tests/catalog/data/repositories/tests_catalog_repository_impl.dart';
import '../../features/tests/catalog/domain/repositories/tests_catalog_repository.dart';
Expand All @@ -47,12 +42,9 @@ 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 @@ -71,13 +63,6 @@ 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 @@ -105,20 +90,12 @@ 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 @@ -190,7 +167,6 @@ Future<void> setupDI() async {
di<AuthRepository>(),
di<TokenStorage>(),
di<FitnessStartProgressStorage>(),
di<GuestSessionStorage>(),
di<AppLogger>(),
),
dispose: (cubit) => cubit.close(),
Expand All @@ -204,14 +180,8 @@ Future<void> setupDI() async {
di<TestsApiClient>(),
),
);
di.registerLazySingleton<GuestTestAttemptRepository>(
() => GuestTestAttemptRepositoryImpl(
di<AppLogger>(),
di<TestsApiClient>(),
),
);
di.registerLazySingleton<AuthenticatedTestAttemptRepository>(
() => AuthenticatedTestAttemptRepositoryImpl(
di.registerLazySingleton<TestAttemptRepository>(
() => TestAttemptRepositoryImpl(
di<AppLogger>(),
di<TestsApiClient>(),
),
Expand Down
6 changes: 0 additions & 6 deletions lib/core/network/api_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@ abstract class ApiPaths {
/// The endpoint prefix for authenticated test attempts.
static const String testAttempts = '${apiPrefix}test-attempts';

/// The endpoint prefix for guest tests.
static const String guestTests = '${apiPrefix}guest/tests';

/// The endpoint prefix for guest test attempts.
static const String guestTestAttempts = '${apiPrefix}guest/test-attempts';

/// The endpoint for the current user workouts overview.
static const String workouts = '${apiPrefix}workouts';

Expand Down
5 changes: 0 additions & 5 deletions lib/core/network/dio_setup.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
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 @@ -13,16 +11,13 @@ 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
25 changes: 0 additions & 25 deletions lib/core/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import '../../features/auth/presentation/pages/verify_email_route_args.dart';
import '../../features/auth/presentation/pages/verify_reset_code_page_builder.dart';
import '../../features/debug/presentation/debug_screen.dart';
import '../../features/fitness_start/presentation/pages/fitness_start_quiz_page_builder.dart';
import '../../features/fitness_start/presentation/pages/fitness_start_test_attempt_page_builder.dart';
import '../../features/fitness_start/presentation/pages/fitness_start_tests_page_builder.dart';
import '../../features/offline/presentation/cubit/network_cubit.dart';
import '../../features/offline/presentation/pages/offline_page.dart';
import '../../features/profile/presentation/pages/profile_page_builder.dart';
Expand Down Expand Up @@ -376,29 +374,6 @@ final router = GoRouter(
path: AppRoutePaths.fitnessStartQuizPath,
builder: (_, _) => const FitnessStartQuizPageBuilder(),
),
GoRoute(
path: AppRoutePaths.fitnessStartTestsPath,
redirect: (_, state) {
if (state.extra == AppRoutePaths.fitnessStartQuizPath) {
return null;
}
return AppRoutePaths.fitnessStartQuizPath;
},
builder: (_, _) => const FitnessStartTestsPageBuilder(),
),
GoRoute(
path: AppRoutePaths.fitnessStartTestAttemptPath,
redirect: (_, state) {
final testingId = int.tryParse(state.pathParameters['testingId'] ?? '');
if (testingId == null || testingId <= 0) {
return AppRoutePaths.fitnessStartTestsPath;
}
return null;
},
builder: (_, state) => FitnessStartTestAttemptPageBuilder(
testingId: int.parse(state.pathParameters['testingId']!),
),
),
],
);

Expand Down
13 changes: 0 additions & 13 deletions lib/core/router/router_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,6 @@ abstract class AppRoutePaths {
/// Route path for the Fitness Start quiz.
static const fitnessStartQuizPath = '$fitnessStartPrefix/quiz';

/// Route path for the next onboarding step that will show tests.
static const fitnessStartTestsPath = '$fitnessStartPrefix/tests';

/// Base route path for a concrete test attempt inside Fitness Start.
static const fitnessStartTestAttemptBasePath = '$fitnessStartTestsPath/attempt';

/// Route path pattern for a concrete test attempt inside Fitness Start.
static const fitnessStartTestAttemptPath = '$fitnessStartTestAttemptBasePath/:testingId';

/// Builds the concrete route path for a test attempt by [testingId].
static String fitnessStartTestAttemptDetailsPath(int testingId) =>
'$fitnessStartTestAttemptBasePath/$testingId';

/// Route path for workouts overview.
static const workoutsPath = '/workouts';

Expand Down

This file was deleted.

This file was deleted.

21 changes: 1 addition & 20 deletions lib/features/auth/presentation/cubits/auth_session_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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 @@ -26,9 +25,6 @@ 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 @@ -37,7 +33,6 @@ final class AuthSessionCubit extends Cubit<AuthSessionState> {
this._repository,
this._tokenStorage,
this._fitnessStartProgressStorage,
this._guestSessionStorage,
this._logger,
) : super(const AuthSessionState.initial());

Expand Down Expand Up @@ -127,22 +122,8 @@ 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 {
final results = await Future.wait([
_clearGuestProgressSafely(),
_clearGuestSessionSafely(),
]);
return results.every((isSuccess) => isSuccess);
return _clearGuestProgressSafely();
}

Future<void> _clearGuestDataAfterAuthSuccess() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

import '../../../../core/constants/app_assets.dart';
import '../../../../core/constants/app_strings.dart';
import '../../../../core/router/router_paths.dart';
import '../../../../uikit/buttons/button_state.dart';
import '../../../../uikit/buttons/main_button.dart';
import '../../../../uikit/buttons/option_button.dart';
Expand Down Expand Up @@ -155,17 +153,14 @@ class _FitnessStartQuizPageState extends State<FitnessStartQuizPage> {
_fitnessStartCubit.clearFailure();
}
if (state.isCompleted) {
context.go(
AppRoutePaths.fitnessStartTestsPath,
extra: AppRoutePaths.fitnessStartQuizPath,
);
_authSessionCubit.completeGuestFitnessStart();
}
},
builder: (context, state) {
return Scaffold(
appBar: FitnessStartFlowAppBar(
title: AppStrings.fitnessStartTitle,
progress: (state.currentStep + 1) / 4,
progress: (state.currentStep + 1) / 3,
showBackButton: isGuestOnboarding,
onBackPressed: isGuestOnboarding ? () => unawaited(_handleGuestBack(state)) : null,
),
Expand Down
Loading
Loading