fix settings

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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