fix settings
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
232
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
232
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/features/auth/data/models/user_model.dart
Normal file
42
lib/features/auth/data/models/user_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
259
lib/features/auth/data/models/user_model.freezed.dart
Normal file
259
lib/features/auth/data/models/user_model.freezed.dart
Normal 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;
|
||||
}
|
||||
29
lib/features/auth/data/models/user_model.g.dart
Normal file
29
lib/features/auth/data/models/user_model.g.dart
Normal 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(),
|
||||
};
|
||||
232
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
232
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
28
lib/features/auth/domain/entities/user.dart
Normal file
28
lib/features/auth/domain/entities/user.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
48
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
48
lib/features/auth/domain/repositories/auth_repository.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
43
lib/features/auth/domain/usecases/login_usecase.dart
Normal file
43
lib/features/auth/domain/usecases/login_usecase.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
15
lib/features/auth/domain/usecases/logout_usecase.dart
Normal file
15
lib/features/auth/domain/usecases/logout_usecase.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
555
lib/features/auth/presentation/pages/login_page.dart
Normal file
555
lib/features/auth/presentation/pages/login_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
7
lib/features/auth/presentation/pages/pages.dart
Normal file
7
lib/features/auth/presentation/pages/pages.dart
Normal 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';
|
||||
685
lib/features/auth/presentation/pages/register_page.dart
Normal file
685
lib/features/auth/presentation/pages/register_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
145
lib/features/auth/presentation/providers/auth_providers.dart
Normal file
145
lib/features/auth/presentation/providers/auth_providers.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
150
lib/features/auth/presentation/providers/auth_providers.g.dart
Normal file
150
lib/features/auth/presentation/providers/auth_providers.g.dart
Normal 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
|
||||
13
lib/features/auth/presentation/providers/auth_state.dart
Normal file
13
lib/features/auth/presentation/providers/auth_state.dart
Normal 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;
|
||||
}
|
||||
794
lib/features/auth/presentation/providers/auth_state.freezed.dart
Normal file
794
lib/features/auth/presentation/providers/auth_state.freezed.dart
Normal 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;
|
||||
}
|
||||
308
lib/features/auth/presentation/widgets/auth_button.dart
Normal file
308
lib/features/auth/presentation/widgets/auth_button.dart
Normal 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,
|
||||
}
|
||||
209
lib/features/auth/presentation/widgets/auth_text_field.dart
Normal file
209
lib/features/auth/presentation/widgets/auth_text_field.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/features/auth/presentation/widgets/widgets.dart
Normal file
7
lib/features/auth/presentation/widgets/widgets.dart
Normal 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';
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/routing/route_paths.dart';
|
||||
import '../../../../core/routing/route_guards.dart';
|
||||
import '../../../../shared/presentation/providers/app_providers.dart';
|
||||
import '../../../auth/presentation/providers/auth_providers.dart';
|
||||
|
||||
/// Main settings page with theme switcher and navigation to other settings
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
@@ -96,9 +97,22 @@ class _ThemeSection extends StatelessWidget {
|
||||
Icons.palette_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: const Text('Theme'),
|
||||
subtitle: Text(_getThemeModeText(themeMode)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text(
|
||||
'Theme',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getThemeModeText(themeMode),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.settingsTheme),
|
||||
),
|
||||
ListTile(
|
||||
@@ -110,8 +124,18 @@ class _ThemeSection extends StatelessWidget {
|
||||
: Icons.brightness_auto,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: const Text('Quick Theme Toggle'),
|
||||
subtitle: const Text('Switch between light and dark mode'),
|
||||
title: Text(
|
||||
'Quick Theme Toggle',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Switch between light and dark mode',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Switch(
|
||||
value: themeMode == ThemeMode.dark,
|
||||
onChanged: (value) {
|
||||
@@ -152,32 +176,96 @@ class _AccountSection extends StatelessWidget {
|
||||
children: [
|
||||
if (authState == AuthState.authenticated) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: const Text('Profile'),
|
||||
subtitle: const Text('Manage your profile information'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Profile',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Manage your profile information',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.profile),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: const Text('Sign Out'),
|
||||
subtitle: const Text('Sign out of your account'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.logout,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Sign Out',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Sign out of your account',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => _showSignOutDialog(context, ref),
|
||||
),
|
||||
] else ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.login),
|
||||
title: const Text('Sign In'),
|
||||
subtitle: const Text('Sign in to your account'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Sign In',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Sign in to your account',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.login),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add_outlined),
|
||||
title: const Text('Create Account'),
|
||||
subtitle: const Text('Sign up for a new account'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.person_add_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Create Account',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Sign up for a new account',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.register),
|
||||
),
|
||||
],
|
||||
@@ -200,7 +288,7 @@ class _AccountSection extends StatelessWidget {
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
ref.read(authStateProvider.notifier).logout();
|
||||
ref.read(authNotifierProvider.notifier).logout();
|
||||
},
|
||||
child: const Text('Sign Out'),
|
||||
),
|
||||
@@ -217,17 +305,49 @@ class _AppSettingsSection extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_outlined),
|
||||
title: const Text('Notifications'),
|
||||
subtitle: const Text('Manage notification preferences'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Notifications',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Manage notification preferences',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.settingsNotifications),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: const Text('Language'),
|
||||
subtitle: const Text('English (United States)'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.language,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Language',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'English (United States)',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Language settings coming soon!')),
|
||||
@@ -235,10 +355,26 @@ class _AppSettingsSection extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.storage_outlined),
|
||||
title: const Text('Storage'),
|
||||
subtitle: const Text('Manage local data and cache'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.storage_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Storage',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Manage local data and cache',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Storage settings coming soon!')),
|
||||
@@ -256,17 +392,49 @@ class _PrivacySection extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.privacy_tip_outlined),
|
||||
title: const Text('Privacy'),
|
||||
subtitle: const Text('Privacy settings and data protection'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.privacy_tip_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Privacy',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Privacy settings and data protection',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.settingsPrivacy),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.security),
|
||||
title: const Text('Security'),
|
||||
subtitle: const Text('App security and permissions'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.security,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Security',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'App security and permissions',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Security settings coming soon!')),
|
||||
@@ -284,17 +452,49 @@ class _AboutSection extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outlined),
|
||||
title: const Text('About'),
|
||||
subtitle: const Text('App version and information'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.info_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'About',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'App version and information',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () => context.push(RoutePaths.about),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.help_outline),
|
||||
title: const Text('Help & Support'),
|
||||
subtitle: const Text('Get help and contact support'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.help_outline,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Help & Support',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Get help and contact support',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Help & Support coming soon!')),
|
||||
@@ -302,10 +502,26 @@ class _AboutSection extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.article_outlined),
|
||||
title: const Text('Terms of Service'),
|
||||
subtitle: const Text('View terms and conditions'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.article_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Terms of Service',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'View terms and conditions',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Terms of Service coming soon!')),
|
||||
@@ -313,10 +529,26 @@ class _AboutSection extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.policy_outlined),
|
||||
title: const Text('Privacy Policy'),
|
||||
subtitle: const Text('View privacy policy'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
leading: Icon(
|
||||
Icons.policy_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
'Privacy Policy',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'View privacy policy',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Privacy Policy coming soon!')),
|
||||
|
||||
Reference in New Issue
Block a user