add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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)

View File

@@ -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()};

View File

@@ -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',
);
}
}

View File

@@ -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.');
}
// ============================================================================

View File

@@ -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});
}

View File

@@ -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

View File

@@ -114,7 +114,7 @@ final class AuthInterceptorProvider
}
}
String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10';
String _$authInterceptorHash() => r'3f964536e03e204d09cc9120dd9d961b6d6d4b71';
/// Provider for AuthInterceptor

View File

@@ -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(),
};
}

View File

@@ -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;
}

View File

@@ -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);
});
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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,
),

View File

@@ -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

View File

@@ -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++) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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'),
],
);
}

View File

@@ -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),
);
}
}

View File

@@ -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,
),
],

View File

@@ -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(),

View File

@@ -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,
),
],