add auth, format
This commit is contained in:
@@ -398,7 +398,10 @@ class ApiConstants {
|
||||
/// final url = ApiConstants.buildUrlWithParams('/products/{id}', {'id': '123'});
|
||||
/// // Returns: https://api.worker.example.com/v1/products/123
|
||||
/// ```
|
||||
static String buildUrlWithParams(String endpoint, Map<String, String> params) {
|
||||
static String buildUrlWithParams(
|
||||
String endpoint,
|
||||
Map<String, String> params,
|
||||
) {
|
||||
String url = endpoint;
|
||||
params.forEach((key, value) {
|
||||
url = url.replaceAll('{$key}', value);
|
||||
|
||||
@@ -440,7 +440,12 @@ class AppConstants {
|
||||
static const int maxProductImageSize = 3;
|
||||
|
||||
/// Supported image formats
|
||||
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
static const List<String> supportedImageFormats = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'webp',
|
||||
];
|
||||
|
||||
/// Image quality for compression (0-100)
|
||||
static const int imageQuality = 85;
|
||||
|
||||
@@ -59,22 +59,22 @@ class HiveBoxNames {
|
||||
|
||||
/// Get all box names for initialization
|
||||
static List<String> get allBoxes => [
|
||||
userBox,
|
||||
productBox,
|
||||
cartBox,
|
||||
orderBox,
|
||||
projectBox,
|
||||
quotes,
|
||||
loyaltyBox,
|
||||
rewardsBox,
|
||||
settingsBox,
|
||||
cacheBox,
|
||||
syncStateBox,
|
||||
notificationBox,
|
||||
addressBox,
|
||||
favoriteBox,
|
||||
offlineQueueBox,
|
||||
];
|
||||
userBox,
|
||||
productBox,
|
||||
cartBox,
|
||||
orderBox,
|
||||
projectBox,
|
||||
quotes,
|
||||
loyaltyBox,
|
||||
rewardsBox,
|
||||
settingsBox,
|
||||
cacheBox,
|
||||
syncStateBox,
|
||||
notificationBox,
|
||||
addressBox,
|
||||
favoriteBox,
|
||||
offlineQueueBox,
|
||||
];
|
||||
}
|
||||
|
||||
/// Hive Type Adapter IDs
|
||||
@@ -152,7 +152,8 @@ class HiveTypeIds {
|
||||
// Aliases for backward compatibility and clarity
|
||||
static const int memberTier = loyaltyTier; // Alias for loyaltyTier
|
||||
static const int userType = userRole; // Alias for userRole
|
||||
static const int projectStatus = submissionStatus; // Alias for submissionStatus
|
||||
static const int projectStatus =
|
||||
submissionStatus; // Alias for submissionStatus
|
||||
static const int transactionType = entryType; // Alias for entryType
|
||||
|
||||
// Cache & Sync Models (60-69)
|
||||
|
||||
@@ -17,14 +17,16 @@ import 'package:worker/core/database/hive_service.dart';
|
||||
/// - Sync state tracking
|
||||
class DatabaseManager {
|
||||
DatabaseManager({HiveService? hiveService})
|
||||
: _hiveService = hiveService ?? HiveService();
|
||||
: _hiveService = hiveService ?? HiveService();
|
||||
|
||||
final HiveService _hiveService;
|
||||
|
||||
/// Get a box safely
|
||||
Box<T> _getBox<T>(String boxName) {
|
||||
if (!_hiveService.isBoxOpen(boxName)) {
|
||||
throw HiveError('Box $boxName is not open. Initialize HiveService first.');
|
||||
throw HiveError(
|
||||
'Box $boxName is not open. Initialize HiveService first.',
|
||||
);
|
||||
}
|
||||
return _hiveService.getBox<T>(boxName);
|
||||
}
|
||||
@@ -49,11 +51,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
/// Get a value from a box
|
||||
T? get<T>({
|
||||
required String boxName,
|
||||
required String key,
|
||||
T? defaultValue,
|
||||
}) {
|
||||
T? get<T>({required String boxName, required String key, T? defaultValue}) {
|
||||
try {
|
||||
final box = _getBox<T>(boxName);
|
||||
return box.get(key, defaultValue: defaultValue);
|
||||
@@ -65,10 +63,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
/// Delete a value from a box
|
||||
Future<void> delete({
|
||||
required String boxName,
|
||||
required String key,
|
||||
}) async {
|
||||
Future<void> delete({required String boxName, required String key}) async {
|
||||
try {
|
||||
final box = _getBox<dynamic>(boxName);
|
||||
await box.delete(key);
|
||||
@@ -81,10 +76,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
/// Check if a key exists in a box
|
||||
bool exists({
|
||||
required String boxName,
|
||||
required String key,
|
||||
}) {
|
||||
bool exists({required String boxName, required String key}) {
|
||||
try {
|
||||
final box = _getBox<dynamic>(boxName);
|
||||
return box.containsKey(key);
|
||||
@@ -138,10 +130,7 @@ class DatabaseManager {
|
||||
// ==================== Cache Operations ====================
|
||||
|
||||
/// Save data to cache with timestamp
|
||||
Future<void> saveToCache<T>({
|
||||
required String key,
|
||||
required T data,
|
||||
}) async {
|
||||
Future<void> saveToCache<T>({required String key, required T data}) async {
|
||||
try {
|
||||
final cacheBox = _getBox<dynamic>(HiveBoxNames.cacheBox);
|
||||
await cacheBox.put(key, {
|
||||
@@ -159,10 +148,7 @@ class DatabaseManager {
|
||||
/// Get data from cache
|
||||
///
|
||||
/// Returns null if cache is expired or doesn't exist
|
||||
T? getFromCache<T>({
|
||||
required String key,
|
||||
Duration? maxAge,
|
||||
}) {
|
||||
T? getFromCache<T>({required String key, Duration? maxAge}) {
|
||||
try {
|
||||
final cacheBox = _getBox<dynamic>(HiveBoxNames.cacheBox);
|
||||
final cachedData = cacheBox.get(key) as Map<dynamic, dynamic>?;
|
||||
@@ -193,10 +179,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
/// Check if cache is valid (exists and not expired)
|
||||
bool isCacheValid({
|
||||
required String key,
|
||||
Duration? maxAge,
|
||||
}) {
|
||||
bool isCacheValid({required String key, Duration? maxAge}) {
|
||||
try {
|
||||
final cacheBox = _getBox<dynamic>(HiveBoxNames.cacheBox);
|
||||
final cachedData = cacheBox.get(key) as Map<dynamic, dynamic>?;
|
||||
@@ -244,7 +227,9 @@ class DatabaseManager {
|
||||
await cacheBox.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('DatabaseManager: Cleared ${keysToDelete.length} expired cache entries');
|
||||
debugPrint(
|
||||
'DatabaseManager: Cleared ${keysToDelete.length} expired cache entries',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('DatabaseManager: Error clearing expired cache: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
@@ -281,10 +266,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
/// Check if data needs sync
|
||||
bool needsSync({
|
||||
required String dataType,
|
||||
required Duration syncInterval,
|
||||
}) {
|
||||
bool needsSync({required String dataType, required Duration syncInterval}) {
|
||||
final lastSync = getLastSyncTime(dataType);
|
||||
|
||||
if (lastSync == null) return true;
|
||||
@@ -296,22 +278,12 @@ class DatabaseManager {
|
||||
// ==================== Settings Operations ====================
|
||||
|
||||
/// Save a setting
|
||||
Future<void> saveSetting<T>({
|
||||
required String key,
|
||||
required T value,
|
||||
}) async {
|
||||
await save(
|
||||
boxName: HiveBoxNames.settingsBox,
|
||||
key: key,
|
||||
value: value,
|
||||
);
|
||||
Future<void> saveSetting<T>({required String key, required T value}) async {
|
||||
await save(boxName: HiveBoxNames.settingsBox, key: key, value: value);
|
||||
}
|
||||
|
||||
/// Get a setting
|
||||
T? getSetting<T>({
|
||||
required String key,
|
||||
T? defaultValue,
|
||||
}) {
|
||||
T? getSetting<T>({required String key, T? defaultValue}) {
|
||||
return get(
|
||||
boxName: HiveBoxNames.settingsBox,
|
||||
key: key,
|
||||
@@ -328,7 +300,9 @@ class DatabaseManager {
|
||||
|
||||
// Check queue size limit
|
||||
if (queueBox.length >= HiveDatabaseConfig.maxOfflineQueueSize) {
|
||||
debugPrint('DatabaseManager: Offline queue is full, removing oldest item');
|
||||
debugPrint(
|
||||
'DatabaseManager: Offline queue is full, removing oldest item',
|
||||
);
|
||||
await queueBox.deleteAt(0);
|
||||
}
|
||||
|
||||
@@ -386,10 +360,7 @@ class DatabaseManager {
|
||||
try {
|
||||
if (_hiveService.isBoxOpen(boxName)) {
|
||||
final box = _getBox<dynamic>(boxName);
|
||||
stats[boxName] = {
|
||||
'count': box.length,
|
||||
'keys': box.keys.length,
|
||||
};
|
||||
stats[boxName] = {'count': box.length, 'keys': box.keys.length};
|
||||
}
|
||||
} catch (e) {
|
||||
stats[boxName] = {'error': e.toString()};
|
||||
|
||||
@@ -5,9 +5,7 @@ import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/favorites/data/models/favorite_model.dart';
|
||||
// TODO: Re-enable when build_runner generates this file successfully
|
||||
// import 'package:worker/hive_registrar.g.dart';
|
||||
import 'package:worker/hive_registrar.g.dart';
|
||||
|
||||
/// Hive CE (Community Edition) Database Service
|
||||
///
|
||||
@@ -92,40 +90,45 @@ class HiveService {
|
||||
debugPrint('HiveService: Registering type adapters...');
|
||||
|
||||
// Register all adapters using the auto-generated extension
|
||||
// This automatically registers:
|
||||
// - CachedDataAdapter (typeId: 30)
|
||||
// - All enum adapters (typeIds: 20-29)
|
||||
// TODO: Re-enable when build_runner generates hive_registrar.g.dart successfully
|
||||
// Hive.registerAdapters();
|
||||
// This automatically registers all model and enum adapters
|
||||
Hive.registerAdapters();
|
||||
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.memberTier) ? "✓" : "✗"} MemberTier adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userType) ? "✓" : "✗"} UserType adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "✓" : "✗"} OrderStatus adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatus) ? "✓" : "✗"} ProjectStatus adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "✓" : "✗"} ProjectType adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.transactionType) ? "✓" : "✗"} TransactionType adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.giftStatus) ? "✓" : "✗"} GiftStatus adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentStatus) ? "✓" : "✗"} PaymentStatus adapter');
|
||||
// NotificationType adapter not needed - notification model uses String type
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter');
|
||||
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter');
|
||||
|
||||
// Register model type adapters manually
|
||||
// FavoriteModel adapter (typeId: 28)
|
||||
if (!Hive.isAdapterRegistered(HiveTypeIds.favoriteModel)) {
|
||||
Hive.registerAdapter(FavoriteModelAdapter());
|
||||
debugPrint('HiveService: ✓ FavoriteModel adapter registered');
|
||||
}
|
||||
|
||||
// TODO: Register other model type adapters when created
|
||||
// Example:
|
||||
// - UserModel (typeId: 0)
|
||||
// - ProductModel (typeId: 1)
|
||||
// - CartItemModel (typeId: 2)
|
||||
// - OrderModel (typeId: 3)
|
||||
// - ProjectModel (typeId: 4)
|
||||
// - LoyaltyTransactionModel (typeId: 5)
|
||||
// etc.
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.loyaltyTier) ? "✓" : "✗"} LoyaltyTier adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userRole) ? "✓" : "✗"} UserRole adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "✓" : "✗"} OrderStatus adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "✓" : "✗"} ProjectType adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "✓" : "✗"} EntryType adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.giftStatus) ? "✓" : "✗"} GiftStatus adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentStatus) ? "✓" : "✗"} PaymentStatus adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.favoriteModel) ? "✓" : "✗"} FavoriteModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.productModel) ? "✓" : "✗"} ProductModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userModel) ? "✓" : "✗"} UserModel adapter',
|
||||
);
|
||||
|
||||
debugPrint('HiveService: Type adapters registered successfully');
|
||||
}
|
||||
@@ -188,21 +191,23 @@ class HiveService {
|
||||
/// Handles schema version upgrades and data migrations.
|
||||
Future<void> _performMigrations() async {
|
||||
final settingsBox = Hive.box<dynamic>(HiveBoxNames.settingsBox);
|
||||
final currentVersion = settingsBox.get(
|
||||
HiveKeys.schemaVersion,
|
||||
defaultValue: 0,
|
||||
) as int;
|
||||
final currentVersion =
|
||||
settingsBox.get(HiveKeys.schemaVersion, defaultValue: 0) as int;
|
||||
|
||||
debugPrint('HiveService: Current schema version: $currentVersion');
|
||||
debugPrint('HiveService: Target schema version: ${HiveDatabaseConfig.currentSchemaVersion}');
|
||||
debugPrint(
|
||||
'HiveService: Target schema version: ${HiveDatabaseConfig.currentSchemaVersion}',
|
||||
);
|
||||
|
||||
if (currentVersion < HiveDatabaseConfig.currentSchemaVersion) {
|
||||
debugPrint('HiveService: Performing migrations...');
|
||||
|
||||
// Perform migrations sequentially
|
||||
for (int version = currentVersion + 1;
|
||||
version <= HiveDatabaseConfig.currentSchemaVersion;
|
||||
version++) {
|
||||
for (
|
||||
int version = currentVersion + 1;
|
||||
version <= HiveDatabaseConfig.currentSchemaVersion;
|
||||
version++
|
||||
) {
|
||||
await _migrateToVersion(version);
|
||||
}
|
||||
|
||||
@@ -278,10 +283,9 @@ class HiveService {
|
||||
|
||||
/// Clear expired cache entries
|
||||
Future<void> _clearExpiredCache() async {
|
||||
final cacheBox = Hive.box<dynamic>(HiveBoxNames.cacheBox);
|
||||
|
||||
// TODO: Implement cache expiration logic
|
||||
// This will be implemented when cache models are created
|
||||
// final cacheBox = Hive.box<dynamic>(HiveBoxNames.cacheBox);
|
||||
|
||||
debugPrint('HiveService: Cleared expired cache entries');
|
||||
}
|
||||
@@ -291,14 +295,17 @@ class HiveService {
|
||||
final queueBox = Hive.box<dynamic>(HiveBoxNames.offlineQueueBox);
|
||||
|
||||
if (queueBox.length > HiveDatabaseConfig.maxOfflineQueueSize) {
|
||||
final itemsToRemove = queueBox.length - HiveDatabaseConfig.maxOfflineQueueSize;
|
||||
final itemsToRemove =
|
||||
queueBox.length - HiveDatabaseConfig.maxOfflineQueueSize;
|
||||
|
||||
// Remove oldest items
|
||||
for (int i = 0; i < itemsToRemove; i++) {
|
||||
await queueBox.deleteAt(0);
|
||||
}
|
||||
|
||||
debugPrint('HiveService: Removed $itemsToRemove old items from offline queue');
|
||||
debugPrint(
|
||||
'HiveService: Removed $itemsToRemove old items from offline queue',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,35 +10,27 @@ library;
|
||||
|
||||
/// Base exception for all network-related errors
|
||||
class NetworkException implements Exception {
|
||||
const NetworkException(
|
||||
this.message, {
|
||||
this.statusCode,
|
||||
this.data,
|
||||
});
|
||||
const NetworkException(this.message, {this.statusCode, this.data});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
final dynamic data;
|
||||
|
||||
@override
|
||||
String toString() => 'NetworkException: $message${statusCode != null ? ' (Status: $statusCode)' : ''}';
|
||||
String toString() =>
|
||||
'NetworkException: $message${statusCode != null ? ' (Status: $statusCode)' : ''}';
|
||||
}
|
||||
|
||||
/// Exception thrown when there's no internet connection
|
||||
class NoInternetException extends NetworkException {
|
||||
const NoInternetException()
|
||||
: super(
|
||||
'Không có kết nối internet. Vui lòng kiểm tra kết nối của bạn.',
|
||||
);
|
||||
: super('Không có kết nối internet. Vui lòng kiểm tra kết nối của bạn.');
|
||||
}
|
||||
|
||||
/// Exception thrown when connection times out
|
||||
class TimeoutException extends NetworkException {
|
||||
const TimeoutException()
|
||||
: super(
|
||||
'Kết nối quá lâu. Vui lòng thử lại.',
|
||||
statusCode: 408,
|
||||
);
|
||||
: super('Kết nối quá lâu. Vui lòng thử lại.', statusCode: 408);
|
||||
}
|
||||
|
||||
/// Exception thrown when server returns 500+ errors
|
||||
@@ -52,10 +44,7 @@ class ServerException extends NetworkException {
|
||||
/// Exception thrown when server is unreachable
|
||||
class ServiceUnavailableException extends ServerException {
|
||||
const ServiceUnavailableException()
|
||||
: super(
|
||||
'Dịch vụ tạm thời không khả dụng. Vui lòng thử lại sau.',
|
||||
503,
|
||||
);
|
||||
: super('Dịch vụ tạm thời không khả dụng. Vui lòng thử lại sau.', 503);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -64,10 +53,7 @@ class ServiceUnavailableException extends ServerException {
|
||||
|
||||
/// Base exception for authentication-related errors
|
||||
class AuthException implements Exception {
|
||||
const AuthException(
|
||||
this.message, {
|
||||
this.statusCode,
|
||||
});
|
||||
const AuthException(this.message, {this.statusCode});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
@@ -79,10 +65,7 @@ class AuthException implements Exception {
|
||||
/// Exception thrown when authentication credentials are invalid
|
||||
class InvalidCredentialsException extends AuthException {
|
||||
const InvalidCredentialsException()
|
||||
: super(
|
||||
'Thông tin đăng nhập không hợp lệ.',
|
||||
statusCode: 401,
|
||||
);
|
||||
: super('Thông tin đăng nhập không hợp lệ.', statusCode: 401);
|
||||
}
|
||||
|
||||
/// Exception thrown when user is not authenticated
|
||||
@@ -95,46 +78,37 @@ class UnauthorizedException extends AuthException {
|
||||
/// Exception thrown when user doesn't have permission
|
||||
class ForbiddenException extends AuthException {
|
||||
const ForbiddenException()
|
||||
: super(
|
||||
'Bạn không có quyền truy cập tài nguyên này.',
|
||||
statusCode: 403,
|
||||
);
|
||||
: super('Bạn không có quyền truy cập tài nguyên này.', statusCode: 403);
|
||||
}
|
||||
|
||||
/// Exception thrown when auth token is expired
|
||||
class TokenExpiredException extends AuthException {
|
||||
const TokenExpiredException()
|
||||
: super(
|
||||
'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.',
|
||||
statusCode: 401,
|
||||
);
|
||||
: super(
|
||||
'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.',
|
||||
statusCode: 401,
|
||||
);
|
||||
}
|
||||
|
||||
/// Exception thrown when refresh token is invalid
|
||||
class InvalidRefreshTokenException extends AuthException {
|
||||
const InvalidRefreshTokenException()
|
||||
: super(
|
||||
'Không thể làm mới phiên đăng nhập. Vui lòng đăng nhập lại.',
|
||||
statusCode: 401,
|
||||
);
|
||||
: super(
|
||||
'Không thể làm mới phiên đăng nhập. Vui lòng đăng nhập lại.',
|
||||
statusCode: 401,
|
||||
);
|
||||
}
|
||||
|
||||
/// Exception thrown when OTP is invalid
|
||||
class InvalidOTPException extends AuthException {
|
||||
const InvalidOTPException()
|
||||
: super(
|
||||
'Mã OTP không hợp lệ. Vui lòng thử lại.',
|
||||
statusCode: 400,
|
||||
);
|
||||
: super('Mã OTP không hợp lệ. Vui lòng thử lại.', statusCode: 400);
|
||||
}
|
||||
|
||||
/// Exception thrown when OTP is expired
|
||||
class OTPExpiredException extends AuthException {
|
||||
const OTPExpiredException()
|
||||
: super(
|
||||
'Mã OTP đã hết hạn. Vui lòng yêu cầu mã mới.',
|
||||
statusCode: 400,
|
||||
);
|
||||
: super('Mã OTP đã hết hạn. Vui lòng yêu cầu mã mới.', statusCode: 400);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -143,10 +117,7 @@ class OTPExpiredException extends AuthException {
|
||||
|
||||
/// Exception thrown when request data is invalid
|
||||
class ValidationException implements Exception {
|
||||
const ValidationException(
|
||||
this.message, {
|
||||
this.errors,
|
||||
});
|
||||
const ValidationException(this.message, {this.errors});
|
||||
|
||||
final String message;
|
||||
final Map<String, List<String>>? errors;
|
||||
@@ -198,9 +169,7 @@ class NotFoundException implements Exception {
|
||||
|
||||
/// Exception thrown when trying to create a duplicate resource
|
||||
class ConflictException implements Exception {
|
||||
const ConflictException([
|
||||
this.message = 'Tài nguyên đã tồn tại.',
|
||||
]);
|
||||
const ConflictException([this.message = 'Tài nguyên đã tồn tại.']);
|
||||
|
||||
final String message;
|
||||
|
||||
@@ -237,10 +206,7 @@ class RateLimitException implements Exception {
|
||||
|
||||
/// Exception thrown for payment-related errors
|
||||
class PaymentException implements Exception {
|
||||
const PaymentException(
|
||||
this.message, {
|
||||
this.transactionId,
|
||||
});
|
||||
const PaymentException(this.message, {this.transactionId});
|
||||
|
||||
final String message;
|
||||
final String? transactionId;
|
||||
@@ -259,8 +225,7 @@ class PaymentFailedException extends PaymentException {
|
||||
|
||||
/// Exception thrown when payment is cancelled
|
||||
class PaymentCancelledException extends PaymentException {
|
||||
const PaymentCancelledException()
|
||||
: super('Thanh toán đã bị hủy.');
|
||||
const PaymentCancelledException() : super('Thanh toán đã bị hủy.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -269,9 +234,7 @@ class PaymentCancelledException extends PaymentException {
|
||||
|
||||
/// Exception thrown for cache-related errors
|
||||
class CacheException implements Exception {
|
||||
const CacheException([
|
||||
this.message = 'Lỗi khi truy cập bộ nhớ đệm.',
|
||||
]);
|
||||
const CacheException([this.message = 'Lỗi khi truy cập bộ nhớ đệm.']);
|
||||
|
||||
final String message;
|
||||
|
||||
@@ -281,8 +244,7 @@ class CacheException implements Exception {
|
||||
|
||||
/// Exception thrown when cache data is corrupted
|
||||
class CacheCorruptedException extends CacheException {
|
||||
const CacheCorruptedException()
|
||||
: super('Dữ liệu bộ nhớ đệm bị hỏng.');
|
||||
const CacheCorruptedException() : super('Dữ liệu bộ nhớ đệm bị hỏng.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -291,9 +253,7 @@ class CacheCorruptedException extends CacheException {
|
||||
|
||||
/// Exception thrown for local storage errors
|
||||
class StorageException implements Exception {
|
||||
const StorageException([
|
||||
this.message = 'Lỗi khi truy cập bộ nhớ cục bộ.',
|
||||
]);
|
||||
const StorageException([this.message = 'Lỗi khi truy cập bộ nhớ cục bộ.']);
|
||||
|
||||
final String message;
|
||||
|
||||
@@ -304,7 +264,7 @@ class StorageException implements Exception {
|
||||
/// Exception thrown when storage is full
|
||||
class StorageFullException extends StorageException {
|
||||
const StorageFullException()
|
||||
: super('Bộ nhớ đã đầy. Vui lòng giải phóng không gian.');
|
||||
: super('Bộ nhớ đã đầy. Vui lòng giải phóng không gian.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -9,16 +9,12 @@ sealed class Failure {
|
||||
const Failure({required this.message});
|
||||
|
||||
/// Network-related failure
|
||||
const factory Failure.network({
|
||||
required String message,
|
||||
int? statusCode,
|
||||
}) = NetworkFailure;
|
||||
const factory Failure.network({required String message, int? statusCode}) =
|
||||
NetworkFailure;
|
||||
|
||||
/// Server error failure (5xx errors)
|
||||
const factory Failure.server({
|
||||
required String message,
|
||||
int? statusCode,
|
||||
}) = ServerFailure;
|
||||
const factory Failure.server({required String message, int? statusCode}) =
|
||||
ServerFailure;
|
||||
|
||||
/// Authentication failure
|
||||
const factory Failure.authentication({
|
||||
@@ -33,20 +29,14 @@ sealed class Failure {
|
||||
}) = ValidationFailure;
|
||||
|
||||
/// Not found failure (404)
|
||||
const factory Failure.notFound({
|
||||
required String message,
|
||||
}) = NotFoundFailure;
|
||||
const factory Failure.notFound({required String message}) = NotFoundFailure;
|
||||
|
||||
/// Conflict failure (409)
|
||||
const factory Failure.conflict({
|
||||
required String message,
|
||||
}) = ConflictFailure;
|
||||
const factory Failure.conflict({required String message}) = ConflictFailure;
|
||||
|
||||
/// Rate limit exceeded failure (429)
|
||||
const factory Failure.rateLimit({
|
||||
required String message,
|
||||
int? retryAfter,
|
||||
}) = RateLimitFailure;
|
||||
const factory Failure.rateLimit({required String message, int? retryAfter}) =
|
||||
RateLimitFailure;
|
||||
|
||||
/// Payment failure
|
||||
const factory Failure.payment({
|
||||
@@ -55,19 +45,13 @@ sealed class Failure {
|
||||
}) = PaymentFailure;
|
||||
|
||||
/// Cache failure
|
||||
const factory Failure.cache({
|
||||
required String message,
|
||||
}) = CacheFailure;
|
||||
const factory Failure.cache({required String message}) = CacheFailure;
|
||||
|
||||
/// Storage failure
|
||||
const factory Failure.storage({
|
||||
required String message,
|
||||
}) = StorageFailure;
|
||||
const factory Failure.storage({required String message}) = StorageFailure;
|
||||
|
||||
/// Parse failure
|
||||
const factory Failure.parse({
|
||||
required String message,
|
||||
}) = ParseFailure;
|
||||
const factory Failure.parse({required String message}) = ParseFailure;
|
||||
|
||||
/// No internet connection failure
|
||||
const factory Failure.noInternet() = NoInternetFailure;
|
||||
@@ -76,9 +60,7 @@ sealed class Failure {
|
||||
const factory Failure.timeout() = TimeoutFailure;
|
||||
|
||||
/// Unknown failure
|
||||
const factory Failure.unknown({
|
||||
required String message,
|
||||
}) = UnknownFailure;
|
||||
const factory Failure.unknown({required String message}) = UnknownFailure;
|
||||
|
||||
final String message;
|
||||
|
||||
@@ -120,15 +102,21 @@ sealed class Failure {
|
||||
/// Get user-friendly error message
|
||||
String getUserMessage() {
|
||||
return switch (this) {
|
||||
ValidationFailure(:final message, :final errors) => _formatValidationMessage(message, errors),
|
||||
RateLimitFailure(:final message, :final retryAfter) => _formatRateLimitMessage(message, retryAfter),
|
||||
NoInternetFailure() => 'Không có kết nối internet. Vui lòng kiểm tra kết nối của bạn.',
|
||||
ValidationFailure(:final message, :final errors) =>
|
||||
_formatValidationMessage(message, errors),
|
||||
RateLimitFailure(:final message, :final retryAfter) =>
|
||||
_formatRateLimitMessage(message, retryAfter),
|
||||
NoInternetFailure() =>
|
||||
'Không có kết nối internet. Vui lòng kiểm tra kết nối của bạn.',
|
||||
TimeoutFailure() => 'Kết nối quá lâu. Vui lòng thử lại.',
|
||||
_ => message,
|
||||
};
|
||||
}
|
||||
|
||||
String _formatValidationMessage(String message, Map<String, List<String>>? errors) {
|
||||
String _formatValidationMessage(
|
||||
String message,
|
||||
Map<String, List<String>>? errors,
|
||||
) {
|
||||
if (errors != null && errors.isNotEmpty) {
|
||||
final firstError = errors.values.first.first;
|
||||
return '$message: $firstError';
|
||||
@@ -146,10 +134,7 @@ sealed class Failure {
|
||||
|
||||
/// Network-related failure
|
||||
final class NetworkFailure extends Failure {
|
||||
const NetworkFailure({
|
||||
required super.message,
|
||||
this.statusCode,
|
||||
});
|
||||
const NetworkFailure({required super.message, this.statusCode});
|
||||
|
||||
@override
|
||||
final int? statusCode;
|
||||
@@ -157,10 +142,7 @@ final class NetworkFailure extends Failure {
|
||||
|
||||
/// Server error failure (5xx errors)
|
||||
final class ServerFailure extends Failure {
|
||||
const ServerFailure({
|
||||
required super.message,
|
||||
this.statusCode,
|
||||
});
|
||||
const ServerFailure({required super.message, this.statusCode});
|
||||
|
||||
@override
|
||||
final int? statusCode;
|
||||
@@ -168,10 +150,7 @@ final class ServerFailure extends Failure {
|
||||
|
||||
/// Authentication failure
|
||||
final class AuthenticationFailure extends Failure {
|
||||
const AuthenticationFailure({
|
||||
required super.message,
|
||||
this.statusCode,
|
||||
});
|
||||
const AuthenticationFailure({required super.message, this.statusCode});
|
||||
|
||||
@override
|
||||
final int? statusCode;
|
||||
@@ -179,84 +158,61 @@ final class AuthenticationFailure extends Failure {
|
||||
|
||||
/// Validation failure
|
||||
final class ValidationFailure extends Failure {
|
||||
const ValidationFailure({
|
||||
required super.message,
|
||||
this.errors,
|
||||
});
|
||||
const ValidationFailure({required super.message, this.errors});
|
||||
|
||||
final Map<String, List<String>>? errors;
|
||||
}
|
||||
|
||||
/// Not found failure (404)
|
||||
final class NotFoundFailure extends Failure {
|
||||
const NotFoundFailure({
|
||||
required super.message,
|
||||
});
|
||||
const NotFoundFailure({required super.message});
|
||||
}
|
||||
|
||||
/// Conflict failure (409)
|
||||
final class ConflictFailure extends Failure {
|
||||
const ConflictFailure({
|
||||
required super.message,
|
||||
});
|
||||
const ConflictFailure({required super.message});
|
||||
}
|
||||
|
||||
/// Rate limit exceeded failure (429)
|
||||
final class RateLimitFailure extends Failure {
|
||||
const RateLimitFailure({
|
||||
required super.message,
|
||||
this.retryAfter,
|
||||
});
|
||||
const RateLimitFailure({required super.message, this.retryAfter});
|
||||
|
||||
final int? retryAfter;
|
||||
}
|
||||
|
||||
/// Payment failure
|
||||
final class PaymentFailure extends Failure {
|
||||
const PaymentFailure({
|
||||
required super.message,
|
||||
this.transactionId,
|
||||
});
|
||||
const PaymentFailure({required super.message, this.transactionId});
|
||||
|
||||
final String? transactionId;
|
||||
}
|
||||
|
||||
/// Cache failure
|
||||
final class CacheFailure extends Failure {
|
||||
const CacheFailure({
|
||||
required super.message,
|
||||
});
|
||||
const CacheFailure({required super.message});
|
||||
}
|
||||
|
||||
/// Storage failure
|
||||
final class StorageFailure extends Failure {
|
||||
const StorageFailure({
|
||||
required super.message,
|
||||
});
|
||||
const StorageFailure({required super.message});
|
||||
}
|
||||
|
||||
/// Parse failure
|
||||
final class ParseFailure extends Failure {
|
||||
const ParseFailure({
|
||||
required super.message,
|
||||
});
|
||||
const ParseFailure({required super.message});
|
||||
}
|
||||
|
||||
/// No internet connection failure
|
||||
final class NoInternetFailure extends Failure {
|
||||
const NoInternetFailure()
|
||||
: super(message: 'Không có kết nối internet');
|
||||
const NoInternetFailure() : super(message: 'Không có kết nối internet');
|
||||
}
|
||||
|
||||
/// Timeout failure
|
||||
final class TimeoutFailure extends Failure {
|
||||
const TimeoutFailure()
|
||||
: super(message: 'Kết nối quá lâu');
|
||||
const TimeoutFailure() : super(message: 'Kết nối quá lâu');
|
||||
}
|
||||
|
||||
/// Unknown failure
|
||||
final class UnknownFailure extends Failure {
|
||||
const UnknownFailure({
|
||||
required super.message,
|
||||
});
|
||||
const UnknownFailure({required super.message});
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@ library;
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
|
||||
|
||||
part 'api_interceptor.g.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ part 'api_interceptor.g.dart';
|
||||
// ============================================================================
|
||||
|
||||
/// Keys for storing auth tokens in SharedPreferences
|
||||
/// @deprecated Use AuthLocalDataSource with Hive instead
|
||||
class AuthStorageKeys {
|
||||
static const String accessToken = 'auth_access_token';
|
||||
static const String refreshToken = 'auth_refresh_token';
|
||||
@@ -33,12 +35,15 @@ class AuthStorageKeys {
|
||||
// Auth Interceptor
|
||||
// ============================================================================
|
||||
|
||||
/// Interceptor for adding authentication tokens to requests
|
||||
/// Interceptor for adding ERPNext session tokens to requests
|
||||
///
|
||||
/// Adds SID (Session ID) and CSRF token from Hive storage to request headers.
|
||||
class AuthInterceptor extends Interceptor {
|
||||
AuthInterceptor(this._prefs, this._dio);
|
||||
AuthInterceptor(this._prefs, this._dio, this._authLocalDataSource);
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
final Dio _dio;
|
||||
final AuthLocalDataSource _authLocalDataSource;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
@@ -47,10 +52,19 @@ class AuthInterceptor extends Interceptor {
|
||||
) async {
|
||||
// Check if this endpoint requires authentication
|
||||
if (_requiresAuth(options.path)) {
|
||||
final token = await _getAccessToken();
|
||||
// Get session data from secure storage (async)
|
||||
final sid = await _authLocalDataSource.getSid();
|
||||
final csrfToken = await _authLocalDataSource.getCsrfToken();
|
||||
|
||||
if (sid != null && csrfToken != null) {
|
||||
// Add ERPNext session headers
|
||||
options.headers['Cookie'] = 'sid=$sid';
|
||||
options.headers['X-Frappe-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
// Legacy: Also check for access token (for backward compatibility)
|
||||
final token = await _getAccessToken();
|
||||
if (token != null) {
|
||||
// Add bearer token to headers
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
}
|
||||
@@ -66,10 +80,7 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Check if error is 401 Unauthorized
|
||||
if (err.response?.statusCode == 401) {
|
||||
// Try to refresh token
|
||||
@@ -113,15 +124,16 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
/// Check if token is expired
|
||||
Future<bool> _isTokenExpired() async {
|
||||
final expiryString = _prefs.getString(AuthStorageKeys.tokenExpiry);
|
||||
if (expiryString == null) return true;
|
||||
|
||||
final expiry = DateTime.tryParse(expiryString);
|
||||
if (expiry == null) return true;
|
||||
|
||||
return DateTime.now().isAfter(expiry);
|
||||
}
|
||||
// TODO: Use this method when implementing token refresh logic
|
||||
// Future<bool> _isTokenExpired() async {
|
||||
// final expiryString = _prefs.getString(AuthStorageKeys.tokenExpiry);
|
||||
// if (expiryString == null) return true;
|
||||
//
|
||||
// final expiry = DateTime.tryParse(expiryString);
|
||||
// if (expiry == null) return true;
|
||||
//
|
||||
// return DateTime.now().isAfter(expiry);
|
||||
// }
|
||||
|
||||
/// Refresh access token using refresh token
|
||||
Future<bool> _refreshAccessToken() async {
|
||||
@@ -135,11 +147,7 @@ class AuthInterceptor extends Interceptor {
|
||||
// Call refresh token endpoint
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.apiBaseUrl}${ApiConstants.refreshToken}',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Authorization': 'Bearer $refreshToken',
|
||||
},
|
||||
),
|
||||
options: Options(headers: {'Authorization': 'Bearer $refreshToken'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -185,10 +193,7 @@ class AuthInterceptor extends Interceptor {
|
||||
|
||||
final options = Options(
|
||||
method: requestOptions.method,
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
headers: {...requestOptions.headers, 'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
return _dio.request(
|
||||
@@ -217,19 +222,13 @@ class LoggingInterceptor extends Interceptor {
|
||||
final bool enableErrorLogging;
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) {
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (enableRequestLogging) {
|
||||
developer.log(
|
||||
'╔══════════════════════════════════════════════════════════════',
|
||||
name: 'HTTP Request',
|
||||
);
|
||||
developer.log(
|
||||
'║ ${options.method} ${options.uri}',
|
||||
name: 'HTTP Request',
|
||||
);
|
||||
developer.log('║ ${options.method} ${options.uri}', name: 'HTTP Request');
|
||||
developer.log(
|
||||
'║ Headers: ${_sanitizeHeaders(options.headers)}',
|
||||
name: 'HTTP Request',
|
||||
@@ -290,10 +289,7 @@ class LoggingInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
if (enableErrorLogging) {
|
||||
developer.log(
|
||||
'╔══════════════════════════════════════════════════════════════',
|
||||
@@ -303,18 +299,12 @@ class LoggingInterceptor extends Interceptor {
|
||||
'║ ${err.requestOptions.method} ${err.requestOptions.uri}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log(
|
||||
'║ Error Type: ${err.type}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log('║ Error Type: ${err.type}', name: 'HTTP Error');
|
||||
developer.log(
|
||||
'║ Status Code: ${err.response?.statusCode}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log(
|
||||
'║ Message: ${err.message}',
|
||||
name: 'HTTP Error',
|
||||
);
|
||||
developer.log('║ Message: ${err.message}', name: 'HTTP Error');
|
||||
|
||||
if (err.response?.data != null) {
|
||||
developer.log(
|
||||
@@ -389,10 +379,7 @@ class LoggingInterceptor extends Interceptor {
|
||||
/// Interceptor for transforming Dio errors into custom exceptions
|
||||
class ErrorTransformerInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
Exception exception;
|
||||
|
||||
switch (err.type) {
|
||||
@@ -415,9 +402,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
break;
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
exception = NetworkException(
|
||||
'Lỗi không xác định: ${err.message}',
|
||||
);
|
||||
exception = NetworkException('Lỗi không xác định: ${err.message}');
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -447,7 +432,8 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
// Extract error message from response
|
||||
String? message;
|
||||
if (data is Map<String, dynamic>) {
|
||||
message = data['message'] as String? ??
|
||||
message =
|
||||
data['message'] as String? ??
|
||||
data['error'] as String? ??
|
||||
data['msg'] as String?;
|
||||
}
|
||||
@@ -460,9 +446,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
final validationErrors = errors.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value is List
|
||||
? value.cast<String>()
|
||||
: [value.toString()],
|
||||
value is List ? value.cast<String>() : [value.toString()],
|
||||
),
|
||||
);
|
||||
return ValidationException(
|
||||
@@ -498,9 +482,7 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
final validationErrors = errors.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
value is List
|
||||
? value.cast<String>()
|
||||
: [value.toString()],
|
||||
value is List ? value.cast<String>() : [value.toString()],
|
||||
),
|
||||
);
|
||||
return ValidationException(
|
||||
@@ -513,7 +495,9 @@ class ErrorTransformerInterceptor extends Interceptor {
|
||||
|
||||
case 429:
|
||||
final retryAfter = response.headers.value('retry-after');
|
||||
final retrySeconds = retryAfter != null ? int.tryParse(retryAfter) : null;
|
||||
final retrySeconds = retryAfter != null
|
||||
? int.tryParse(retryAfter)
|
||||
: null;
|
||||
return RateLimitException(message ?? 'Quá nhiều yêu cầu', retrySeconds);
|
||||
|
||||
case 500:
|
||||
@@ -549,7 +533,15 @@ Future<SharedPreferences> sharedPreferences(Ref ref) async {
|
||||
@riverpod
|
||||
Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
||||
final prefs = await ref.watch(sharedPreferencesProvider.future);
|
||||
return AuthInterceptor(prefs, dio);
|
||||
|
||||
// Create AuthLocalDataSource with FlutterSecureStorage
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
final authLocalDataSource = AuthLocalDataSource(secureStorage);
|
||||
|
||||
return AuthInterceptor(prefs, dio, authLocalDataSource);
|
||||
}
|
||||
|
||||
/// Provider for LoggingInterceptor
|
||||
|
||||
@@ -114,7 +114,7 @@ final class AuthInterceptorProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10';
|
||||
String _$authInterceptorHash() => r'3f964536e03e204d09cc9120dd9d961b6d6d4b71';
|
||||
|
||||
/// Provider for AuthInterceptor
|
||||
|
||||
|
||||
@@ -215,14 +215,14 @@ class DioClient {
|
||||
/// Clear all cached responses
|
||||
Future<void> clearCache() async {
|
||||
if (_cacheStore != null) {
|
||||
await _cacheStore!.clean();
|
||||
await _cacheStore.clean();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear specific cached response by key
|
||||
Future<void> clearCacheByKey(String key) async {
|
||||
if (_cacheStore != null) {
|
||||
await _cacheStore!.delete(key);
|
||||
await _cacheStore.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ class DioClient {
|
||||
final key = CacheOptions.defaultCacheKeyBuilder(
|
||||
RequestOptions(path: path),
|
||||
);
|
||||
await _cacheStore!.delete(key);
|
||||
await _cacheStore.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,10 +258,7 @@ class RetryInterceptor extends Interceptor {
|
||||
final double delayMultiplier;
|
||||
|
||||
@override
|
||||
void onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Get retry count from request extra
|
||||
final retries = err.requestOptions.extra['retries'] as int? ?? 0;
|
||||
|
||||
@@ -279,8 +276,9 @@ class RetryInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
final delayMs = (initialDelay.inMilliseconds *
|
||||
(delayMultiplier * (retries + 1))).toInt();
|
||||
final delayMs =
|
||||
(initialDelay.inMilliseconds * (delayMultiplier * (retries + 1)))
|
||||
.toInt();
|
||||
final delay = Duration(
|
||||
milliseconds: delayMs.clamp(
|
||||
initialDelay.inMilliseconds,
|
||||
@@ -341,10 +339,7 @@ class RetryInterceptor extends Interceptor {
|
||||
@riverpod
|
||||
Future<CacheStore> cacheStore(Ref ref) async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
return HiveCacheStore(
|
||||
directory.path,
|
||||
hiveBoxName: 'dio_cache',
|
||||
);
|
||||
return HiveCacheStore(directory.path, hiveBoxName: 'dio_cache');
|
||||
}
|
||||
|
||||
/// Provider for cache options
|
||||
@@ -371,31 +366,32 @@ Future<Dio> dio(Ref ref) async {
|
||||
// Base configuration
|
||||
dio
|
||||
..options = BaseOptions(
|
||||
baseUrl: ApiConstants.apiBaseUrl,
|
||||
connectTimeout: ApiConstants.connectionTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': ApiConstants.contentTypeJson,
|
||||
'Accept': ApiConstants.acceptJson,
|
||||
'Accept-Language': ApiConstants.acceptLanguageVi,
|
||||
},
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) {
|
||||
// Accept all status codes and handle errors in interceptor
|
||||
return status != null && status < 500;
|
||||
},
|
||||
)
|
||||
|
||||
// Add interceptors in order
|
||||
|
||||
baseUrl: ApiConstants.apiBaseUrl,
|
||||
connectTimeout: ApiConstants.connectionTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': ApiConstants.contentTypeJson,
|
||||
'Accept': ApiConstants.acceptJson,
|
||||
'Accept-Language': ApiConstants.acceptLanguageVi,
|
||||
},
|
||||
responseType: ResponseType.json,
|
||||
validateStatus: (status) {
|
||||
// Accept all status codes and handle errors in interceptor
|
||||
return status != null && status < 500;
|
||||
},
|
||||
)
|
||||
// Add interceptors in order
|
||||
// 1. Logging interceptor (first to log everything)
|
||||
..interceptors.add(ref.watch(loggingInterceptorProvider))
|
||||
|
||||
// 2. Auth interceptor (add tokens to requests)
|
||||
..interceptors.add(await ref.watch(authInterceptorProvider(dio).future))
|
||||
// 3. Cache interceptor
|
||||
..interceptors.add(DioCacheInterceptor(options: await ref.watch(cacheOptionsProvider.future)))
|
||||
..interceptors.add(
|
||||
DioCacheInterceptor(
|
||||
options: await ref.watch(cacheOptionsProvider.future),
|
||||
),
|
||||
)
|
||||
// 4. Retry interceptor
|
||||
..interceptors.add(RetryInterceptor(ref.watch(networkInfoProvider)))
|
||||
// 5. Error transformer (last to transform all errors)
|
||||
@@ -430,9 +426,7 @@ class ApiRequestOptions {
|
||||
final bool forceRefresh;
|
||||
|
||||
/// Options with cache enabled
|
||||
static const cached = ApiRequestOptions(
|
||||
cachePolicy: CachePolicy.forceCache,
|
||||
);
|
||||
static const cached = ApiRequestOptions(cachePolicy: CachePolicy.forceCache);
|
||||
|
||||
/// Options with network-first strategy
|
||||
static const networkFirst = ApiRequestOptions(
|
||||
@@ -449,12 +443,9 @@ class ApiRequestOptions {
|
||||
Options toDioOptions() {
|
||||
return Options(
|
||||
extra: <String, dynamic>{
|
||||
if (cachePolicy != null)
|
||||
CacheResponse.cacheKey: cachePolicy!.index,
|
||||
if (cacheDuration != null)
|
||||
'maxStale': cacheDuration,
|
||||
if (forceRefresh)
|
||||
'policy': CachePolicy.refresh.index,
|
||||
if (cachePolicy != null) CacheResponse.cacheKey: cachePolicy!.index,
|
||||
if (cacheDuration != null) 'maxStale': cacheDuration,
|
||||
if (forceRefresh) 'policy': CachePolicy.refresh.index,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -487,10 +478,10 @@ class QueuedRequest {
|
||||
final DateTime timestamp;
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'method': method,
|
||||
'path': path,
|
||||
'data': data,
|
||||
'queryParameters': queryParameters,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
'method': method,
|
||||
'path': path,
|
||||
'data': data,
|
||||
'queryParameters': queryParameters,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,7 +191,9 @@ class NetworkInfoImpl implements NetworkInfo {
|
||||
return !results.contains(ConnectivityResult.none);
|
||||
}
|
||||
|
||||
NetworkConnectionType _mapConnectivityResult(List<ConnectivityResult> results) {
|
||||
NetworkConnectionType _mapConnectivityResult(
|
||||
List<ConnectivityResult> results,
|
||||
) {
|
||||
if (results.isEmpty || results.contains(ConnectivityResult.none)) {
|
||||
return NetworkConnectionType.none;
|
||||
}
|
||||
@@ -273,14 +275,11 @@ class NetworkStatusNotifier extends _$NetworkStatusNotifier {
|
||||
final status = await networkInfo.networkStatus;
|
||||
|
||||
// Listen to network changes
|
||||
ref.listen(
|
||||
networkStatusStreamProvider,
|
||||
(_, next) {
|
||||
next.whenData((newStatus) {
|
||||
state = AsyncValue.data(newStatus);
|
||||
});
|
||||
},
|
||||
);
|
||||
ref.listen(networkStatusStreamProvider, (_, next) {
|
||||
next.whenData((newStatus) {
|
||||
state = AsyncValue.data(newStatus);
|
||||
});
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ Stream<bool> isOnline(Ref ref) {
|
||||
return connectivity.onConnectivityChanged.map((result) {
|
||||
// Online if connected to WiFi or mobile
|
||||
return result.contains(ConnectivityResult.wifi) ||
|
||||
result.contains(ConnectivityResult.mobile);
|
||||
result.contains(ConnectivityResult.mobile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ String appVersion(Ref ref) {
|
||||
int pointsMultiplier(Ref ref) {
|
||||
// Can read other providers
|
||||
final userTier = 'diamond'; // This would come from another provider
|
||||
return userTier == 'diamond' ? 3 : userTier == 'platinum' ? 2 : 1;
|
||||
return userTier == 'diamond'
|
||||
? 3
|
||||
: userTier == 'platinum'
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -46,7 +50,7 @@ int pointsMultiplier(Ref ref) {
|
||||
@riverpod
|
||||
Future<String> userData(Ref ref) async {
|
||||
// Simulate API call
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
return 'User Data';
|
||||
}
|
||||
|
||||
@@ -55,7 +59,7 @@ Future<String> userData(Ref ref) async {
|
||||
@riverpod
|
||||
Future<String> userProfile(Ref ref, String userId) async {
|
||||
// Simulate API call with userId
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
return 'Profile for user: $userId';
|
||||
}
|
||||
|
||||
@@ -70,7 +74,7 @@ Future<List<String>> productList(
|
||||
String? searchQuery,
|
||||
}) async {
|
||||
// Simulate API call with parameters
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
return ['Product 1', 'Product 2', 'Product 3'];
|
||||
}
|
||||
|
||||
@@ -82,10 +86,7 @@ Future<List<String>> productList(
|
||||
/// Use this for WebSocket connections, real-time updates, etc.
|
||||
@riverpod
|
||||
Stream<int> timer(Ref ref) {
|
||||
return Stream.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(count) => count,
|
||||
);
|
||||
return Stream.periodic(const Duration(seconds: 1), (count) => count);
|
||||
}
|
||||
|
||||
/// Stream provider with parameters
|
||||
@@ -176,7 +177,7 @@ class UserProfileNotifier extends _$UserProfileNotifier {
|
||||
@override
|
||||
Future<UserProfileData> build() async {
|
||||
// Fetch initial data
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
return UserProfileData(name: 'John Doe', email: 'john@example.com');
|
||||
}
|
||||
|
||||
@@ -225,10 +226,7 @@ class UserProfileData {
|
||||
final String email;
|
||||
|
||||
UserProfileData copyWith({String? name, String? email}) {
|
||||
return UserProfileData(
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
);
|
||||
return UserProfileData(name: name ?? this.name, email: email ?? this.email);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ final class UserDataProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$userDataHash() => r'3df905d6ea9f81ce7ca8205bd785ad4d4376b399';
|
||||
String _$userDataHash() => r'1b754e931a5d4c202189fcdd3de54815f93aaba2';
|
||||
|
||||
/// Async provider with parameters (Family pattern)
|
||||
/// Parameters are just function parameters - much simpler than before!
|
||||
@@ -248,7 +248,7 @@ final class UserProfileProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$userProfileHash() => r'd42ed517f41ce0dfde58d74b2beb3d8415b81a22';
|
||||
String _$userProfileHash() => r'35cdc3f9117e81c0150399d7015840ed034661d3';
|
||||
|
||||
/// Async provider with parameters (Family pattern)
|
||||
/// Parameters are just function parameters - much simpler than before!
|
||||
@@ -346,7 +346,7 @@ final class ProductListProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$productListHash() => r'aacee7761543692ccd59f0ecd2f290e1a7de203a';
|
||||
String _$productListHash() => r'db1568fb33a3615db0a31265c953d6db62b6535b';
|
||||
|
||||
/// Async provider with multiple parameters
|
||||
/// Named parameters, optional parameters, defaults - all supported!
|
||||
@@ -732,7 +732,7 @@ final class UserProfileNotifierProvider
|
||||
}
|
||||
|
||||
String _$userProfileNotifierHash() =>
|
||||
r'87c9a9277552095a0ed0b768829e2930fa475c7f';
|
||||
r'be7bcbe81f84be6ef50e94f52e0c65b00230291c';
|
||||
|
||||
/// AsyncNotifier for state that requires async initialization
|
||||
/// Perfect for fetching data that can then be modified
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/features/account/presentation/pages/addresses_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||
import 'package:worker/features/auth/presentation/pages/login_page.dart';
|
||||
import 'package:worker/features/auth/presentation/pages/register_page.dart';
|
||||
import 'package:worker/features/cart/presentation/pages/cart_page.dart';
|
||||
import 'package:worker/features/cart/presentation/pages/checkout_page.dart';
|
||||
import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
|
||||
@@ -46,10 +48,24 @@ class AppRouter {
|
||||
/// Router configuration
|
||||
static final GoRouter router = GoRouter(
|
||||
// Initial route
|
||||
initialLocation: RouteNames.home,
|
||||
initialLocation: RouteNames.login,
|
||||
|
||||
// Route definitions
|
||||
routes: [
|
||||
// Authentication Routes
|
||||
GoRoute(
|
||||
path: RouteNames.login,
|
||||
name: RouteNames.login,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const LoginPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.register,
|
||||
name: RouteNames.register,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const RegisterPage()),
|
||||
),
|
||||
|
||||
// Main Route (with bottom navigation)
|
||||
GoRoute(
|
||||
path: RouteNames.home,
|
||||
@@ -278,8 +294,10 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: RouteNames.designRequestCreate,
|
||||
name: RouteNames.designRequestCreate,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const DesignRequestCreatePage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: const DesignRequestCreatePage(),
|
||||
),
|
||||
),
|
||||
|
||||
// Design Request Detail Route
|
||||
@@ -421,7 +439,8 @@ class RouteNames {
|
||||
|
||||
// Model Houses & Design Requests Routes
|
||||
static const String modelHouses = '/model-houses';
|
||||
static const String designRequestCreate = '/model-houses/design-request/create';
|
||||
static const String designRequestCreate =
|
||||
'/model-houses/design-request/create';
|
||||
static const String designRequestDetail = '/model-houses/design-request/:id';
|
||||
|
||||
// Authentication Routes (TODO: implement when auth feature is ready)
|
||||
@@ -434,6 +453,12 @@ class RouteNames {
|
||||
///
|
||||
/// Helper extensions for common navigation patterns.
|
||||
extension GoRouterExtension on BuildContext {
|
||||
/// Navigate to login page
|
||||
void goLogin() => go(RouteNames.login);
|
||||
|
||||
/// Navigate to register page
|
||||
void goRegister() => go(RouteNames.register);
|
||||
|
||||
/// Navigate to home page
|
||||
void goHome() => go(RouteNames.home);
|
||||
|
||||
|
||||
@@ -40,10 +40,7 @@ class AppTheme {
|
||||
color: AppColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColors.white,
|
||||
size: 24,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
|
||||
@@ -65,9 +62,7 @@ class AppTheme {
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
minimumSize: const Size(64, 48),
|
||||
),
|
||||
@@ -78,9 +73,7 @@ class AppTheme {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
),
|
||||
),
|
||||
@@ -91,9 +84,7 @@ class AppTheme {
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
minimumSize: const Size(64, 48),
|
||||
),
|
||||
@@ -103,7 +94,10 @@ class AppTheme {
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
|
||||
@@ -124,15 +118,9 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(
|
||||
color: AppColors.danger,
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
||||
),
|
||||
|
||||
// ==================== Bottom Navigation Bar Theme ====================
|
||||
@@ -144,10 +132,7 @@ class AppTheme {
|
||||
size: 28,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: 24,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
||||
selectedLabelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -182,26 +167,25 @@ class AppTheme {
|
||||
color: AppColors.white,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
|
||||
// ==================== Dialog Theme ====================
|
||||
dialogTheme: const DialogThemeData(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
).copyWith(
|
||||
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
dialogTheme:
|
||||
const DialogThemeData(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
).copyWith(
|
||||
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Snackbar Theme ====================
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
@@ -209,9 +193,7 @@ class AppTheme {
|
||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.white,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 4,
|
||||
),
|
||||
@@ -224,10 +206,7 @@ class AppTheme {
|
||||
),
|
||||
|
||||
// ==================== Icon Theme ====================
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColors.grey900,
|
||||
size: 24,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.grey900, size: 24),
|
||||
|
||||
// ==================== List Tile Theme ====================
|
||||
listTileTheme: ListTileThemeData(
|
||||
@@ -266,9 +245,7 @@ class AppTheme {
|
||||
return AppColors.white;
|
||||
}),
|
||||
checkColor: MaterialStateProperty.all(AppColors.white),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
|
||||
// ==================== Radio Theme ====================
|
||||
@@ -297,14 +274,15 @@ class AppTheme {
|
||||
),
|
||||
|
||||
// ==================== Tab Bar Theme ====================
|
||||
tabBarTheme: const TabBarThemeData(
|
||||
labelColor: AppColors.primaryBlue,
|
||||
unselectedLabelColor: AppColors.grey500,
|
||||
indicatorColor: AppColors.primaryBlue,
|
||||
).copyWith(
|
||||
labelStyle: AppTypography.labelLarge,
|
||||
unselectedLabelStyle: AppTypography.labelLarge,
|
||||
),
|
||||
tabBarTheme:
|
||||
const TabBarThemeData(
|
||||
labelColor: AppColors.primaryBlue,
|
||||
unselectedLabelColor: AppColors.grey500,
|
||||
indicatorColor: AppColors.primaryBlue,
|
||||
).copyWith(
|
||||
labelStyle: AppTypography.labelLarge,
|
||||
unselectedLabelStyle: AppTypography.labelLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,10 +316,7 @@ class AppTheme {
|
||||
color: AppColors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColors.white,
|
||||
size: 24,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
|
||||
@@ -363,9 +338,7 @@ class AppTheme {
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
minimumSize: const Size(64, 48),
|
||||
),
|
||||
@@ -375,7 +348,10 @@ class AppTheme {
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF2A2A2A),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
||||
@@ -396,15 +372,9 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(
|
||||
color: AppColors.danger,
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
||||
),
|
||||
|
||||
// ==================== Bottom Navigation Bar Theme ====================
|
||||
@@ -412,14 +382,8 @@ class AppTheme {
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
selectedItemColor: AppColors.lightBlue,
|
||||
unselectedItemColor: AppColors.grey500,
|
||||
selectedIconTheme: IconThemeData(
|
||||
size: 28,
|
||||
color: AppColors.lightBlue,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: 24,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
selectedIconTheme: IconThemeData(size: 28, color: AppColors.lightBlue),
|
||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
||||
selectedLabelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -449,9 +413,7 @@ class AppTheme {
|
||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.white,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 4,
|
||||
),
|
||||
|
||||
@@ -423,16 +423,14 @@ extension BuildContextExtensions on BuildContext {
|
||||
|
||||
/// Navigate to route
|
||||
Future<T?> push<T>(Widget page) {
|
||||
return Navigator.of(this).push<T>(
|
||||
MaterialPageRoute(builder: (_) => page),
|
||||
);
|
||||
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
|
||||
}
|
||||
|
||||
/// Navigate and replace current route
|
||||
Future<T?> pushReplacement<T>(Widget page) {
|
||||
return Navigator.of(this).pushReplacement<T, void>(
|
||||
MaterialPageRoute(builder: (_) => page),
|
||||
);
|
||||
return Navigator.of(
|
||||
this,
|
||||
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
|
||||
}
|
||||
|
||||
/// Pop current route
|
||||
|
||||
@@ -313,15 +313,21 @@ class TextFormatter {
|
||||
}
|
||||
|
||||
/// Truncate text with ellipsis
|
||||
static String truncate(String text, int maxLength, {String ellipsis = '...'}) {
|
||||
static String truncate(
|
||||
String text,
|
||||
int maxLength, {
|
||||
String ellipsis = '...',
|
||||
}) {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - ellipsis.length) + ellipsis;
|
||||
}
|
||||
|
||||
/// Remove diacritics from Vietnamese text
|
||||
static String removeDiacritics(String text) {
|
||||
const withDiacritics = 'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ';
|
||||
const withoutDiacritics = 'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd';
|
||||
const withDiacritics =
|
||||
'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ';
|
||||
const withoutDiacritics =
|
||||
'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd';
|
||||
|
||||
var result = text.toLowerCase();
|
||||
for (var i = 0; i < withDiacritics.length; i++) {
|
||||
|
||||
@@ -241,8 +241,11 @@ class L10nHelper {
|
||||
/// final pointsText = L10nHelper.formatPoints(context, 100);
|
||||
/// // Returns: "+100 điểm" (Vietnamese) or "+100 points" (English)
|
||||
/// ```
|
||||
static String formatPoints(BuildContext context, int points,
|
||||
{bool showSign = true}) {
|
||||
static String formatPoints(
|
||||
BuildContext context,
|
||||
int points, {
|
||||
bool showSign = true,
|
||||
}) {
|
||||
if (showSign && points > 0) {
|
||||
return context.l10n.earnedPoints(points);
|
||||
} else if (showSign && points < 0) {
|
||||
|
||||
@@ -66,9 +66,7 @@ class QRGenerator {
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
gapless: true,
|
||||
embeddedImageStyle: const QrEmbeddedImageStyle(
|
||||
size: Size(48, 48),
|
||||
),
|
||||
embeddedImageStyle: const QrEmbeddedImageStyle(size: Size(48, 48)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,9 +187,7 @@ class QRGenerator {
|
||||
embeddedImage: embeddedImage is AssetImage
|
||||
? (embeddedImage as AssetImage).assetName as ImageProvider
|
||||
: null,
|
||||
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||
size: embeddedImageSize,
|
||||
),
|
||||
embeddedImageStyle: QrEmbeddedImageStyle(size: embeddedImageSize),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -203,18 +199,12 @@ class QRGenerator {
|
||||
if (data.contains(':')) {
|
||||
final parts = data.split(':');
|
||||
if (parts.length == 2) {
|
||||
return {
|
||||
'type': parts[0].toUpperCase(),
|
||||
'value': parts[1],
|
||||
};
|
||||
return {'type': parts[0].toUpperCase(), 'value': parts[1]};
|
||||
}
|
||||
}
|
||||
|
||||
// If no type prefix, return as generic data
|
||||
return {
|
||||
'type': 'GENERIC',
|
||||
'value': data,
|
||||
};
|
||||
return {'type': 'GENERIC', 'value': data};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -244,9 +244,7 @@ class Validators {
|
||||
}
|
||||
|
||||
if (double.tryParse(value) == null) {
|
||||
return fieldName != null
|
||||
? '$fieldName phải là số'
|
||||
: 'Giá trị phải là số';
|
||||
return fieldName != null ? '$fieldName phải là số' : 'Giá trị phải là số';
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -351,7 +349,8 @@ class Validators {
|
||||
);
|
||||
|
||||
final today = DateTime.now();
|
||||
final age = today.year -
|
||||
final age =
|
||||
today.year -
|
||||
birthDate.year -
|
||||
(today.month > birthDate.month ||
|
||||
(today.month == birthDate.month && today.day >= birthDate.day)
|
||||
@@ -456,11 +455,7 @@ class Validators {
|
||||
// ========================================================================
|
||||
|
||||
/// Validate against custom regex pattern
|
||||
static String? pattern(
|
||||
String? value,
|
||||
RegExp pattern,
|
||||
String errorMessage,
|
||||
) {
|
||||
static String? pattern(String? value, RegExp pattern, String errorMessage) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Trường này là bắt buộc';
|
||||
}
|
||||
@@ -491,12 +486,7 @@ class Validators {
|
||||
}
|
||||
|
||||
/// Password strength enum
|
||||
enum PasswordStrength {
|
||||
weak,
|
||||
medium,
|
||||
strong,
|
||||
veryStrong,
|
||||
}
|
||||
enum PasswordStrength { weak, medium, strong, veryStrong }
|
||||
|
||||
/// Password strength calculator
|
||||
class PasswordStrengthCalculator {
|
||||
|
||||
@@ -58,10 +58,7 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.shopping_bag),
|
||||
label: 'Products',
|
||||
@@ -70,14 +67,8 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
icon: Icon(Icons.card_membership),
|
||||
label: 'Loyalty',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person),
|
||||
label: 'Account',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.menu),
|
||||
label: 'More',
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Account'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.menu), label: 'More'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,10 +124,7 @@ class CustomButton extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -135,10 +132,7 @@ class CustomButton extends StatelessWidget {
|
||||
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,7 @@ class EmptyState extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
Icon(icon, size: iconSize, color: AppColors.grey500),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
@@ -73,10 +69,7 @@ class EmptyState extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -58,15 +58,9 @@ class ChatFloatingButton extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 20,
|
||||
minHeight: 20,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
|
||||
child: Center(
|
||||
child: Text(
|
||||
unreadCount! > 99 ? '99+' : unreadCount.toString(),
|
||||
|
||||
@@ -50,10 +50,7 @@ class CustomLoadingIndicator extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user