This commit is contained in:
2025-10-10 22:49:05 +07:00
parent 02941e2234
commit 38c16bf0b9
49 changed files with 2702 additions and 740 deletions

View File

@@ -75,6 +75,10 @@ class ApiConstants {
/// Use: '${ApiConstants.categories}/:id'
static String categoryById(String id) => '$categories/$id';
/// GET - Fetch category with its products
/// Use: '${ApiConstants.categories}/:id/products'
static String categoryWithProducts(String id) => '$categories/$id/products';
/// POST - Sync categories (bulk update/create)
static const String syncCategories = '$categories/sync';

View File

@@ -14,7 +14,7 @@
/// - Storage: Secure storage, database
/// - Theme: Material 3 theme, colors, typography
/// - Utils: Formatters, validators, extensions, helpers
/// - DI: Dependency injection setup
/// - Providers: Riverpod providers for core dependencies
/// - Widgets: Reusable UI components
/// - Errors: Exception and failure handling
library;
@@ -23,7 +23,6 @@ library;
export 'config/config.dart';
export 'constants/constants.dart';
export 'database/database.dart';
export 'di/di.dart';
export 'errors/errors.dart';
export 'network/network.dart';
export 'performance.dart';

View File

@@ -19,6 +19,7 @@ class SeedData {
color: '#2196F3', // Blue
productCount: 0,
createdAt: now.subtract(const Duration(days: 60)),
updatedAt: now.subtract(const Duration(days: 60)),
),
CategoryModel(
id: 'cat_appliances',
@@ -28,6 +29,7 @@ class SeedData {
color: '#4CAF50', // Green
productCount: 0,
createdAt: now.subtract(const Duration(days: 55)),
updatedAt: now.subtract(const Duration(days: 55)),
),
CategoryModel(
id: 'cat_sports',
@@ -37,6 +39,7 @@ class SeedData {
color: '#FF9800', // Orange
productCount: 0,
createdAt: now.subtract(const Duration(days: 50)),
updatedAt: now.subtract(const Duration(days: 50)),
),
CategoryModel(
id: 'cat_fashion',
@@ -46,6 +49,7 @@ class SeedData {
color: '#E91E63', // Pink
productCount: 0,
createdAt: now.subtract(const Duration(days: 45)),
updatedAt: now.subtract(const Duration(days: 45)),
),
CategoryModel(
id: 'cat_books',
@@ -55,6 +59,7 @@ class SeedData {
color: '#9C27B0', // Purple
productCount: 0,
createdAt: now.subtract(const Duration(days: 40)),
updatedAt: now.subtract(const Duration(days: 40)),
),
];
}

View File

@@ -1,7 +0,0 @@
/// Export all dependency injection components
///
/// Contains service locator and injection container setup
library;
export 'injection_container.dart';
export 'service_locator.dart';

View File

@@ -1,74 +0,0 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import '../../features/auth/data/datasources/auth_remote_datasource.dart';
import '../../features/auth/data/repositories/auth_repository_impl.dart';
import '../../features/auth/domain/repositories/auth_repository.dart';
import '../network/dio_client.dart';
import '../network/network_info.dart';
import '../storage/secure_storage.dart';
/// Service locator instance
final sl = GetIt.instance;
/// Initialize all dependencies
///
/// This function registers all the dependencies required by the app
/// in the GetIt service locator. Call this in main() before runApp().
Future<void> initDependencies() async {
// ===== Core =====
// Connectivity (external) - Register first as it's a dependency
sl.registerLazySingleton<Connectivity>(
() => Connectivity(),
);
// Network Info
sl.registerLazySingleton<NetworkInfo>(
() => NetworkInfo(sl()),
);
// Dio Client
sl.registerLazySingleton<DioClient>(
() => DioClient(),
);
// Secure Storage
sl.registerLazySingleton<SecureStorage>(
() => SecureStorage(),
);
// ===== Authentication Feature =====
// Auth Remote Data Source
sl.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImpl(dioClient: sl()),
);
// Auth Repository
sl.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImpl(
remoteDataSource: sl(),
secureStorage: sl(),
dioClient: sl(),
),
);
// ===== Data Sources =====
// Note: Other data sources are managed by Riverpod providers
// No direct registration needed here
// ===== Repositories =====
// TODO: Register other repositories when they are implemented
// ===== Use Cases =====
// TODO: Register use cases when they are implemented
// ===== Providers (Riverpod) =====
// Note: Riverpod providers are registered differently
// This is just for dependency injection of external dependencies
}
/// Clear all dependencies (useful for testing)
void resetDependencies() {
sl.reset();
}

View File

@@ -1,22 +0,0 @@
import 'package:get_it/get_it.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../network/dio_client.dart';
import '../network/network_info.dart';
final getIt = GetIt.instance;
/// Setup dependency injection
Future<void> setupServiceLocator() async {
// External dependencies
getIt.registerLazySingleton(() => Connectivity());
// Core
getIt.registerLazySingleton(() => DioClient());
getIt.registerLazySingleton(() => NetworkInfo(getIt()));
// Data sources - to be added when features are implemented
// Repositories - to be added when features are implemented
// Use cases - to be added when features are implemented
}

View File

@@ -0,0 +1,104 @@
/// Generic API Response wrapper
///
/// Wraps all API responses in a consistent format with success status,
/// data payload, optional message, and optional pagination metadata.
class ApiResponse<T> {
final bool success;
final T data;
final String? message;
final PaginationMeta? meta;
const ApiResponse({
required this.success,
required this.data,
this.message,
this.meta,
});
/// Create from JSON with a data parser function
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic) dataParser,
) {
return ApiResponse(
success: json['success'] as bool? ?? false,
data: dataParser(json['data']),
message: json['message'] as String?,
meta: json['meta'] != null
? PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>)
: null,
);
}
/// Convert to JSON
Map<String, dynamic> toJson(dynamic Function(T) dataSerializer) {
return {
'success': success,
'data': dataSerializer(data),
if (message != null) 'message': message,
if (meta != null) 'meta': meta!.toJson(),
};
}
}
/// Pagination metadata
class PaginationMeta {
final int page;
final int limit;
final int total;
final int totalPages;
final bool hasPreviousPage;
final bool hasNextPage;
const PaginationMeta({
required this.page,
required this.limit,
required this.total,
required this.totalPages,
required this.hasPreviousPage,
required this.hasNextPage,
});
/// Create from JSON
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
return PaginationMeta(
page: json['page'] as int,
limit: json['limit'] as int,
total: json['total'] as int,
totalPages: json['totalPages'] as int,
hasPreviousPage: json['hasPreviousPage'] as bool,
hasNextPage: json['hasNextPage'] as bool,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'page': page,
'limit': limit,
'total': total,
'totalPages': totalPages,
'hasPreviousPage': hasPreviousPage,
'hasNextPage': hasNextPage,
};
}
/// Create a copy with updated fields
PaginationMeta copyWith({
int? page,
int? limit,
int? total,
int? totalPages,
bool? hasPreviousPage,
bool? hasNextPage,
}) {
return PaginationMeta(
page: page ?? this.page,
limit: limit ?? this.limit,
total: total ?? this.total,
totalPages: totalPages ?? this.totalPages,
hasPreviousPage: hasPreviousPage ?? this.hasPreviousPage,
hasNextPage: hasNextPage ?? this.hasNextPage,
);
}
}

View File

@@ -4,5 +4,6 @@
library;
export 'api_interceptor.dart';
export 'api_response.dart';
export 'dio_client.dart';
export 'network_info.dart';

View File

@@ -0,0 +1,23 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../network/dio_client.dart';
import '../storage/secure_storage.dart';
part 'core_providers.g.dart';
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
@Riverpod(keepAlive: true)
DioClient dioClient(Ref ref) {
return DioClient();
}
/// Provider for SecureStorage (singleton)
///
/// This is the global secure storage used for storing sensitive data like tokens.
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
@Riverpod(keepAlive: true)
SecureStorage secureStorage(Ref ref) {
return SecureStorage();
}

View File

@@ -0,0 +1,119 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'core_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
@ProviderFor(dioClient)
const dioClientProvider = DioClientProvider._();
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
final class DioClientProvider
extends $FunctionalProvider<DioClient, DioClient, DioClient>
with $Provider<DioClient> {
/// Provider for DioClient (singleton)
///
/// This is the global HTTP client used across the entire app.
/// It's configured with interceptors, timeout settings, and auth token injection.
const DioClientProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dioClientProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dioClientHash();
@$internal
@override
$ProviderElement<DioClient> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
DioClient create(Ref ref) {
return dioClient(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(DioClient value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<DioClient>(value),
);
}
}
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';
/// Provider for SecureStorage (singleton)
///
/// This is the global secure storage used for storing sensitive data like tokens.
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
@ProviderFor(secureStorage)
const secureStorageProvider = SecureStorageProvider._();
/// Provider for SecureStorage (singleton)
///
/// This is the global secure storage used for storing sensitive data like tokens.
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
final class SecureStorageProvider
extends $FunctionalProvider<SecureStorage, SecureStorage, SecureStorage>
with $Provider<SecureStorage> {
/// Provider for SecureStorage (singleton)
///
/// This is the global secure storage used for storing sensitive data like tokens.
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
const SecureStorageProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'secureStorageProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$secureStorageHash();
@$internal
@override
$ProviderElement<SecureStorage> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
SecureStorage create(Ref ref) {
return secureStorage(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(SecureStorage value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<SecureStorage>(value),
);
}
}
String _$secureStorageHash() => r'5c9908c0046ad0e39469ee7acbb5540397b36693';

View File

@@ -1,3 +1,4 @@
/// Export all core providers
export 'core_providers.dart';
export 'network_info_provider.dart';
export 'sync_status_provider.dart';

View File

@@ -45,10 +45,10 @@ class SyncStatus extends _$SyncStatus {
try {
// Sync categories first (products depend on categories)
await ref.read(categoriesProvider.notifier).syncCategories();
await ref.read(categoriesProvider.notifier).refresh();
// Then sync products
await ref.read(productsProvider.notifier).syncProducts();
await ref.read(productsProvider.notifier).refresh();
// Update last sync time in settings
await ref.read(settingsProvider.notifier).updateLastSyncTime();
@@ -100,7 +100,7 @@ class SyncStatus extends _$SyncStatus {
);
try {
await ref.read(productsProvider.notifier).syncProducts();
await ref.read(productsProvider.notifier).refresh();
await ref.read(settingsProvider.notifier).updateLastSyncTime();
state = AsyncValue.data(
@@ -146,7 +146,7 @@ class SyncStatus extends _$SyncStatus {
);
try {
await ref.read(categoriesProvider.notifier).syncCategories();
await ref.read(categoriesProvider.notifier).refresh();
await ref.read(settingsProvider.notifier).updateLastSyncTime();
state = AsyncValue.data(

View File

@@ -36,7 +36,7 @@ final class SyncStatusProvider
SyncStatus create() => SyncStatus();
}
String _$syncStatusHash() => r'dc92a1b83c89af94dfe94b646aa81d9501f371d7';
String _$syncStatusHash() => r'bf09683a3a67b6c7104274c7a4b92ee410de8e45';
/// Sync status provider - manages data synchronization state