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

File diff suppressed because one or more lines are too long

View File

@@ -31,11 +31,11 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -1,13 +1,16 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../constants/api_constants.dart'; import '../constants/api_constants.dart';
import '../storage/secure_storage.dart';
import 'api_interceptor.dart'; import 'api_interceptor.dart';
import 'refresh_token_interceptor.dart';
/// Dio HTTP client configuration /// Dio HTTP client configuration
class DioClient { class DioClient {
late final Dio _dio; late final Dio _dio;
String? _authToken; String? _authToken;
final SecureStorage? secureStorage;
DioClient() { DioClient({this.secureStorage}) {
_dio = Dio( _dio = Dio(
BaseOptions( BaseOptions(
baseUrl: ApiConstants.fullBaseUrl, baseUrl: ApiConstants.fullBaseUrl,
@@ -34,6 +37,17 @@ class DioClient {
}, },
), ),
); );
// Add refresh token interceptor (if secureStorage is provided)
if (secureStorage != null) {
_dio.interceptors.add(
RefreshTokenInterceptor(
dio: _dio,
secureStorage: secureStorage!,
),
);
print('🔧 DioClient: Refresh token interceptor added');
}
} }
Dio get dio => _dio; Dio get dio => _dio;

View File

@@ -0,0 +1,104 @@
import 'package:dio/dio.dart';
import '../constants/api_constants.dart';
import '../storage/secure_storage.dart';
/// Interceptor to handle automatic token refresh on 401 errors
class RefreshTokenInterceptor extends Interceptor {
final Dio dio;
final SecureStorage secureStorage;
// To prevent infinite loop of refresh attempts
bool _isRefreshing = false;
RefreshTokenInterceptor({
required this.dio,
required this.secureStorage,
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Check if error is 401 Unauthorized
if (err.response?.statusCode == 401) {
print('🔄 Interceptor: Got 401 error, attempting token refresh...');
// Avoid infinite refresh loop
if (_isRefreshing) {
print('❌ Interceptor: Already refreshing, skip');
return handler.next(err);
}
// Check if this is NOT the refresh token endpoint itself
final requestPath = err.requestOptions.path;
if (requestPath.contains('refresh')) {
print('❌ Interceptor: 401 on refresh endpoint, cannot retry');
// Clear tokens as refresh token is invalid
await secureStorage.deleteAllTokens();
return handler.next(err);
}
try {
_isRefreshing = true;
// Get refresh token from storage
final refreshToken = await secureStorage.getRefreshToken();
if (refreshToken == null) {
print('❌ Interceptor: No refresh token available');
await secureStorage.deleteAllTokens();
return handler.next(err);
}
print('🔄 Interceptor: Calling refresh token API...');
// Call refresh token API
final response = await dio.post(
ApiConstants.refreshToken,
data: {'refreshToken': refreshToken},
options: Options(
headers: {
// Don't include auth header for refresh request
ApiConstants.authorization: null,
},
),
);
if (response.statusCode == 200) {
// Extract new tokens from response
final responseData = response.data['data'] as Map<String, dynamic>;
final newAccessToken = responseData['access_token'] as String;
final newRefreshToken = responseData['refresh_token'] as String;
print('✅ Interceptor: Got new tokens, saving...');
// Save new tokens
await secureStorage.saveAccessToken(newAccessToken);
await secureStorage.saveRefreshToken(newRefreshToken);
// Update the failed request with new token
err.requestOptions.headers[ApiConstants.authorization] = 'Bearer $newAccessToken';
print('🔄 Interceptor: Retrying original request...');
// Retry the original request
final retryResponse = await dio.fetch(err.requestOptions);
print('✅ Interceptor: Original request succeeded after refresh');
_isRefreshing = false;
return handler.resolve(retryResponse);
} else {
print('❌ Interceptor: Refresh token API returned ${response.statusCode}');
await secureStorage.deleteAllTokens();
_isRefreshing = false;
return handler.next(err);
}
} catch (e) {
print('❌ Interceptor: Error during token refresh: $e');
await secureStorage.deleteAllTokens();
_isRefreshing = false;
return handler.next(err);
}
}
// Not a 401 error, pass through
return handler.next(err);
}
}

View File

@@ -7,10 +7,12 @@ part 'core_providers.g.dart';
/// Provider for DioClient (singleton) /// Provider for DioClient (singleton)
/// ///
/// This is the global HTTP client used across the entire app. /// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection. /// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
DioClient dioClient(Ref ref) { DioClient dioClient(Ref ref) {
return DioClient(); final storage = ref.watch(secureStorageProvider);
return DioClient(secureStorage: storage);
} }
/// Provider for SecureStorage (singleton) /// Provider for SecureStorage (singleton)

View File

@@ -11,7 +11,8 @@ part of 'core_providers.dart';
/// Provider for DioClient (singleton) /// Provider for DioClient (singleton)
/// ///
/// This is the global HTTP client used across the entire app. /// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection. /// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
@ProviderFor(dioClient) @ProviderFor(dioClient)
const dioClientProvider = DioClientProvider._(); const dioClientProvider = DioClientProvider._();
@@ -19,7 +20,8 @@ const dioClientProvider = DioClientProvider._();
/// Provider for DioClient (singleton) /// Provider for DioClient (singleton)
/// ///
/// This is the global HTTP client used across the entire app. /// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection. /// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
final class DioClientProvider final class DioClientProvider
extends $FunctionalProvider<DioClient, DioClient, DioClient> extends $FunctionalProvider<DioClient, DioClient, DioClient>
@@ -27,7 +29,8 @@ final class DioClientProvider
/// Provider for DioClient (singleton) /// Provider for DioClient (singleton)
/// ///
/// This is the global HTTP client used across the entire app. /// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection. /// It's configured with interceptors, timeout settings, auth token injection,
/// and automatic token refresh on 401 errors.
const DioClientProvider._() const DioClientProvider._()
: super( : super(
from: null, from: null,
@@ -61,7 +64,7 @@ final class DioClientProvider
} }
} }
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d'; String _$dioClientHash() => r'a9edc35e0e918bfa8e6c4e3ecd72412fba383cb2';
/// Provider for SecureStorage (singleton) /// Provider for SecureStorage (singleton)
/// ///

View File

@@ -18,8 +18,8 @@ abstract class AuthRemoteDataSource {
/// Get current user profile /// Get current user profile
Future<UserModel> getProfile(); Future<UserModel> getProfile();
/// Refresh access token /// Refresh access token using refresh token
Future<AuthResponseModel> refreshToken(); Future<AuthResponseModel> refreshToken(String refreshToken);
} }
/// Implementation of AuthRemoteDataSource /// Implementation of AuthRemoteDataSource
@@ -119,21 +119,28 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
} }
@override @override
Future<AuthResponseModel> refreshToken() async { Future<AuthResponseModel> refreshToken(String refreshToken) async {
try { 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) { 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 // Extract the 'data' object
final responseData = response.data['data'] as Map<String, dynamic>; final responseData = response.data['data'] as Map<String, dynamic>;
print('📡 DataSource: Token refreshed successfully');
return AuthResponseModel.fromJson(responseData); return AuthResponseModel.fromJson(responseData);
} else { } else {
throw ServerException('Token refresh failed with status: ${response.statusCode}'); throw ServerException('Token refresh failed with status: ${response.statusCode}');
} }
} on DioException catch (e) { } on DioException catch (e) {
print('❌ DataSource: Refresh token failed - ${e.message}');
throw _handleDioError(e); throw _handleDioError(e);
} catch (e) { } catch (e) {
print('❌ DataSource: Unexpected error refreshing token: $e');
throw ServerException('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 { class AuthResponseModel extends AuthResponse {
const AuthResponseModel({ const AuthResponseModel({
required super.accessToken, required super.accessToken,
required super.refreshToken,
required super.user, required super.user,
}); });
@@ -12,6 +13,7 @@ class AuthResponseModel extends AuthResponse {
factory AuthResponseModel.fromJson(Map<String, dynamic> json) { factory AuthResponseModel.fromJson(Map<String, dynamic> json) {
return AuthResponseModel( return AuthResponseModel(
accessToken: json['access_token'] as String, accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
user: UserModel.fromJson(json['user'] as Map<String, dynamic>), user: UserModel.fromJson(json['user'] as Map<String, dynamic>),
); );
} }
@@ -20,6 +22,7 @@ class AuthResponseModel extends AuthResponse {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'access_token': accessToken, 'access_token': accessToken,
'refresh_token': refreshToken,
'user': (user as UserModel).toJson(), 'user': (user as UserModel).toJson(),
}; };
} }
@@ -28,6 +31,7 @@ class AuthResponseModel extends AuthResponse {
factory AuthResponseModel.fromEntity(AuthResponse authResponse) { factory AuthResponseModel.fromEntity(AuthResponse authResponse) {
return AuthResponseModel( return AuthResponseModel(
accessToken: authResponse.accessToken, accessToken: authResponse.accessToken,
refreshToken: authResponse.refreshToken,
user: authResponse.user, user: authResponse.user,
); );
} }
@@ -36,6 +40,7 @@ class AuthResponseModel extends AuthResponse {
AuthResponse toEntity() { AuthResponse toEntity() {
return AuthResponse( return AuthResponse(
accessToken: accessToken, accessToken: accessToken,
refreshToken: refreshToken,
user: user, user: user,
); );
} }

View File

@@ -35,12 +35,13 @@ class AuthRepositoryImpl implements AuthRepository {
print('🔐 Repository: Got response, token length=${authResponse.accessToken.length}'); 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) { if (rememberMe) {
await secureStorage.saveAccessToken(authResponse.accessToken); 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 { } 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) // 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); final authResponse = await remoteDataSource.register(registerDto);
// Save token to secure storage // Save both tokens to secure storage
await secureStorage.saveAccessToken(authResponse.accessToken); await secureStorage.saveAccessToken(authResponse.accessToken);
await secureStorage.saveRefreshToken(authResponse.refreshToken);
// Set token in Dio client for subsequent requests // Set token in Dio client for subsequent requests
dioClient.setAuthToken(authResponse.accessToken); dioClient.setAuthToken(authResponse.accessToken);
@@ -127,24 +129,44 @@ class AuthRepositoryImpl implements AuthRepository {
@override @override
Future<Either<Failure, AuthResponse>> refreshToken() async { Future<Either<Failure, AuthResponse>> refreshToken() async {
try { 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.saveAccessToken(authResponse.accessToken);
await secureStorage.saveRefreshToken(authResponse.refreshToken);
print('🔄 Repository: New tokens saved to secure storage');
// Update token in Dio client // Update token in Dio client
dioClient.setAuthToken(authResponse.accessToken); dioClient.setAuthToken(authResponse.accessToken);
print('🔄 Repository: New access token set in DioClient');
return Right(authResponse); return Right(authResponse);
} on UnauthorizedException catch (e) { } on UnauthorizedException catch (e) {
print('❌ Repository: Unauthorized during refresh - ${e.message}');
// Clear invalid tokens
await secureStorage.deleteAllTokens();
return Left(UnauthorizedFailure(e.message)); return Left(UnauthorizedFailure(e.message));
} on TokenExpiredException catch (e) { } on TokenExpiredException catch (e) {
print('❌ Repository: Token expired during refresh - ${e.message}');
// Clear expired tokens
await secureStorage.deleteAllTokens();
return Left(TokenExpiredFailure(e.message)); return Left(TokenExpiredFailure(e.message));
} on NetworkException catch (e) { } on NetworkException catch (e) {
return Left(NetworkFailure(e.message)); return Left(NetworkFailure(e.message));
} on ServerException catch (e) { } on ServerException catch (e) {
return Left(ServerFailure(e.message)); return Left(ServerFailure(e.message));
} catch (e) { } catch (e) {
print('❌ Repository: Unexpected error during refresh: $e');
return Left(ServerFailure('Unexpected error: $e')); return Left(ServerFailure('Unexpected error: $e'));
} }
} }

View File

@@ -4,13 +4,15 @@ import 'user.dart';
/// Authentication response entity /// Authentication response entity
class AuthResponse extends Equatable { class AuthResponse extends Equatable {
final String accessToken; final String accessToken;
final String refreshToken;
final User user; final User user;
const AuthResponse({ const AuthResponse({
required this.accessToken, required this.accessToken,
required this.refreshToken,
required this.user, required this.user,
}); });
@override @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; final int productCount;
@HiveField(6) @HiveField(6)
final DateTime createdAt; final DateTime? createdAt;
@HiveField(7) @HiveField(7)
final DateTime updatedAt; final DateTime? updatedAt;
CategoryModel({ CategoryModel({
required this.id, required this.id,
@@ -37,8 +37,8 @@ class CategoryModel extends HiveObject {
this.iconPath, this.iconPath,
this.color, this.color,
required this.productCount, required this.productCount,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
}); });
/// Convert to domain entity /// Convert to domain entity
@@ -78,8 +78,12 @@ class CategoryModel extends HiveObject {
iconPath: json['iconPath'] as String?, iconPath: json['iconPath'] as String?,
color: json['color'] as String?, color: json['color'] as String?,
productCount: json['productCount'] as int? ?? 0, productCount: json['productCount'] as int? ?? 0,
createdAt: DateTime.parse(json['createdAt'] as String), createdAt: json['createdAt'] != null
updatedAt: DateTime.parse(json['updatedAt'] as String), ? 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, 'iconPath': iconPath,
'color': color, 'color': color,
'productCount': productCount, 'productCount': productCount,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(), 'updatedAt': updatedAt?.toIso8601String(),
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -296,14 +296,18 @@ class ProductDetailPage extends ConsumerWidget {
context, context,
icon: Icons.calendar_today, icon: Icons.calendar_today,
label: 'Created', label: 'Created',
value: dateFormat.format(product.createdAt), value: product.createdAt != null
? dateFormat.format(product.createdAt!)
: 'N/A',
), ),
const Divider(height: 24), const Divider(height: 24),
_buildInfoRow( _buildInfoRow(
context, context,
icon: Icons.update, icon: Icons.update,
label: 'Last Updated', 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)); sorted.sort((a, b) => b.price.compareTo(a.price));
break; break;
case ProductSortOption.newest: 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; break;
case ProductSortOption.oldest: 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; break;
} }

View File

@@ -91,10 +91,18 @@ class SortedProducts extends _$SortedProducts {
sorted.sort((a, b) => b.price.compareTo(a.price)); sorted.sort((a, b) => b.price.compareTo(a.price));
break; break;
case ProductSortOption.newest: 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; break;
case ProductSortOption.oldest: 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; break;
} }

View File

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

View File

@@ -472,14 +472,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
url: "https://pub.dev"
source: hosted
version: "8.2.0"
glob: glob:
dependency: transitive dependency: transitive
description: description: