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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added auth session foundation with `AuthSessionCubit` (`initial/checking/guest/authenticated/unauthenticated`) and startup session restore.
- Added session-aware router refresh integration via `GoRouterCubitRefreshStream`.
- Added unit tests for `AuthRepositoryImpl.getCurrentUser` and `AuthSessionCubit`.
- Added register API contract and DTOs in auth data layer (`RegisterRequestDto`, `RegisterResponseDto`) and wired `AuthApiClient.register`.
- Added auth domain/data sign-up flow: `AuthRepository.signUp` contract and `AuthRepositoryImpl` implementation.
- Added sign-up presentation flow: `SignUpCubit`, `SignUpPageBuilder`, and `SignUpPage` integrated with router.
- Added unit tests for `AuthRepositoryImpl.signUp` and `SignUpCubit`.
- Added shared auth UI widgets for reusable screen composition: (`AuthFlowShell`, `AuthTextField`, `AuthPasswordField`, `AuthSwitchSection`)

### Changed

Expand All @@ -47,6 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated `SignInPageBuilder` to provide the shared `AuthSessionCubit` instance from DI for router/session consistency.
- Updated app bootstrap to call `restoreSession()` asynchronously on startup.
- Refactored auth repository tests to reuse shared Dio exception fixtures.
- Updated `UserDto.emailVerifiedAt` to nullable to align with `/register` response payload.
- Refactored sign-in and sign-up pages to use shared auth widgets and unified auth layout.
- Updated sign-up consent UX: extracted local `ConsentRow` widget and clarified validation snackbar message.

### Fixed

Expand Down
5 changes: 5 additions & 0 deletions lib/core/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';

import '../../features/auth/presentation/cubits/auth_session_cubit.dart';
import '../../features/auth/presentation/pages/sign_in_page_builder.dart';
import '../../features/auth/presentation/pages/sign_up_page_builder.dart';
import '../../features/debug/presentation/debug_screen.dart';
import '../di/di.dart';
import '../utils/analytics/app_analytics.dart';
Expand Down Expand Up @@ -55,6 +56,10 @@ final router = GoRouter(
path: AppRoutePaths.signInPath,
builder: (_, _) => const SignInPageBuilder(),
),
GoRoute(
path: AppRoutePaths.signUpPath,
builder: (_, _) => const SignUpPageBuilder(),
),
],
);

Expand Down
3 changes: 3 additions & 0 deletions lib/core/router/router_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ abstract class AppRoutePaths {

/// Route path for the sign-in page.
static const signInPath = '$authPrefix/sign-in';

/// Route path for the sign-up page.
static const signUpPath = '$authPrefix/sign-up';
}
26 changes: 26 additions & 0 deletions lib/features/auth/data/dto/register_request_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';

part 'register_request_dto.g.dart';

/// DTO for the register request.
@JsonSerializable(createFactory: false)
class RegisterRequestDto {
/// User name.
final String name;

/// User email.
final String email;

/// User password.
final String password;

/// Creates an instance of [RegisterRequestDto].
RegisterRequestDto({
required this.name,
required this.email,
required this.password,
});

/// Converts [RegisterRequestDto] to JSON.
Map<String, dynamic> toJson() => _$RegisterRequestDtoToJson(this);
}
29 changes: 29 additions & 0 deletions lib/features/auth/data/dto/register_response_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';

import 'user_dto.dart';

part 'register_response_dto.g.dart';

/// DTO for the register response.
@JsonSerializable(createToJson: false)
class RegisterResponseDto {
/// Indicates whether the register request succeeded.
final bool success;

/// Backend message about registration result.
final String message;

/// Registered user.
final UserDto user;

/// Creates an instance of [RegisterResponseDto].
RegisterResponseDto({
required this.success,
required this.message,
required this.user,
});

/// Creates a [RegisterResponseDto] from JSON.
factory RegisterResponseDto.fromJson(Map<String, dynamic> json) =>
_$RegisterResponseDtoFromJson(json);
}
2 changes: 1 addition & 1 deletion lib/features/auth/data/dto/user_dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class UserDto {

/// Timestamp of the user email verification.
@JsonKey(name: 'email_verified_at')
final String emailVerifiedAt;
final String? emailVerifiedAt;

/// Timestamp of the last update to the user information.
@JsonKey(name: 'updated_at')
Expand Down
6 changes: 6 additions & 0 deletions lib/features/auth/data/remote/auth_api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:retrofit/retrofit.dart';
import '../dto/login_request_dto.dart';
import '../dto/login_response_dto.dart';
import '../dto/me_response_dto.dart';
import '../dto/register_request_dto.dart';
import '../dto/register_response_dto.dart';

part 'auth_api_client.g.dart';

Expand All @@ -17,6 +19,10 @@ abstract class AuthApiClient {
@POST('/login')
Future<LoginResponseDto> login(@Body() LoginRequestDto request);

/// Sends register request and returns registered user payload.
@POST('/register')
Future<RegisterResponseDto> register(@Body() RegisterRequestDto request);

/// Returns current authorized user profile.
@GET('/me')
Future<MeResponseDto> me();
Expand Down
16 changes: 16 additions & 0 deletions lib/features/auth/data/repositories/auth_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../../../core/utils/logger/app_logger.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../dto/login_request_dto.dart';
import '../dto/register_request_dto.dart';
import '../mappers/auth_mapper.dart';
import '../mappers/user_entity_mapper.dart';
import '../remote/auth_api_client.dart';
Expand Down Expand Up @@ -42,6 +43,21 @@ final class AuthRepositoryImpl implements AuthRepository {
}
}

@override
Future<Result<User, AuthFailure>> signUp(String name, String email, String password) async {
try {
final request = RegisterRequestDto(name: name, email: email, password: password);
final response = await _apiClient.register(request);
return Result.success(response.user.toEntity());
Comment thread
CowboyGH marked this conversation as resolved.
} on DioException catch (e) {
final networkFailure = e.toNetworkFailure();
return Result.failure(networkFailure.toAuthFailure());
} catch (e, s) {
_logger.e('SignUp failed with unexpected error', e, s);
return Result.failure(UnknownAuthFailure(parentException: e, stackTrace: s));
}
}

@override
Future<Result<User, AuthFailure>> getCurrentUser() async {
try {
Expand Down
3 changes: 3 additions & 0 deletions lib/features/auth/domain/repositories/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ abstract interface class AuthRepository {
/// Signs in a user with email and password.
Future<Result<User, AuthFailure>> signIn(String email, String password);

/// Signs up a user with name, email and password.
Future<Result<User, AuthFailure>> signUp(String name, String email, String password);

/// Returns current authorized user.
Future<Result<User, AuthFailure>> getCurrentUser();
}
36 changes: 36 additions & 0 deletions lib/features/auth/presentation/cubits/sign_up_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

import '../../../../core/failures/feature/auth/auth_failure.dart';
import '../../../../core/result/result.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';

part 'sign_up_cubit.freezed.dart';
part 'sign_up_state.dart';

/// Cubit that manages sign-up flow and emits [SignUpState].
final class SignUpCubit extends Cubit<SignUpState> {
/// Authentication repository used for sign-up requests.
final AuthRepository _repository;

/// Creates an instance of [SignUpCubit].
SignUpCubit(this._repository) : super(const SignUpState.initial());

/// Attempts to sign up with [name], [email] and [password].
Future<void> signUp(String name, String email, String password) async {
if (state is _InProgress) return;

emit(const SignUpState.inProgress());

final result = await _repository.signUp(name, email, password);
if (isClosed) return;

switch (result) {
case Success(data: final user):
emit(SignUpState.succeed(user));
case Failure(:final error):
emit(SignUpState.failed(error));
}
}
}
17 changes: 17 additions & 0 deletions lib/features/auth/presentation/cubits/sign_up_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
part of 'sign_up_cubit.dart';

/// States for [SignUpCubit].
@freezed
class SignUpState with _$SignUpState {
/// Initial idle state before any register attempt.
const factory SignUpState.initial() = _Initial;

/// State emitted when sign-up succeeds.
const factory SignUpState.succeed(User user) = _Succeed;

/// State emitted when sign-up fails.
const factory SignUpState.failed(AuthFailure failure) = _Failed;

/// State emitted while sign-up request is in progress.
const factory SignUpState.inProgress() = _InProgress;
}
Loading