add refresh token

This commit is contained in:
Phuoc Nguyen
2025-10-21 16:30:11 +07:00
parent b94a19dd3f
commit 9c20a44a04
21 changed files with 246 additions and 67 deletions

View File

@@ -18,8 +18,8 @@ abstract class AuthRemoteDataSource {
/// Get current user profile
Future<UserModel> getProfile();
/// Refresh access token
Future<AuthResponseModel> refreshToken();
/// Refresh access token using refresh token
Future<AuthResponseModel> refreshToken(String refreshToken);
}
/// Implementation of AuthRemoteDataSource
@@ -119,21 +119,28 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
}
@override
Future<AuthResponseModel> refreshToken() async {
Future<AuthResponseModel> refreshToken(String refreshToken) async {
try {
final response = await dioClient.post(ApiConstants.refreshToken);
print('📡 DataSource: Calling refresh token API...');
final response = await dioClient.post(
ApiConstants.refreshToken,
data: {'refreshToken': refreshToken},
);
if (response.statusCode == ApiConstants.statusOk) {
// API returns nested structure: {success, data: {access_token, user}, message}
// API returns nested structure: {success, data: {access_token, refresh_token, user}, message}
// Extract the 'data' object
final responseData = response.data['data'] as Map<String, dynamic>;
print('📡 DataSource: Token refreshed successfully');
return AuthResponseModel.fromJson(responseData);
} else {
throw ServerException('Token refresh failed with status: ${response.statusCode}');
}
} on DioException catch (e) {
print('❌ DataSource: Refresh token failed - ${e.message}');
throw _handleDioError(e);
} catch (e) {
print('❌ DataSource: Unexpected error refreshing token: $e');
throw ServerException('Unexpected error refreshing token: $e');
}
}

View File

@@ -5,6 +5,7 @@ import 'user_model.dart';
class AuthResponseModel extends AuthResponse {
const AuthResponseModel({
required super.accessToken,
required super.refreshToken,
required super.user,
});
@@ -12,6 +13,7 @@ class AuthResponseModel extends AuthResponse {
factory AuthResponseModel.fromJson(Map<String, dynamic> json) {
return AuthResponseModel(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
user: UserModel.fromJson(json['user'] as Map<String, dynamic>),
);
}
@@ -20,6 +22,7 @@ class AuthResponseModel extends AuthResponse {
Map<String, dynamic> toJson() {
return {
'access_token': accessToken,
'refresh_token': refreshToken,
'user': (user as UserModel).toJson(),
};
}
@@ -28,6 +31,7 @@ class AuthResponseModel extends AuthResponse {
factory AuthResponseModel.fromEntity(AuthResponse authResponse) {
return AuthResponseModel(
accessToken: authResponse.accessToken,
refreshToken: authResponse.refreshToken,
user: authResponse.user,
);
}
@@ -36,6 +40,7 @@ class AuthResponseModel extends AuthResponse {
AuthResponse toEntity() {
return AuthResponse(
accessToken: accessToken,
refreshToken: refreshToken,
user: user,
);
}

View File

@@ -35,12 +35,13 @@ class AuthRepositoryImpl implements AuthRepository {
print('🔐 Repository: Got response, token length=${authResponse.accessToken.length}');
// Save token to secure storage only if rememberMe is true
// Save tokens to secure storage only if rememberMe is true
if (rememberMe) {
await secureStorage.saveAccessToken(authResponse.accessToken);
print('🔐 Repository: Token saved to secure storage (persistent)');
await secureStorage.saveRefreshToken(authResponse.refreshToken);
print('🔐 Repository: Access token and refresh token saved to secure storage (persistent)');
} else {
print('🔐 Repository: Token NOT saved (session only - rememberMe is false)');
print('🔐 Repository: Tokens NOT saved (session only - rememberMe is false)');
}
// Set token in Dio client for subsequent requests (always for current session)
@@ -86,8 +87,9 @@ class AuthRepositoryImpl implements AuthRepository {
);
final authResponse = await remoteDataSource.register(registerDto);
// Save token to secure storage
// Save both tokens to secure storage
await secureStorage.saveAccessToken(authResponse.accessToken);
await secureStorage.saveRefreshToken(authResponse.refreshToken);
// Set token in Dio client for subsequent requests
dioClient.setAuthToken(authResponse.accessToken);
@@ -127,24 +129,44 @@ class AuthRepositoryImpl implements AuthRepository {
@override
Future<Either<Failure, AuthResponse>> refreshToken() async {
try {
final authResponse = await remoteDataSource.refreshToken();
print('🔄 Repository: Starting token refresh...');
// Update token in secure storage
// Get refresh token from storage
final storedRefreshToken = await secureStorage.getRefreshToken();
if (storedRefreshToken == null) {
print('❌ Repository: No refresh token found in storage');
return const Left(UnauthorizedFailure('No refresh token available'));
}
print('🔄 Repository: Calling datasource with refresh token...');
final authResponse = await remoteDataSource.refreshToken(storedRefreshToken);
// Update both tokens in secure storage (token rotation)
await secureStorage.saveAccessToken(authResponse.accessToken);
await secureStorage.saveRefreshToken(authResponse.refreshToken);
print('🔄 Repository: New tokens saved to secure storage');
// Update token in Dio client
dioClient.setAuthToken(authResponse.accessToken);
print('🔄 Repository: New access token set in DioClient');
return Right(authResponse);
} on UnauthorizedException catch (e) {
print('❌ Repository: Unauthorized during refresh - ${e.message}');
// Clear invalid tokens
await secureStorage.deleteAllTokens();
return Left(UnauthorizedFailure(e.message));
} on TokenExpiredException catch (e) {
print('❌ Repository: Token expired during refresh - ${e.message}');
// Clear expired tokens
await secureStorage.deleteAllTokens();
return Left(TokenExpiredFailure(e.message));
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message));
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
print('❌ Repository: Unexpected error during refresh: $e');
return Left(ServerFailure('Unexpected error: $e'));
}
}

View File

@@ -4,13 +4,15 @@ import 'user.dart';
/// Authentication response entity
class AuthResponse extends Equatable {
final String accessToken;
final String refreshToken;
final User user;
const AuthResponse({
required this.accessToken,
required this.refreshToken,
required this.user,
});
@override
List<Object?> get props => [accessToken, user];
List<Object?> get props => [accessToken, refreshToken, user];
}

View File

@@ -25,10 +25,10 @@ class CategoryModel extends HiveObject {
final int productCount;
@HiveField(6)
final DateTime createdAt;
final DateTime? createdAt;
@HiveField(7)
final DateTime updatedAt;
final DateTime? updatedAt;
CategoryModel({
required this.id,
@@ -37,8 +37,8 @@ class CategoryModel extends HiveObject {
this.iconPath,
this.color,
required this.productCount,
required this.createdAt,
required this.updatedAt,
this.createdAt,
this.updatedAt,
});
/// Convert to domain entity
@@ -78,8 +78,12 @@ class CategoryModel extends HiveObject {
iconPath: json['iconPath'] as String?,
color: json['color'] as String?,
productCount: json['productCount'] as int? ?? 0,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
@@ -92,8 +96,8 @@ class CategoryModel extends HiveObject {
'iconPath': iconPath,
'color': color,
'productCount': productCount,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}

View File

@@ -23,8 +23,8 @@ class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
iconPath: fields[3] as String?,
color: fields[4] as String?,
productCount: (fields[5] as num).toInt(),
createdAt: fields[6] as DateTime,
updatedAt: fields[7] as DateTime,
createdAt: fields[6] as DateTime?,
updatedAt: fields[7] as DateTime?,
);
}

View File

@@ -8,8 +8,8 @@ class Category extends Equatable {
final String? iconPath;
final String? color;
final int productCount;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? createdAt;
final DateTime? updatedAt;
const Category({
required this.id,
@@ -18,8 +18,8 @@ class Category extends Equatable {
this.iconPath,
this.color,
required this.productCount,
required this.createdAt,
required this.updatedAt,
this.createdAt,
this.updatedAt,
});
@override

View File

@@ -31,10 +31,10 @@ class ProductModel extends HiveObject {
final bool isAvailable;
@HiveField(8)
final DateTime createdAt;
final DateTime? createdAt;
@HiveField(9)
final DateTime updatedAt;
final DateTime? updatedAt;
ProductModel({
required this.id,
@@ -45,8 +45,8 @@ class ProductModel extends HiveObject {
required this.categoryId,
required this.stockQuantity,
required this.isAvailable,
required this.createdAt,
required this.updatedAt,
this.createdAt,
this.updatedAt,
});
/// Convert to domain entity
@@ -92,8 +92,12 @@ class ProductModel extends HiveObject {
categoryId: json['categoryId'] as String,
stockQuantity: json['stockQuantity'] as int? ?? 0,
isAvailable: json['isAvailable'] as bool? ?? true,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
);
}
@@ -108,8 +112,8 @@ class ProductModel extends HiveObject {
'categoryId': categoryId,
'stockQuantity': stockQuantity,
'isAvailable': isAvailable,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
}
}

View File

@@ -25,8 +25,8 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
categoryId: fields[5] as String,
stockQuantity: (fields[6] as num).toInt(),
isAvailable: fields[7] as bool,
createdAt: fields[8] as DateTime,
updatedAt: fields[9] as DateTime,
createdAt: fields[8] as DateTime?,
updatedAt: fields[9] as DateTime?,
);
}

View File

@@ -10,8 +10,8 @@ class Product extends Equatable {
final String categoryId;
final int stockQuantity;
final bool isAvailable;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? createdAt;
final DateTime? updatedAt;
const Product({
required this.id,
@@ -22,8 +22,8 @@ class Product extends Equatable {
required this.categoryId,
required this.stockQuantity,
required this.isAvailable,
required this.createdAt,
required this.updatedAt,
this.createdAt,
this.updatedAt,
});
@override

View File

@@ -296,14 +296,18 @@ class ProductDetailPage extends ConsumerWidget {
context,
icon: Icons.calendar_today,
label: 'Created',
value: dateFormat.format(product.createdAt),
value: product.createdAt != null
? dateFormat.format(product.createdAt!)
: 'N/A',
),
const Divider(height: 24),
_buildInfoRow(
context,
icon: Icons.update,
label: 'Last Updated',
value: dateFormat.format(product.updatedAt),
value: product.updatedAt != null
? dateFormat.format(product.updatedAt!)
: 'N/A',
),
],
),

View File

@@ -493,10 +493,18 @@ class _ProductsPageState extends ConsumerState<ProductsPage> {
sorted.sort((a, b) => b.price.compareTo(a.price));
break;
case ProductSortOption.newest:
sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
sorted.sort((a, b) {
final aDate = a.createdAt ?? DateTime(2000);
final bDate = b.createdAt ?? DateTime(2000);
return bDate.compareTo(aDate);
});
break;
case ProductSortOption.oldest:
sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
sorted.sort((a, b) {
final aDate = a.createdAt ?? DateTime(2000);
final bDate = b.createdAt ?? DateTime(2000);
return aDate.compareTo(bDate);
});
break;
}

View File

@@ -91,10 +91,18 @@ class SortedProducts extends _$SortedProducts {
sorted.sort((a, b) => b.price.compareTo(a.price));
break;
case ProductSortOption.newest:
sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
sorted.sort((a, b) {
final aDate = a.createdAt ?? DateTime(2000);
final bDate = b.createdAt ?? DateTime(2000);
return bDate.compareTo(aDate);
});
break;
case ProductSortOption.oldest:
sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
sorted.sort((a, b) {
final aDate = a.createdAt ?? DateTime(2000);
final bDate = b.createdAt ?? DateTime(2000);
return aDate.compareTo(bDate);
});
break;
}

View File

@@ -131,7 +131,7 @@ final class SortedProductsProvider
}
}
String _$sortedProductsHash() => r'653f1e9af8c188631dadbfe9ed7d944c6876d2d3';
String _$sortedProductsHash() => r'8a526ae12a15ca7decc8880ebbd083df455875a8';
/// Provider for sorted products
/// Adds sorting capability on top of filtered products