fix settings

This commit is contained in:
2025-09-26 20:54:32 +07:00
parent 30ed6b39b5
commit 74d0e3d44c
36 changed files with 5040 additions and 192 deletions

View File

@@ -0,0 +1,73 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../../../core/errors/exceptions.dart';
import '../models/user_model.dart';
abstract class AuthLocalDataSource {
Future<void> cacheUser(UserModel user);
Future<UserModel?> getCachedUser();
Future<void> clearCache();
Future<void> cacheToken(String token);
Future<String?> getCachedToken();
}
class AuthLocalDataSourceImpl implements AuthLocalDataSource {
final FlutterSecureStorage secureStorage;
static const String userKey = 'CACHED_USER';
static const String tokenKey = 'AUTH_TOKEN';
AuthLocalDataSourceImpl({required this.secureStorage});
@override
Future<void> cacheUser(UserModel user) async {
try {
final userJson = json.encode(user.toJson());
await secureStorage.write(key: userKey, value: userJson);
} catch (e) {
throw CacheException('Failed to cache user');
}
}
@override
Future<UserModel?> getCachedUser() async {
try {
final userJson = await secureStorage.read(key: userKey);
if (userJson != null) {
final userMap = json.decode(userJson) as Map<String, dynamic>;
return UserModel.fromJson(userMap);
}
return null;
} catch (e) {
throw CacheException('Failed to get cached user');
}
}
@override
Future<void> clearCache() async {
try {
await secureStorage.delete(key: userKey);
await secureStorage.delete(key: tokenKey);
} catch (e) {
throw CacheException('Failed to clear cache');
}
}
@override
Future<void> cacheToken(String token) async {
try {
await secureStorage.write(key: tokenKey, value: token);
} catch (e) {
throw CacheException('Failed to cache token');
}
}
@override
Future<String?> getCachedToken() async {
try {
return await secureStorage.read(key: tokenKey);
} catch (e) {
throw CacheException('Failed to get cached token');
}
}
}

View File

@@ -0,0 +1,232 @@
import 'package:dio/dio.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/dio_client.dart';
import '../models/user_model.dart';
abstract class AuthRemoteDataSource {
Future<UserModel> login({
required String email,
required String password,
});
Future<UserModel> register({
required String email,
required String password,
required String name,
});
Future<void> logout();
Future<UserModel> refreshToken(String token);
Future<UserModel> updateProfile({
required String name,
String? avatarUrl,
});
Future<void> changePassword({
required String oldPassword,
required String newPassword,
});
Future<void> resetPassword({
required String email,
});
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final DioClient dioClient;
AuthRemoteDataSourceImpl({required this.dioClient});
@override
Future<UserModel> login({
required String email,
required String password,
}) async {
try {
// Using JSONPlaceholder as a mock API
// In real app, this would be your actual auth endpoint
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'email': email,
'password': password,
},
);
// Mock validation - accept any email/password for demo
// In real app, the server would validate credentials
if (email.isEmpty || password.isEmpty) {
throw const ServerException('Invalid credentials');
}
// Mock response for demonstration
// In real app, parse actual API response
final mockUser = {
'id': '1',
'email': email,
'name': email.split('@').first,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const ServerException('Invalid credentials');
} else if (e.response?.statusCode == 404) {
throw const ServerException('User not found');
} else {
throw ServerException(e.message ?? 'Login failed');
}
} catch (e) {
if (e.toString().contains('Invalid credentials')) {
rethrow;
}
throw ServerException(e.toString());
}
}
@override
Future<UserModel> register({
required String email,
required String password,
required String name,
}) async {
try {
// Mock API call
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/users',
data: {
'email': email,
'password': password,
'name': name,
},
);
// Mock response
final mockUser = {
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'email': email,
'name': name,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} on DioException catch (e) {
if (e.response?.statusCode == 409) {
throw const ServerException('Email already exists');
} else {
throw ServerException(e.message ?? 'Registration failed');
}
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> logout() async {
try {
// Mock API call
await dioClient.dio.post('https://jsonplaceholder.typicode.com/posts');
// In real app, you might call a logout endpoint to invalidate token
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<UserModel> refreshToken(String token) async {
try {
// Mock API call
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/users',
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
// Mock response
final mockUser = {
'id': '1',
'email': 'user@example.com',
'name': 'User',
'token': 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<UserModel> updateProfile({
required String name,
String? avatarUrl,
}) async {
try {
// Mock API call
final response = await dioClient.dio.put(
'https://jsonplaceholder.typicode.com/users/1',
data: {
'name': name,
'avatarUrl': avatarUrl,
},
);
// Mock response
final mockUser = {
'id': '1',
'email': 'user@example.com',
'name': name,
'avatarUrl': avatarUrl,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> changePassword({
required String oldPassword,
required String newPassword,
}) async {
try {
// Mock API call
await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'oldPassword': oldPassword,
'newPassword': newPassword,
},
);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> resetPassword({
required String email,
}) async {
try {
// Mock API call
await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'email': email,
},
);
} catch (e) {
throw ServerException(e.toString());
}
}
}

View File

@@ -0,0 +1,42 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/user.dart';
part 'user_model.freezed.dart';
part 'user_model.g.dart';
@freezed
class UserModel with _$UserModel {
const factory UserModel({
required String id,
required String email,
required String name,
String? avatarUrl,
required String token,
DateTime? tokenExpiry,
}) = _UserModel;
const UserModel._();
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
/// Convert to domain entity
User toEntity() => User(
id: id,
email: email,
name: name,
avatarUrl: avatarUrl,
token: token,
tokenExpiry: tokenExpiry,
);
/// Create from domain entity
factory UserModel.fromEntity(User user) => UserModel(
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
token: user.token,
tokenExpiry: user.tokenExpiry,
);
}

View File

@@ -0,0 +1,259 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
UserModel _$UserModelFromJson(Map<String, dynamic> json) {
return _UserModel.fromJson(json);
}
/// @nodoc
mixin _$UserModel {
String get id => throw _privateConstructorUsedError;
String get email => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String? get avatarUrl => throw _privateConstructorUsedError;
String get token => throw _privateConstructorUsedError;
DateTime? get tokenExpiry => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UserModelCopyWith<UserModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserModelCopyWith<$Res> {
factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) then) =
_$UserModelCopyWithImpl<$Res, UserModel>;
@useResult
$Res call(
{String id,
String email,
String name,
String? avatarUrl,
String token,
DateTime? tokenExpiry});
}
/// @nodoc
class _$UserModelCopyWithImpl<$Res, $Val extends UserModel>
implements $UserModelCopyWith<$Res> {
_$UserModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? email = null,
Object? name = null,
Object? avatarUrl = freezed,
Object? token = null,
Object? tokenExpiry = freezed,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
avatarUrl: freezed == avatarUrl
? _value.avatarUrl
: avatarUrl // ignore: cast_nullable_to_non_nullable
as String?,
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
tokenExpiry: freezed == tokenExpiry
? _value.tokenExpiry
: tokenExpiry // ignore: cast_nullable_to_non_nullable
as DateTime?,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserModelImplCopyWith<$Res>
implements $UserModelCopyWith<$Res> {
factory _$$UserModelImplCopyWith(
_$UserModelImpl value, $Res Function(_$UserModelImpl) then) =
__$$UserModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String email,
String name,
String? avatarUrl,
String token,
DateTime? tokenExpiry});
}
/// @nodoc
class __$$UserModelImplCopyWithImpl<$Res>
extends _$UserModelCopyWithImpl<$Res, _$UserModelImpl>
implements _$$UserModelImplCopyWith<$Res> {
__$$UserModelImplCopyWithImpl(
_$UserModelImpl _value, $Res Function(_$UserModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? email = null,
Object? name = null,
Object? avatarUrl = freezed,
Object? token = null,
Object? tokenExpiry = freezed,
}) {
return _then(_$UserModelImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
avatarUrl: freezed == avatarUrl
? _value.avatarUrl
: avatarUrl // ignore: cast_nullable_to_non_nullable
as String?,
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
tokenExpiry: freezed == tokenExpiry
? _value.tokenExpiry
: tokenExpiry // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserModelImpl extends _UserModel {
const _$UserModelImpl(
{required this.id,
required this.email,
required this.name,
this.avatarUrl,
required this.token,
this.tokenExpiry})
: super._();
factory _$UserModelImpl.fromJson(Map<String, dynamic> json) =>
_$$UserModelImplFromJson(json);
@override
final String id;
@override
final String email;
@override
final String name;
@override
final String? avatarUrl;
@override
final String token;
@override
final DateTime? tokenExpiry;
@override
String toString() {
return 'UserModel(id: $id, email: $email, name: $name, avatarUrl: $avatarUrl, token: $token, tokenExpiry: $tokenExpiry)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.avatarUrl, avatarUrl) ||
other.avatarUrl == avatarUrl) &&
(identical(other.token, token) || other.token == token) &&
(identical(other.tokenExpiry, tokenExpiry) ||
other.tokenExpiry == tokenExpiry));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, id, email, name, avatarUrl, token, tokenExpiry);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$UserModelImplCopyWith<_$UserModelImpl> get copyWith =>
__$$UserModelImplCopyWithImpl<_$UserModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserModelImplToJson(
this,
);
}
}
abstract class _UserModel extends UserModel {
const factory _UserModel(
{required final String id,
required final String email,
required final String name,
final String? avatarUrl,
required final String token,
final DateTime? tokenExpiry}) = _$UserModelImpl;
const _UserModel._() : super._();
factory _UserModel.fromJson(Map<String, dynamic> json) =
_$UserModelImpl.fromJson;
@override
String get id;
@override
String get email;
@override
String get name;
@override
String? get avatarUrl;
@override
String get token;
@override
DateTime? get tokenExpiry;
@override
@JsonKey(ignore: true)
_$$UserModelImplCopyWith<_$UserModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserModelImpl _$$UserModelImplFromJson(Map<String, dynamic> json) =>
_$UserModelImpl(
id: json['id'] as String,
email: json['email'] as String,
name: json['name'] as String,
avatarUrl: json['avatarUrl'] as String?,
token: json['token'] as String,
tokenExpiry: json['tokenExpiry'] == null
? null
: DateTime.parse(json['tokenExpiry'] as String),
);
Map<String, dynamic> _$$UserModelImplToJson(_$UserModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'email': instance.email,
'name': instance.name,
'avatarUrl': instance.avatarUrl,
'token': instance.token,
'tokenExpiry': instance.tokenExpiry?.toIso8601String(),
};

View File

@@ -0,0 +1,232 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart';
import '../../../../core/network/network_info.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_local_datasource.dart';
import '../datasources/auth_remote_datasource.dart';
import '../models/user_model.dart';
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final AuthLocalDataSource localDataSource;
final NetworkInfo networkInfo;
AuthRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, User>> login({
required String email,
required String password,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
final userModel = await remoteDataSource.login(
email: email,
password: password,
);
// Cache user data and token
await localDataSource.cacheUser(userModel);
await localDataSource.cacheToken(userModel.token);
return Right(userModel.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, User>> register({
required String email,
required String password,
required String name,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
final userModel = await remoteDataSource.register(
email: email,
password: password,
name: name,
);
// Cache user data and token
await localDataSource.cacheUser(userModel);
await localDataSource.cacheToken(userModel.token);
return Right(userModel.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
// Clear local cache first
await localDataSource.clearCache();
// If online, notify server
if (await networkInfo.isConnected) {
await remoteDataSource.logout();
}
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, User?>> getCurrentUser() async {
try {
final cachedUser = await localDataSource.getCachedUser();
if (cachedUser != null) {
// Check if token is still valid
final user = cachedUser.toEntity();
if (user.isTokenValid) {
return Right(user);
} else {
// Token expired, try to refresh
if (await networkInfo.isConnected) {
return await refreshToken();
}
}
}
return const Right(null);
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(CacheFailure(e.toString()));
}
}
@override
Future<bool> isAuthenticated() async {
try {
final cachedUser = await localDataSource.getCachedUser();
if (cachedUser != null) {
final user = cachedUser.toEntity();
return user.isTokenValid;
}
return false;
} catch (_) {
return false;
}
}
@override
Future<Either<Failure, User>> refreshToken() async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
final token = await localDataSource.getCachedToken();
if (token == null) {
return const Left(AuthFailure('No token available'));
}
final userModel = await remoteDataSource.refreshToken(token);
// Update cached user and token
await localDataSource.cacheUser(userModel);
await localDataSource.cacheToken(userModel.token);
return Right(userModel.toEntity());
} on ServerException catch (e) {
// If refresh fails, clear cache
await localDataSource.clearCache();
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, User>> updateProfile({
required String name,
String? avatarUrl,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
final userModel = await remoteDataSource.updateProfile(
name: name,
avatarUrl: avatarUrl,
);
// Update cached user
await localDataSource.cacheUser(userModel);
return Right(userModel.toEntity());
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, void>> changePassword({
required String oldPassword,
required String newPassword,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
await remoteDataSource.changePassword(
oldPassword: oldPassword,
newPassword: newPassword,
);
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
@override
Future<Either<Failure, void>> resetPassword({
required String email,
}) async {
if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try {
await remoteDataSource.resetPassword(email: email);
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
}

View File

@@ -0,0 +1,28 @@
import 'package:equatable/equatable.dart';
/// User entity representing authenticated user
class User extends Equatable {
final String id;
final String email;
final String name;
final String? avatarUrl;
final String token;
final DateTime? tokenExpiry;
const User({
required this.id,
required this.email,
required this.name,
this.avatarUrl,
required this.token,
this.tokenExpiry,
});
@override
List<Object?> get props => [id, email, name, avatarUrl, token, tokenExpiry];
bool get isTokenValid {
if (tokenExpiry == null) return true;
return tokenExpiry!.isAfter(DateTime.now());
}
}

View File

@@ -0,0 +1,48 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/failures.dart';
import '../entities/user.dart';
/// Auth repository interface
abstract class AuthRepository {
/// Login with email and password
Future<Either<Failure, User>> login({
required String email,
required String password,
});
/// Register new user
Future<Either<Failure, User>> register({
required String email,
required String password,
required String name,
});
/// Logout current user
Future<Either<Failure, void>> logout();
/// Get current user
Future<Either<Failure, User?>> getCurrentUser();
/// Check if user is authenticated
Future<bool> isAuthenticated();
/// Refresh token
Future<Either<Failure, User>> refreshToken();
/// Update user profile
Future<Either<Failure, User>> updateProfile({
required String name,
String? avatarUrl,
});
/// Change password
Future<Either<Failure, void>> changePassword({
required String oldPassword,
required String newPassword,
});
/// Reset password
Future<Either<Failure, void>> resetPassword({
required String email,
});
}

View File

@@ -0,0 +1,16 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/failures.dart';
import '../../../../shared/domain/usecases/usecase.dart';
import '../entities/user.dart';
import '../repositories/auth_repository.dart';
class GetCurrentUserUseCase implements UseCase<User?, NoParams> {
final AuthRepository repository;
GetCurrentUserUseCase(this.repository);
@override
Future<Either<Failure, User?>> call(NoParams params) async {
return repository.getCurrentUser();
}
}

View File

@@ -0,0 +1,43 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/failures.dart';
import '../../../../shared/domain/usecases/usecase.dart';
import '../entities/user.dart';
import '../repositories/auth_repository.dart';
class LoginParams {
final String email;
final String password;
const LoginParams({
required this.email,
required this.password,
});
}
class LoginUseCase implements UseCase<User, LoginParams> {
final AuthRepository repository;
LoginUseCase(this.repository);
@override
Future<Either<Failure, User>> call(LoginParams params) async {
// Validate email format
if (!_isValidEmail(params.email)) {
return Left(ValidationFailure('Invalid email format'));
}
// Validate password
if (params.password.length < 6) {
return Left(ValidationFailure('Password must be at least 6 characters'));
}
return repository.login(
email: params.email,
password: params.password,
);
}
bool _isValidEmail(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/errors/failures.dart';
import '../../../../shared/domain/usecases/usecase.dart';
import '../repositories/auth_repository.dart';
class LogoutUseCase implements UseCase<void, NoParams> {
final AuthRepository repository;
LogoutUseCase(this.repository);
@override
Future<Either<Failure, void>> call(NoParams params) async {
return repository.logout();
}
}

View File

@@ -0,0 +1,555 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../providers/auth_providers.dart';
import '../providers/auth_state.dart';
import '../widgets/auth_button.dart';
import '../widgets/auth_text_field.dart';
/// Beautiful and functional login page with Material 3 design
/// Features responsive design, form validation, and proper state management
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({super.key});
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _emailFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
bool _isPasswordVisible = false;
bool _rememberMe = false;
late AnimationController _fadeAnimationController;
late AnimationController _slideAnimationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startAnimations();
}
void _setupAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideAnimationController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() {
Future.delayed(const Duration(milliseconds: 100), () {
_fadeAnimationController.forward();
_slideAnimationController.forward();
});
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
_fadeAnimationController.dispose();
_slideAnimationController.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegExp = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegExp.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 6) {
return 'Password must be at least 6 characters long';
}
return null;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
// Provide haptic feedback
HapticFeedback.lightImpact();
// Clear any existing errors
ref.read(authNotifierProvider.notifier).clearError();
// Attempt login
await ref.read(authNotifierProvider.notifier).login(
email: _emailController.text.trim(),
password: _passwordController.text,
);
}
void _handleForgotPassword() {
HapticFeedback.selectionClick();
// TODO: Navigate to forgot password page
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Forgot password functionality coming soon'),
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height * 0.1,
left: AppSpacing.lg,
right: AppSpacing.lg,
),
),
);
}
void _handleSignUp() {
HapticFeedback.selectionClick();
Navigator.of(context).pushNamed('/auth/register');
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.onError,
),
AppSpacing.horizontalSpaceSM,
Expanded(child: Text(message)),
],
),
backgroundColor: Theme.of(context).colorScheme.error,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height * 0.1,
left: AppSpacing.lg,
right: AppSpacing.lg,
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final mediaQuery = MediaQuery.of(context);
final isKeyboardVisible = mediaQuery.viewInsets.bottom > 0;
// Listen to auth state changes
ref.listen<AuthState>(authNotifierProvider, (previous, current) {
current.when(
initial: () {},
loading: () {},
authenticated: (user) {
// TODO: Navigate to home page
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.check_circle_outline,
color: theme.extension<AppColorsExtension>()?.onSuccess ??
colorScheme.onPrimary,
),
AppSpacing.horizontalSpaceSM,
Text('Welcome back, ${user.name}!'),
],
),
backgroundColor: theme.extension<AppColorsExtension>()?.success ??
colorScheme.primary,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
bottom: mediaQuery.size.height * 0.1,
left: AppSpacing.lg,
right: AppSpacing.lg,
),
),
);
},
unauthenticated: (message) {
if (message != null) {
_showErrorSnackBar(message);
}
},
error: (message) {
_showErrorSnackBar(message);
},
);
});
final authState = ref.watch(authNotifierProvider);
final isLoading = authState is AuthStateLoading;
return Scaffold(
backgroundColor: colorScheme.surface,
body: SafeArea(
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Center(
child: SingleChildScrollView(
padding: AppSpacing.responsivePadding(context),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: AppSpacing.isMobile(context) ? double.infinity : 400,
),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// App Logo and Welcome Section
_buildHeaderSection(theme, colorScheme, isKeyboardVisible),
AppSpacing.verticalSpaceXXL,
// Form Fields
_buildFormSection(theme, colorScheme, isLoading),
AppSpacing.verticalSpaceXXL,
// Login Button
_buildLoginButton(isLoading),
AppSpacing.verticalSpaceXL,
// Footer Links
_buildFooterSection(theme, colorScheme),
],
),
),
),
),
),
),
),
),
);
}
Widget _buildHeaderSection(
ThemeData theme,
ColorScheme colorScheme,
bool isKeyboardVisible,
) {
return AnimatedContainer(
duration: AppSpacing.animationNormal,
height: isKeyboardVisible ? 120 : 180,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Icon/Logo
Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.1),
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.primaryContainer.withValues(alpha: 0.2),
width: 1,
),
),
child: Icon(
Icons.lock_person_outlined,
size: isKeyboardVisible ? 48 : 64,
color: colorScheme.primary,
),
),
AppSpacing.verticalSpaceLG,
// Welcome Text
Text(
'Welcome Back',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
AppSpacing.verticalSpaceSM,
Text(
'Sign in to continue to your account',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildFormSection(
ThemeData theme,
ColorScheme colorScheme,
bool isLoading,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Email Field
AuthTextField(
controller: _emailController,
focusNode: _emailFocusNode,
labelText: 'Email Address',
hintText: 'Enter your email',
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
enabled: !isLoading,
autofillHints: const [AutofillHints.email],
prefixIcon: const Icon(Icons.email_outlined),
onFieldSubmitted: (_) => _passwordFocusNode.requestFocus(),
),
AppSpacing.verticalSpaceLG,
// Password Field
AuthTextField(
controller: _passwordController,
focusNode: _passwordFocusNode,
labelText: 'Password',
hintText: 'Enter your password',
obscureText: !_isPasswordVisible,
textInputAction: TextInputAction.done,
validator: _validatePassword,
enabled: !isLoading,
autofillHints: const [AutofillHints.password],
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: isLoading
? null
: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
tooltip: _isPasswordVisible ? 'Hide password' : 'Show password',
),
onFieldSubmitted: (_) => _handleLogin(),
),
AppSpacing.verticalSpaceMD,
// Remember Me and Forgot Password Row
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: isLoading
? null
: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
),
AppSpacing.horizontalSpaceSM,
Expanded(
child: Text(
'Remember me',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
TextButton(
onPressed: isLoading ? null : _handleForgotPassword,
child: Text(
'Forgot Password?',
style: TextStyle(
color: isLoading
? colorScheme.onSurface.withValues(alpha: 0.38)
: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
);
}
Widget _buildLoginButton(bool isLoading) {
return AuthButton(
onPressed: isLoading ? null : _handleLogin,
text: 'Sign In',
isLoading: isLoading,
type: AuthButtonType.filled,
icon: isLoading ? null : const Icon(Icons.login),
);
}
Widget _buildFooterSection(ThemeData theme, ColorScheme colorScheme) {
return Column(
children: [
// Divider with "or" text
Row(
children: [
Expanded(
child: Divider(
color: colorScheme.outlineVariant,
thickness: 1,
),
),
Padding(
padding: AppSpacing.horizontalLG,
child: Text(
'or',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: Divider(
color: colorScheme.outlineVariant,
thickness: 1,
),
),
],
),
AppSpacing.verticalSpaceXL,
// Sign Up Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Don't have an account? ",
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
TextButton(
onPressed: _handleSignUp,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Sign Up',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
AppSpacing.verticalSpaceLG,
// Privacy and Terms
Wrap(
alignment: WrapAlignment.center,
children: [
Text(
'By continuing, you agree to our ',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
InkWell(
onTap: () {
// TODO: Navigate to terms of service
},
child: Text(
'Terms of Service',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
Text(
' and ',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
InkWell(
onTap: () {
// TODO: Navigate to privacy policy
},
child: Text(
'Privacy Policy',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,7 @@
// Auth pages exports
//
// This file exports all auth-related pages for easy importing
// throughout the application.
export 'login_page.dart';
export 'register_page.dart';

View File

@@ -0,0 +1,685 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../providers/auth_providers.dart';
import '../widgets/auth_button.dart';
import '../widgets/auth_text_field.dart';
/// Beautiful and functional registration page with Material 3 design
/// Features comprehensive form validation, password confirmation, and responsive design
class RegisterPage extends ConsumerStatefulWidget {
const RegisterPage({super.key});
@override
ConsumerState<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends ConsumerState<RegisterPage>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
final _nameFocusNode = FocusNode();
final _emailFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
final _confirmPasswordFocusNode = FocusNode();
bool _isPasswordVisible = false;
bool _isConfirmPasswordVisible = false;
bool _agreeToTerms = false;
late AnimationController _fadeAnimationController;
late AnimationController _slideAnimationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startAnimations();
}
void _setupAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideAnimationController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() {
Future.delayed(const Duration(milliseconds: 100), () {
_fadeAnimationController.forward();
_slideAnimationController.forward();
});
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_nameFocusNode.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
_confirmPasswordFocusNode.dispose();
_fadeAnimationController.dispose();
_slideAnimationController.dispose();
super.dispose();
}
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return 'Full name is required';
}
if (value.trim().length < 2) {
return 'Name must be at least 2 characters long';
}
if (value.trim().split(' ').length < 2) {
return 'Please enter your full name';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
final emailRegExp = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
if (!emailRegExp.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 8) {
return 'Password must be at least 8 characters long';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return 'Password must contain uppercase, lowercase, and number';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Please confirm your password';
}
if (value != _passwordController.text) {
return 'Passwords do not match';
}
return null;
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_agreeToTerms) {
_showErrorSnackBar('Please agree to the Terms of Service and Privacy Policy');
return;
}
// Provide haptic feedback
HapticFeedback.lightImpact();
// Clear any existing errors
ref.read(authNotifierProvider.notifier).clearError();
// TODO: Implement registration logic
// For now, we'll simulate the registration process
await _simulateRegistration();
}
Future<void> _simulateRegistration() async {
// Show loading state
setState(() {});
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
// Simulate successful registration
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.check_circle_outline,
color: Theme.of(context).colorScheme.onPrimary,
),
AppSpacing.horizontalSpaceSM,
const Expanded(
child: Text('Account created successfully! Please sign in.'),
),
],
),
backgroundColor: Theme.of(context).extension<AppColorsExtension>()?.success ??
Theme.of(context).colorScheme.primary,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height * 0.1,
left: AppSpacing.lg,
right: AppSpacing.lg,
),
),
);
// Navigate back to login
Navigator.of(context).pop();
}
void _handleSignIn() {
HapticFeedback.selectionClick();
Navigator.of(context).pop();
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.onError,
),
AppSpacing.horizontalSpaceSM,
Expanded(child: Text(message)),
],
),
backgroundColor: Theme.of(context).colorScheme.error,
behavior: SnackBarBehavior.floating,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height * 0.1,
left: AppSpacing.lg,
right: AppSpacing.lg,
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final mediaQuery = MediaQuery.of(context);
final isKeyboardVisible = mediaQuery.viewInsets.bottom > 0;
// For simulation, we'll track loading state locally
// In real implementation, this would come from auth state
final isLoading = false; // Replace with actual auth state when implemented
return Scaffold(
backgroundColor: colorScheme.surface,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: colorScheme.onSurface,
),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: Center(
child: SingleChildScrollView(
padding: AppSpacing.responsivePadding(context),
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: AppSpacing.isMobile(context) ? double.infinity : 400,
),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Section
_buildHeaderSection(theme, colorScheme, isKeyboardVisible),
AppSpacing.verticalSpaceXXL,
// Form Fields
_buildFormSection(theme, colorScheme, isLoading),
AppSpacing.verticalSpaceXL,
// Terms Agreement
_buildTermsAgreement(theme, colorScheme, isLoading),
AppSpacing.verticalSpaceXL,
// Register Button
_buildRegisterButton(isLoading),
AppSpacing.verticalSpaceXL,
// Footer Links
_buildFooterSection(theme, colorScheme),
],
),
),
),
),
),
),
),
),
);
}
Widget _buildHeaderSection(
ThemeData theme,
ColorScheme colorScheme,
bool isKeyboardVisible,
) {
return AnimatedContainer(
duration: AppSpacing.animationNormal,
height: isKeyboardVisible ? 100 : 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// App Icon/Logo
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.1),
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.primaryContainer.withValues(alpha: 0.2),
width: 1,
),
),
child: Icon(
Icons.person_add_outlined,
size: isKeyboardVisible ? 40 : 56,
color: colorScheme.primary,
),
),
AppSpacing.verticalSpaceLG,
// Welcome Text
Text(
'Create Account',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
AppSpacing.verticalSpaceSM,
Text(
'Sign up to get started',
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildFormSection(
ThemeData theme,
ColorScheme colorScheme,
bool isLoading,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Full Name Field
AuthTextField(
controller: _nameController,
focusNode: _nameFocusNode,
labelText: 'Full Name',
hintText: 'Enter your full name',
keyboardType: TextInputType.name,
textInputAction: TextInputAction.next,
validator: _validateName,
enabled: !isLoading,
autofillHints: const [AutofillHints.name],
prefixIcon: const Icon(Icons.person_outline),
onFieldSubmitted: (_) => _emailFocusNode.requestFocus(),
),
AppSpacing.verticalSpaceLG,
// Email Field
AuthTextField(
controller: _emailController,
focusNode: _emailFocusNode,
labelText: 'Email Address',
hintText: 'Enter your email',
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
enabled: !isLoading,
autofillHints: const [AutofillHints.email],
prefixIcon: const Icon(Icons.email_outlined),
onFieldSubmitted: (_) => _passwordFocusNode.requestFocus(),
),
AppSpacing.verticalSpaceLG,
// Password Field
AuthTextField(
controller: _passwordController,
focusNode: _passwordFocusNode,
labelText: 'Password',
hintText: 'Create a strong password',
obscureText: !_isPasswordVisible,
textInputAction: TextInputAction.next,
validator: _validatePassword,
enabled: !isLoading,
autofillHints: const [AutofillHints.newPassword],
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: isLoading
? null
: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
tooltip: _isPasswordVisible ? 'Hide password' : 'Show password',
),
onFieldSubmitted: (_) => _confirmPasswordFocusNode.requestFocus(),
),
AppSpacing.verticalSpaceSM,
// Password Requirements
_buildPasswordRequirements(theme, colorScheme),
AppSpacing.verticalSpaceLG,
// Confirm Password Field
AuthTextField(
controller: _confirmPasswordController,
focusNode: _confirmPasswordFocusNode,
labelText: 'Confirm Password',
hintText: 'Confirm your password',
obscureText: !_isConfirmPasswordVisible,
textInputAction: TextInputAction.done,
validator: _validateConfirmPassword,
enabled: !isLoading,
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_isConfirmPasswordVisible
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: isLoading
? null
: () {
setState(() {
_isConfirmPasswordVisible = !_isConfirmPasswordVisible;
});
},
tooltip: _isConfirmPasswordVisible
? 'Hide password'
: 'Show password',
),
onFieldSubmitted: (_) => _handleRegister(),
),
],
);
}
Widget _buildPasswordRequirements(ThemeData theme, ColorScheme colorScheme) {
final password = _passwordController.text;
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: AppSpacing.radiusSM,
border: Border.all(
color: colorScheme.outlineVariant,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Password Requirements:',
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
AppSpacing.verticalSpaceXS,
_buildRequirementItem(
'At least 8 characters',
password.length >= 8,
theme,
colorScheme,
),
_buildRequirementItem(
'Contains uppercase letter',
RegExp(r'[A-Z]').hasMatch(password),
theme,
colorScheme,
),
_buildRequirementItem(
'Contains lowercase letter',
RegExp(r'[a-z]').hasMatch(password),
theme,
colorScheme,
),
_buildRequirementItem(
'Contains number',
RegExp(r'\d').hasMatch(password),
theme,
colorScheme,
),
],
),
);
}
Widget _buildRequirementItem(
String text,
bool isValid,
ThemeData theme,
ColorScheme colorScheme,
) {
return Row(
children: [
Icon(
isValid ? Icons.check_circle : Icons.radio_button_unchecked,
size: 16,
color: isValid
? (theme.extension<AppColorsExtension>()?.success ?? colorScheme.primary)
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
AppSpacing.horizontalSpaceXS,
Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
color: isValid
? (theme.extension<AppColorsExtension>()?.success ?? colorScheme.primary)
: colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _buildTermsAgreement(
ThemeData theme,
ColorScheme colorScheme,
bool isLoading,
) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _agreeToTerms,
onChanged: isLoading
? null
: (value) {
setState(() {
_agreeToTerms = value ?? false;
});
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
),
AppSpacing.horizontalSpaceSM,
Expanded(
child: Wrap(
children: [
Text(
'I agree to the ',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
InkWell(
onTap: () {
// TODO: Navigate to terms of service
},
child: Text(
'Terms of Service',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
Text(
' and ',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
InkWell(
onTap: () {
// TODO: Navigate to privacy policy
},
child: Text(
'Privacy Policy',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
),
),
),
],
),
),
],
);
}
Widget _buildRegisterButton(bool isLoading) {
return AuthButton(
onPressed: isLoading ? null : _handleRegister,
text: 'Create Account',
isLoading: isLoading,
type: AuthButtonType.filled,
icon: isLoading ? null : const Icon(Icons.person_add),
);
}
Widget _buildFooterSection(ThemeData theme, ColorScheme colorScheme) {
return Column(
children: [
// Sign In Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Already have an account? ',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
TextButton(
onPressed: _handleSignIn,
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Sign In',
style: TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,145 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/network/network_info.dart';
import '../../../../core/providers/network_providers.dart';
import '../../../../shared/presentation/providers/app_providers.dart' hide secureStorageProvider;
import '../../../../shared/domain/usecases/usecase.dart';
import '../../data/datasources/auth_local_datasource.dart';
import '../../data/datasources/auth_remote_datasource.dart';
import '../../data/repositories/auth_repository_impl.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../../domain/usecases/get_current_user_usecase.dart';
import '../../domain/usecases/login_usecase.dart';
import '../../domain/usecases/logout_usecase.dart';
import 'auth_state.dart';
part 'auth_providers.g.dart';
// Data sources
@riverpod
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
final dioClient = ref.watch(dioClientProvider);
return AuthRemoteDataSourceImpl(dioClient: dioClient);
}
@riverpod
AuthLocalDataSource authLocalDataSource(Ref ref) {
final secureStorage = ref.watch(secureStorageProvider);
return AuthLocalDataSourceImpl(secureStorage: secureStorage);
}
// Repository
@riverpod
AuthRepository authRepository(Ref ref) {
final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
final localDataSource = ref.watch(authLocalDataSourceProvider);
final networkInfo = ref.watch(networkInfoProvider);
return AuthRepositoryImpl(
remoteDataSource: remoteDataSource,
localDataSource: localDataSource,
networkInfo: networkInfo,
);
}
// Use cases
@riverpod
LoginUseCase loginUseCase(Ref ref) {
final repository = ref.watch(authRepositoryProvider);
return LoginUseCase(repository);
}
@riverpod
LogoutUseCase logoutUseCase(Ref ref) {
final repository = ref.watch(authRepositoryProvider);
return LogoutUseCase(repository);
}
@riverpod
GetCurrentUserUseCase getCurrentUserUseCase(Ref ref) {
final repository = ref.watch(authRepositoryProvider);
return GetCurrentUserUseCase(repository);
}
// Auth state notifier
@riverpod
class AuthNotifier extends _$AuthNotifier {
@override
AuthState build() {
// Check for cached user on startup
_checkAuthStatus();
return const AuthState.initial();
}
Future<void> _checkAuthStatus() async {
final getCurrentUser = ref.read(getCurrentUserUseCaseProvider);
final result = await getCurrentUser(const NoParams());
result.fold(
(failure) => state = AuthState.unauthenticated(failure.message),
(user) {
if (user != null) {
state = AuthState.authenticated(user);
} else {
state = const AuthState.unauthenticated();
}
},
);
}
Future<void> login({
required String email,
required String password,
}) async {
state = const AuthState.loading();
final loginUseCase = ref.read(loginUseCaseProvider);
final params = LoginParams(email: email, password: password);
final result = await loginUseCase(params);
result.fold(
(failure) => state = AuthState.error(failure.message),
(user) => state = AuthState.authenticated(user),
);
}
Future<void> logout() async {
state = const AuthState.loading();
final logoutUseCase = ref.read(logoutUseCaseProvider);
final result = await logoutUseCase(const NoParams());
result.fold(
(failure) => state = AuthState.error(failure.message),
(_) => state = const AuthState.unauthenticated(),
);
}
void clearError() {
if (state is AuthStateError) {
state = const AuthState.unauthenticated();
}
}
}
// Current user provider
@riverpod
User? currentUser(Ref ref) {
final authState = ref.watch(authNotifierProvider);
return authState.maybeWhen(
authenticated: (user) => user,
orElse: () => null,
);
}
// Is authenticated provider
@riverpod
bool isAuthenticated(Ref ref) {
final authState = ref.watch(authNotifierProvider);
return authState.maybeWhen(
authenticated: (_) => true,
orElse: () => false,
);
}

View File

@@ -0,0 +1,150 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auth_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authRemoteDataSourceHash() =>
r'e1e2164defcfc3905e9fb8e75e346817a6e0bf73';
/// See also [authRemoteDataSource].
@ProviderFor(authRemoteDataSource)
final authRemoteDataSourceProvider =
AutoDisposeProvider<AuthRemoteDataSource>.internal(
authRemoteDataSource,
name: r'authRemoteDataSourceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authRemoteDataSourceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AuthRemoteDataSourceRef = AutoDisposeProviderRef<AuthRemoteDataSource>;
String _$authLocalDataSourceHash() =>
r'dfab2fdd71de815f93c16ab9e234bd2d0885d2f4';
/// See also [authLocalDataSource].
@ProviderFor(authLocalDataSource)
final authLocalDataSourceProvider =
AutoDisposeProvider<AuthLocalDataSource>.internal(
authLocalDataSource,
name: r'authLocalDataSourceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authLocalDataSourceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AuthLocalDataSourceRef = AutoDisposeProviderRef<AuthLocalDataSource>;
String _$authRepositoryHash() => r'8ce22ed16336f42a50e8266fbafbdbd7db71d613';
/// See also [authRepository].
@ProviderFor(authRepository)
final authRepositoryProvider = AutoDisposeProvider<AuthRepository>.internal(
authRepository,
name: r'authRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AuthRepositoryRef = AutoDisposeProviderRef<AuthRepository>;
String _$loginUseCaseHash() => r'cbfd4200f40c132516f20f942ae9d825a31e2515';
/// See also [loginUseCase].
@ProviderFor(loginUseCase)
final loginUseCaseProvider = AutoDisposeProvider<LoginUseCase>.internal(
loginUseCase,
name: r'loginUseCaseProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$loginUseCaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef LoginUseCaseRef = AutoDisposeProviderRef<LoginUseCase>;
String _$logoutUseCaseHash() => r'67224f00aebb158eab2aba2c4398e98150dd958c';
/// See also [logoutUseCase].
@ProviderFor(logoutUseCase)
final logoutUseCaseProvider = AutoDisposeProvider<LogoutUseCase>.internal(
logoutUseCase,
name: r'logoutUseCaseProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$logoutUseCaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef LogoutUseCaseRef = AutoDisposeProviderRef<LogoutUseCase>;
String _$getCurrentUserUseCaseHash() =>
r'1e9d6222283b80c2b6fc6ed8c89f4130614c0a11';
/// See also [getCurrentUserUseCase].
@ProviderFor(getCurrentUserUseCase)
final getCurrentUserUseCaseProvider =
AutoDisposeProvider<GetCurrentUserUseCase>.internal(
getCurrentUserUseCase,
name: r'getCurrentUserUseCaseProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$getCurrentUserUseCaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef GetCurrentUserUseCaseRef
= AutoDisposeProviderRef<GetCurrentUserUseCase>;
String _$currentUserHash() => r'a5dbfda090aa4a2784b934352ff00cf3c751332b';
/// See also [currentUser].
@ProviderFor(currentUser)
final currentUserProvider = AutoDisposeProvider<User?>.internal(
currentUser,
name: r'currentUserProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$currentUserHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef CurrentUserRef = AutoDisposeProviderRef<User?>;
String _$isAuthenticatedHash() => r'0f8559d2c47c9554b3c1b9643ed0c2bf1cb18727';
/// See also [isAuthenticated].
@ProviderFor(isAuthenticated)
final isAuthenticatedProvider = AutoDisposeProvider<bool>.internal(
isAuthenticated,
name: r'isAuthenticatedProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$isAuthenticatedHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsAuthenticatedRef = AutoDisposeProviderRef<bool>;
String _$authNotifierHash() => r'e97041b6776589adb6e6d424d2ebbb7bc837cb5b';
/// See also [AuthNotifier].
@ProviderFor(AuthNotifier)
final authNotifierProvider =
AutoDisposeNotifierProvider<AuthNotifier, AuthState>.internal(
AuthNotifier.new,
name: r'authNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$authNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AuthNotifier = AutoDisposeNotifier<AuthState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/user.dart';
part 'auth_state.freezed.dart';
@freezed
class AuthState with _$AuthState {
const factory AuthState.initial() = AuthStateInitial;
const factory AuthState.loading() = AuthStateLoading;
const factory AuthState.authenticated(User user) = AuthStateAuthenticated;
const factory AuthState.unauthenticated([String? message]) = AuthStateUnauthenticated;
const factory AuthState.error(String message) = AuthStateError;
}

View File

@@ -0,0 +1,794 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'auth_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$AuthState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AuthStateCopyWith<$Res> {
factory $AuthStateCopyWith(AuthState value, $Res Function(AuthState) then) =
_$AuthStateCopyWithImpl<$Res, AuthState>;
}
/// @nodoc
class _$AuthStateCopyWithImpl<$Res, $Val extends AuthState>
implements $AuthStateCopyWith<$Res> {
_$AuthStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
}
/// @nodoc
abstract class _$$AuthStateInitialImplCopyWith<$Res> {
factory _$$AuthStateInitialImplCopyWith(_$AuthStateInitialImpl value,
$Res Function(_$AuthStateInitialImpl) then) =
__$$AuthStateInitialImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$AuthStateInitialImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateInitialImpl>
implements _$$AuthStateInitialImplCopyWith<$Res> {
__$$AuthStateInitialImplCopyWithImpl(_$AuthStateInitialImpl _value,
$Res Function(_$AuthStateInitialImpl) _then)
: super(_value, _then);
}
/// @nodoc
class _$AuthStateInitialImpl implements AuthStateInitial {
const _$AuthStateInitialImpl();
@override
String toString() {
return 'AuthState.initial()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$AuthStateInitialImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) {
return initial(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) {
return initial?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial(this);
}
return orElse();
}
}
abstract class AuthStateInitial implements AuthState {
const factory AuthStateInitial() = _$AuthStateInitialImpl;
}
/// @nodoc
abstract class _$$AuthStateLoadingImplCopyWith<$Res> {
factory _$$AuthStateLoadingImplCopyWith(_$AuthStateLoadingImpl value,
$Res Function(_$AuthStateLoadingImpl) then) =
__$$AuthStateLoadingImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$AuthStateLoadingImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateLoadingImpl>
implements _$$AuthStateLoadingImplCopyWith<$Res> {
__$$AuthStateLoadingImplCopyWithImpl(_$AuthStateLoadingImpl _value,
$Res Function(_$AuthStateLoadingImpl) _then)
: super(_value, _then);
}
/// @nodoc
class _$AuthStateLoadingImpl implements AuthStateLoading {
const _$AuthStateLoadingImpl();
@override
String toString() {
return 'AuthState.loading()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$AuthStateLoadingImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) {
return loading();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) {
return loading?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) {
return loading(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) {
return loading?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) {
if (loading != null) {
return loading(this);
}
return orElse();
}
}
abstract class AuthStateLoading implements AuthState {
const factory AuthStateLoading() = _$AuthStateLoadingImpl;
}
/// @nodoc
abstract class _$$AuthStateAuthenticatedImplCopyWith<$Res> {
factory _$$AuthStateAuthenticatedImplCopyWith(
_$AuthStateAuthenticatedImpl value,
$Res Function(_$AuthStateAuthenticatedImpl) then) =
__$$AuthStateAuthenticatedImplCopyWithImpl<$Res>;
@useResult
$Res call({User user});
}
/// @nodoc
class __$$AuthStateAuthenticatedImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateAuthenticatedImpl>
implements _$$AuthStateAuthenticatedImplCopyWith<$Res> {
__$$AuthStateAuthenticatedImplCopyWithImpl(
_$AuthStateAuthenticatedImpl _value,
$Res Function(_$AuthStateAuthenticatedImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? user = null,
}) {
return _then(_$AuthStateAuthenticatedImpl(
null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as User,
));
}
}
/// @nodoc
class _$AuthStateAuthenticatedImpl implements AuthStateAuthenticated {
const _$AuthStateAuthenticatedImpl(this.user);
@override
final User user;
@override
String toString() {
return 'AuthState.authenticated(user: $user)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthStateAuthenticatedImpl &&
(identical(other.user, user) || other.user == user));
}
@override
int get hashCode => Object.hash(runtimeType, user);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AuthStateAuthenticatedImplCopyWith<_$AuthStateAuthenticatedImpl>
get copyWith => __$$AuthStateAuthenticatedImplCopyWithImpl<
_$AuthStateAuthenticatedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) {
return authenticated(user);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) {
return authenticated?.call(user);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (authenticated != null) {
return authenticated(user);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) {
return authenticated(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) {
return authenticated?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) {
if (authenticated != null) {
return authenticated(this);
}
return orElse();
}
}
abstract class AuthStateAuthenticated implements AuthState {
const factory AuthStateAuthenticated(final User user) =
_$AuthStateAuthenticatedImpl;
User get user;
@JsonKey(ignore: true)
_$$AuthStateAuthenticatedImplCopyWith<_$AuthStateAuthenticatedImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$AuthStateUnauthenticatedImplCopyWith<$Res> {
factory _$$AuthStateUnauthenticatedImplCopyWith(
_$AuthStateUnauthenticatedImpl value,
$Res Function(_$AuthStateUnauthenticatedImpl) then) =
__$$AuthStateUnauthenticatedImplCopyWithImpl<$Res>;
@useResult
$Res call({String? message});
}
/// @nodoc
class __$$AuthStateUnauthenticatedImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateUnauthenticatedImpl>
implements _$$AuthStateUnauthenticatedImplCopyWith<$Res> {
__$$AuthStateUnauthenticatedImplCopyWithImpl(
_$AuthStateUnauthenticatedImpl _value,
$Res Function(_$AuthStateUnauthenticatedImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = freezed,
}) {
return _then(_$AuthStateUnauthenticatedImpl(
freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$AuthStateUnauthenticatedImpl implements AuthStateUnauthenticated {
const _$AuthStateUnauthenticatedImpl([this.message]);
@override
final String? message;
@override
String toString() {
return 'AuthState.unauthenticated(message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthStateUnauthenticatedImpl &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, message);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AuthStateUnauthenticatedImplCopyWith<_$AuthStateUnauthenticatedImpl>
get copyWith => __$$AuthStateUnauthenticatedImplCopyWithImpl<
_$AuthStateUnauthenticatedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) {
return unauthenticated(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) {
return unauthenticated?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (unauthenticated != null) {
return unauthenticated(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) {
return unauthenticated(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) {
return unauthenticated?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) {
if (unauthenticated != null) {
return unauthenticated(this);
}
return orElse();
}
}
abstract class AuthStateUnauthenticated implements AuthState {
const factory AuthStateUnauthenticated([final String? message]) =
_$AuthStateUnauthenticatedImpl;
String? get message;
@JsonKey(ignore: true)
_$$AuthStateUnauthenticatedImplCopyWith<_$AuthStateUnauthenticatedImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$AuthStateErrorImplCopyWith<$Res> {
factory _$$AuthStateErrorImplCopyWith(_$AuthStateErrorImpl value,
$Res Function(_$AuthStateErrorImpl) then) =
__$$AuthStateErrorImplCopyWithImpl<$Res>;
@useResult
$Res call({String message});
}
/// @nodoc
class __$$AuthStateErrorImplCopyWithImpl<$Res>
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateErrorImpl>
implements _$$AuthStateErrorImplCopyWith<$Res> {
__$$AuthStateErrorImplCopyWithImpl(
_$AuthStateErrorImpl _value, $Res Function(_$AuthStateErrorImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
}) {
return _then(_$AuthStateErrorImpl(
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$AuthStateErrorImpl implements AuthStateError {
const _$AuthStateErrorImpl(this.message);
@override
final String message;
@override
String toString() {
return 'AuthState.error(message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthStateErrorImpl &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, message);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AuthStateErrorImplCopyWith<_$AuthStateErrorImpl> get copyWith =>
__$$AuthStateErrorImplCopyWithImpl<_$AuthStateErrorImpl>(
this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(User user) authenticated,
required TResult Function(String? message) unauthenticated,
required TResult Function(String message) error,
}) {
return error(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(User user)? authenticated,
TResult? Function(String? message)? unauthenticated,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(User user)? authenticated,
TResult Function(String? message)? unauthenticated,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(AuthStateInitial value) initial,
required TResult Function(AuthStateLoading value) loading,
required TResult Function(AuthStateAuthenticated value) authenticated,
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
required TResult Function(AuthStateError value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(AuthStateInitial value)? initial,
TResult? Function(AuthStateLoading value)? loading,
TResult? Function(AuthStateAuthenticated value)? authenticated,
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
TResult? Function(AuthStateError value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(AuthStateInitial value)? initial,
TResult Function(AuthStateLoading value)? loading,
TResult Function(AuthStateAuthenticated value)? authenticated,
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
TResult Function(AuthStateError value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
}
abstract class AuthStateError implements AuthState {
const factory AuthStateError(final String message) = _$AuthStateErrorImpl;
String get message;
@JsonKey(ignore: true)
_$$AuthStateErrorImplCopyWith<_$AuthStateErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
/// Reusable primary button specifically designed for authentication actions
/// Provides consistent styling, loading states, and accessibility features
class AuthButton extends StatelessWidget {
const AuthButton({
super.key,
required this.onPressed,
required this.text,
this.isLoading = false,
this.isEnabled = true,
this.type = AuthButtonType.filled,
this.icon,
this.width = double.infinity,
this.height = AppSpacing.buttonHeightLarge,
});
final VoidCallback? onPressed;
final String text;
final bool isLoading;
final bool isEnabled;
final AuthButtonType type;
final Widget? icon;
final double width;
final double height;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isButtonEnabled = isEnabled && !isLoading && onPressed != null;
Widget child = _buildButtonChild(theme, colorScheme);
return SizedBox(
width: width,
height: height,
child: AnimatedContainer(
duration: AppSpacing.animationNormal,
curve: Curves.easeInOut,
child: _buildButtonByType(context, child, isButtonEnabled),
),
);
}
Widget _buildButtonChild(ThemeData theme, ColorScheme colorScheme) {
if (isLoading) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: AppSpacing.iconSM,
height: AppSpacing.iconSM,
child: CircularProgressIndicator(
strokeWidth: 2,
color: _getLoadingIndicatorColor(colorScheme),
),
),
AppSpacing.horizontalSpaceSM,
Text(
'Please wait...',
style: _getTextStyle(theme, colorScheme),
),
],
);
}
if (icon != null) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconTheme(
data: IconThemeData(
color: _getIconColor(colorScheme),
size: AppSpacing.iconSM,
),
child: icon!,
),
AppSpacing.horizontalSpaceSM,
Flexible(
child: Text(
text,
style: _getTextStyle(theme, colorScheme),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
return Text(
text,
style: _getTextStyle(theme, colorScheme),
overflow: TextOverflow.ellipsis,
);
}
Widget _buildButtonByType(BuildContext context, Widget child, bool enabled) {
switch (type) {
case AuthButtonType.filled:
return FilledButton(
onPressed: enabled ? onPressed : null,
style: _getFilledButtonStyle(context),
child: child,
);
case AuthButtonType.outlined:
return OutlinedButton(
onPressed: enabled ? onPressed : null,
style: _getOutlinedButtonStyle(context),
child: child,
);
case AuthButtonType.text:
return TextButton(
onPressed: enabled ? onPressed : null,
style: _getTextButtonStyle(context),
child: child,
);
}
}
ButtonStyle _getFilledButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return FilledButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
disabledBackgroundColor: colorScheme.onSurface.withValues(alpha: 0.12),
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
elevation: AppSpacing.elevationNone,
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
vertical: AppSpacing.buttonPaddingVertical,
),
tapTargetSize: MaterialTapTargetSize.padded,
).copyWith(
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return colorScheme.onPrimary.withValues(alpha: 0.1);
}
if (states.contains(WidgetState.hovered)) {
return colorScheme.onPrimary.withValues(alpha: 0.08);
}
if (states.contains(WidgetState.focused)) {
return colorScheme.onPrimary.withValues(alpha: 0.1);
}
return null;
},
),
);
}
ButtonStyle _getOutlinedButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return OutlinedButton.styleFrom(
foregroundColor: colorScheme.primary,
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
side: BorderSide(
color: colorScheme.outline,
width: AppSpacing.borderWidth,
),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
vertical: AppSpacing.buttonPaddingVertical,
),
tapTargetSize: MaterialTapTargetSize.padded,
).copyWith(
side: WidgetStateProperty.resolveWith<BorderSide?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(
color: colorScheme.onSurface.withValues(alpha: 0.12),
width: AppSpacing.borderWidth,
);
}
if (states.contains(WidgetState.focused)) {
return BorderSide(
color: colorScheme.primary,
width: AppSpacing.borderWidthThick,
);
}
return BorderSide(
color: colorScheme.outline,
width: AppSpacing.borderWidth,
);
},
),
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return colorScheme.primary.withValues(alpha: 0.1);
}
if (states.contains(WidgetState.hovered)) {
return colorScheme.primary.withValues(alpha: 0.08);
}
if (states.contains(WidgetState.focused)) {
return colorScheme.primary.withValues(alpha: 0.1);
}
return null;
},
),
);
}
ButtonStyle _getTextButtonStyle(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return TextButton.styleFrom(
foregroundColor: colorScheme.primary,
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
shape: RoundedRectangleBorder(
borderRadius: AppSpacing.buttonRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.buttonPaddingHorizontal,
vertical: AppSpacing.buttonPaddingVertical,
),
tapTargetSize: MaterialTapTargetSize.padded,
).copyWith(
overlayColor: WidgetStateProperty.resolveWith<Color?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.pressed)) {
return colorScheme.primary.withValues(alpha: 0.1);
}
if (states.contains(WidgetState.hovered)) {
return colorScheme.primary.withValues(alpha: 0.08);
}
if (states.contains(WidgetState.focused)) {
return colorScheme.primary.withValues(alpha: 0.1);
}
return null;
},
),
);
}
TextStyle _getTextStyle(ThemeData theme, ColorScheme colorScheme) {
final baseStyle = theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
) ??
const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.1,
);
if (!isEnabled || isLoading) {
return baseStyle.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.38),
);
}
switch (type) {
case AuthButtonType.filled:
return baseStyle.copyWith(color: colorScheme.onPrimary);
case AuthButtonType.outlined:
case AuthButtonType.text:
return baseStyle.copyWith(color: colorScheme.primary);
}
}
Color _getLoadingIndicatorColor(ColorScheme colorScheme) {
switch (type) {
case AuthButtonType.filled:
return colorScheme.onPrimary;
case AuthButtonType.outlined:
case AuthButtonType.text:
return colorScheme.primary;
}
}
Color _getIconColor(ColorScheme colorScheme) {
if (!isEnabled) {
return colorScheme.onSurface.withValues(alpha: 0.38);
}
switch (type) {
case AuthButtonType.filled:
return colorScheme.onPrimary;
case AuthButtonType.outlined:
case AuthButtonType.text:
return colorScheme.primary;
}
}
}
/// Types of auth buttons available
enum AuthButtonType {
/// Filled button with primary color background
filled,
/// Outlined button with transparent background and border
outlined,
/// Text button with no background or border
text,
}

View File

@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/theme/app_spacing.dart';
/// Reusable styled text field specifically designed for authentication forms
/// Follows Material 3 design guidelines with consistent theming
class AuthTextField extends StatefulWidget {
const AuthTextField({
super.key,
required this.controller,
required this.labelText,
this.hintText,
this.prefixIcon,
this.suffixIcon,
this.validator,
this.keyboardType,
this.textInputAction,
this.onFieldSubmitted,
this.onChanged,
this.obscureText = false,
this.enabled = true,
this.autofillHints,
this.inputFormatters,
this.maxLength,
this.focusNode,
this.autofocus = false,
});
final TextEditingController controller;
final String labelText;
final String? hintText;
final Widget? prefixIcon;
final Widget? suffixIcon;
final String? Function(String?)? validator;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final void Function(String)? onFieldSubmitted;
final void Function(String)? onChanged;
final bool obscureText;
final bool enabled;
final List<String>? autofillHints;
final List<TextInputFormatter>? inputFormatters;
final int? maxLength;
final FocusNode? focusNode;
final bool autofocus;
@override
State<AuthTextField> createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<AuthTextField> {
bool _isFocused = false;
late FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onFocusChanged);
}
@override
void dispose() {
if (widget.focusNode == null) {
_focusNode.dispose();
} else {
_focusNode.removeListener(_onFocusChanged);
}
super.dispose();
}
void _onFocusChanged() {
setState(() {
_isFocused = _focusNode.hasFocus;
});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: widget.controller,
focusNode: _focusNode,
validator: widget.validator,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
onFieldSubmitted: widget.onFieldSubmitted,
onChanged: widget.onChanged,
obscureText: widget.obscureText,
enabled: widget.enabled,
autofillHints: widget.autofillHints,
inputFormatters: widget.inputFormatters,
maxLength: widget.maxLength,
autofocus: widget.autofocus,
style: theme.textTheme.bodyLarge?.copyWith(
color: widget.enabled ? colorScheme.onSurface : colorScheme.onSurface.withValues(alpha: 0.38),
),
decoration: InputDecoration(
labelText: widget.labelText,
hintText: widget.hintText,
prefixIcon: widget.prefixIcon != null
? Padding(
padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm),
child: IconTheme.merge(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
size: AppSpacing.iconMD,
),
child: widget.prefixIcon!,
),
)
: null,
suffixIcon: widget.suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: AppSpacing.md),
child: IconTheme.merge(
data: IconThemeData(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
size: AppSpacing.iconMD,
),
child: widget.suffixIcon!,
),
)
: null,
filled: true,
fillColor: widget.enabled
? (_isFocused
? colorScheme.primaryContainer.withValues(alpha: 0.08)
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5))
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.lg,
),
border: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.outline,
width: AppSpacing.borderWidth,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.6),
width: AppSpacing.borderWidth,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.primary,
width: AppSpacing.borderWidthThick,
),
),
errorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.error,
width: AppSpacing.borderWidth,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.error,
width: AppSpacing.borderWidthThick,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: AppSpacing.fieldRadius,
borderSide: BorderSide(
color: colorScheme.onSurface.withValues(alpha: 0.12),
width: AppSpacing.borderWidth,
),
),
labelStyle: theme.textTheme.bodyMedium?.copyWith(
color: _isFocused
? colorScheme.primary
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
hintStyle: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
errorStyle: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error,
),
counterStyle: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
// Enhanced visual feedback with subtle animations
cursorColor: colorScheme.primary,
cursorHeight: 24,
cursorWidth: 2,
),
],
);
}
}

View File

@@ -0,0 +1,7 @@
// Auth widgets exports
//
// This file exports all auth-related widgets for easy importing
// throughout the auth feature module.
export 'auth_button.dart';
export 'auth_text_field.dart';