add refresh token
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -31,11 +31,11 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/api_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
import 'api_interceptor.dart';
|
||||
import 'refresh_token_interceptor.dart';
|
||||
|
||||
/// Dio HTTP client configuration
|
||||
class DioClient {
|
||||
late final Dio _dio;
|
||||
String? _authToken;
|
||||
final SecureStorage? secureStorage;
|
||||
|
||||
DioClient() {
|
||||
DioClient({this.secureStorage}) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
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;
|
||||
|
||||
104
lib/core/network/refresh_token_interceptor.dart
Normal file
104
lib/core/network/refresh_token_interceptor.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@ part 'core_providers.g.dart';
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// 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)
|
||||
DioClient dioClient(Ref ref) {
|
||||
return DioClient();
|
||||
final storage = ref.watch(secureStorageProvider);
|
||||
return DioClient(secureStorage: storage);
|
||||
}
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
|
||||
@@ -11,7 +11,8 @@ part of 'core_providers.dart';
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// 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)
|
||||
const dioClientProvider = DioClientProvider._();
|
||||
@@ -19,7 +20,8 @@ const dioClientProvider = DioClientProvider._();
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// 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
|
||||
extends $FunctionalProvider<DioClient, DioClient, DioClient>
|
||||
@@ -27,7 +29,8 @@ final class DioClientProvider
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// 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._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -61,7 +64,7 @@ final class DioClientProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';
|
||||
String _$dioClientHash() => r'a9edc35e0e918bfa8e6c4e3ecd72412fba383cb2';
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -472,14 +472,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user