From 628c81ce1396b13d9bd195c6899f84aedc957165 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 17 Oct 2025 17:22:28 +0700 Subject: [PATCH] runable --- .claude/agents/api-expert.md | 128 +- .claude/agents/architecture-expert.md | 244 +- .claude/agents/flutter-iap-expert.md | 204 - .claude/agents/flutter-widget-expert.md | 134 +- .claude/agents/hive-expert.md | 508 +-- .claude/agents/performance-expert.md | 600 +-- .claude/agents/riverpod-expert.md | 1080 +++--- CLAUDE.md | 6 +- HIVE_SETUP.md | 522 +++ LOCALIZATION.md | 760 ++++ RIVERPOD_SETUP.md | 626 +++ RIVERPOD_SUMMARY.md | 551 +++ analysis_options.yaml | 158 +- .../reports/problems/problems-report.html | 663 ++++ build.yaml | 47 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 + ios/Podfile.lock | 145 + ios/Runner.xcodeproj/project.pbxproj | 130 + .../contents.xcworkspacedata | 3 + ios/Runner/Info.plist | 4 + l10n.yaml | 5 + lib/app.dart | 353 ++ lib/core/constants/api_constants.dart | 408 ++ lib/core/constants/app_constants.dart | 521 +++ lib/core/constants/storage_constants.dart | 211 ++ lib/core/constants/ui_constants.dart | 467 +++ lib/core/database/QUICK_START.md | 119 + lib/core/database/README.md | 478 +++ lib/core/database/database.dart | 25 + lib/core/database/database_manager.dart | 411 ++ lib/core/database/hive_initializer.dart | 115 + lib/core/database/hive_service.dart | 409 ++ lib/core/database/models/cached_data.dart | 79 + lib/core/database/models/cached_data.g.dart | 50 + lib/core/database/models/enums.dart | 425 +++ lib/core/database/models/enums.g.dart | 517 +++ lib/core/errors/exceptions.dart | 351 ++ lib/core/errors/failures.dart | 262 ++ lib/core/network/README.md | 449 +++ lib/core/network/api_interceptor.dart | 572 +++ lib/core/network/api_interceptor.g.dart | 246 ++ lib/core/network/dio_client.dart | 496 +++ lib/core/network/dio_client.g.dart | 177 + lib/core/network/network_info.dart | 365 ++ lib/core/network/network_info.g.dart | 282 ++ lib/core/providers/QUICK_REFERENCE.md | 462 +++ lib/core/providers/README.md | 454 +++ lib/core/providers/connectivity_provider.dart | 113 + .../providers/connectivity_provider.g.dart | 283 ++ lib/core/providers/provider_examples.dart | 474 +++ lib/core/providers/provider_examples.g.dart | 1155 ++++++ lib/core/theme/app_theme.dart | 460 +++ lib/core/theme/colors.dart | 68 + lib/core/theme/typography.dart | 243 ++ lib/core/utils/README_L10N.md | 278 ++ lib/core/utils/extensions.dart | 471 +++ lib/core/utils/formatters.dart | 371 ++ lib/core/utils/l10n_extensions.dart | 274 ++ lib/core/utils/localization_extension.dart | 136 + lib/core/utils/qr_generator.dart | 308 ++ lib/core/utils/validators.dart | 540 +++ lib/core/widgets/bottom_nav_bar.dart | 84 + lib/core/widgets/custom_button.dart | 144 + lib/core/widgets/empty_state.dart | 112 + lib/core/widgets/error_widget.dart | 85 + lib/core/widgets/floating_chat_button.dart | 87 + lib/core/widgets/loading_indicator.dart | 64 + lib/generated/l10n/app_localizations.dart | 3368 +++++++++++++++++ lib/generated/l10n/app_localizations_en.dart | 1738 +++++++++ lib/generated/l10n/app_localizations_vi.dart | 1735 +++++++++ lib/hive_registrar.g.dart | 39 + lib/l10n/app_en.arb | 914 +++++ lib/l10n/app_vi.arb | 914 +++++ lib/main.dart | 347 +- lib/shared/widgets/custom_app_bar.dart | 145 + lib/shared/widgets/date_picker_field.dart | 375 ++ lib/shared/widgets/gradient_card.dart | 270 ++ lib/shared/widgets/price_display.dart | 269 ++ lib/shared/widgets/status_badge.dart | 245 ++ .../widgets/vietnamese_phone_field.dart | 271 ++ pubspec.lock | 1267 ++++++- pubspec.yaml | 83 +- scripts/setup_riverpod.sh | 30 + test/widget_test.dart | 22 +- 86 files changed, 31339 insertions(+), 1710 deletions(-) delete mode 100644 .claude/agents/flutter-iap-expert.md create mode 100644 HIVE_SETUP.md create mode 100644 LOCALIZATION.md create mode 100644 RIVERPOD_SETUP.md create mode 100644 RIVERPOD_SUMMARY.md create mode 100644 android/build/reports/problems/problems-report.html create mode 100644 build.yaml create mode 100644 ios/Podfile create mode 100644 ios/Podfile.lock create mode 100644 l10n.yaml create mode 100644 lib/app.dart create mode 100644 lib/core/constants/api_constants.dart create mode 100644 lib/core/constants/app_constants.dart create mode 100644 lib/core/constants/storage_constants.dart create mode 100644 lib/core/constants/ui_constants.dart create mode 100644 lib/core/database/QUICK_START.md create mode 100644 lib/core/database/README.md create mode 100644 lib/core/database/database.dart create mode 100644 lib/core/database/database_manager.dart create mode 100644 lib/core/database/hive_initializer.dart create mode 100644 lib/core/database/hive_service.dart create mode 100644 lib/core/database/models/cached_data.dart create mode 100644 lib/core/database/models/cached_data.g.dart create mode 100644 lib/core/database/models/enums.dart create mode 100644 lib/core/database/models/enums.g.dart create mode 100644 lib/core/errors/exceptions.dart create mode 100644 lib/core/errors/failures.dart create mode 100644 lib/core/network/README.md create mode 100644 lib/core/network/api_interceptor.dart create mode 100644 lib/core/network/api_interceptor.g.dart create mode 100644 lib/core/network/dio_client.dart create mode 100644 lib/core/network/dio_client.g.dart create mode 100644 lib/core/network/network_info.dart create mode 100644 lib/core/network/network_info.g.dart create mode 100644 lib/core/providers/QUICK_REFERENCE.md create mode 100644 lib/core/providers/README.md create mode 100644 lib/core/providers/connectivity_provider.dart create mode 100644 lib/core/providers/connectivity_provider.g.dart create mode 100644 lib/core/providers/provider_examples.dart create mode 100644 lib/core/providers/provider_examples.g.dart create mode 100644 lib/core/theme/app_theme.dart create mode 100644 lib/core/theme/colors.dart create mode 100644 lib/core/theme/typography.dart create mode 100644 lib/core/utils/README_L10N.md create mode 100644 lib/core/utils/extensions.dart create mode 100644 lib/core/utils/formatters.dart create mode 100644 lib/core/utils/l10n_extensions.dart create mode 100644 lib/core/utils/localization_extension.dart create mode 100644 lib/core/utils/qr_generator.dart create mode 100644 lib/core/utils/validators.dart create mode 100644 lib/core/widgets/bottom_nav_bar.dart create mode 100644 lib/core/widgets/custom_button.dart create mode 100644 lib/core/widgets/empty_state.dart create mode 100644 lib/core/widgets/error_widget.dart create mode 100644 lib/core/widgets/floating_chat_button.dart create mode 100644 lib/core/widgets/loading_indicator.dart create mode 100644 lib/generated/l10n/app_localizations.dart create mode 100644 lib/generated/l10n/app_localizations_en.dart create mode 100644 lib/generated/l10n/app_localizations_vi.dart create mode 100644 lib/hive_registrar.g.dart create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_vi.arb create mode 100644 lib/shared/widgets/custom_app_bar.dart create mode 100644 lib/shared/widgets/date_picker_field.dart create mode 100644 lib/shared/widgets/gradient_card.dart create mode 100644 lib/shared/widgets/price_display.dart create mode 100644 lib/shared/widgets/status_badge.dart create mode 100644 lib/shared/widgets/vietnamese_phone_field.dart create mode 100755 scripts/setup_riverpod.sh diff --git a/.claude/agents/api-expert.md b/.claude/agents/api-expert.md index c8ecdb6..617e61a 100644 --- a/.claude/agents/api-expert.md +++ b/.claude/agents/api-expert.md @@ -1,120 +1,120 @@ -[//]: # (---) +--- -[//]: # (name: api-integration-expert) +name: api-integration-expert -[//]: # (description: HTTP client and API integration specialist. MUST BE USED for API calls, network operations, Dio configuration, error handling, and REST endpoint integration.) +description: HTTP client and API integration specialist. MUST BE USED for API calls, network operations, Dio configuration, error handling, and REST endpoint integration. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are an API integration expert specializing in:) -[//]: # (- HTTP client configuration with Dio) +You are an API integration expert specializing in: -[//]: # (- RESTful API integration for backend services) +- HTTP client configuration with Dio -[//]: # (- Network error handling and retry strategies) +- RESTful API integration for backend services -[//]: # (- API authentication (OAuth, JWT, API keys, etc.)) +- Network error handling and retry strategies -[//]: # (- Response parsing and data transformation) +- API authentication (OAuth, JWT, API keys, etc.) -[//]: # (- Network connectivity and offline handling) +- Response parsing and data transformation -[//]: # () -[//]: # (## Key Responsibilities:) +- Network connectivity and offline handling -[//]: # (- Design robust API clients for backend services) -[//]: # (- Implement proper error handling for network failures) +## Key Responsibilities: -[//]: # (- Configure Dio interceptors for authentication and logging) +- Design robust API clients for backend services -[//]: # (- Handle API response parsing and model mapping) +- Implement proper error handling for network failures -[//]: # (- Implement proper timeout and retry mechanisms) +- Configure Dio interceptors for authentication and logging -[//]: # (- Design offline-first architecture with network fallbacks) +- Handle API response parsing and model mapping -[//]: # () -[//]: # (## Always Check First:) +- Implement proper timeout and retry mechanisms -[//]: # (- `lib/core/network/` or `lib/services/` - Existing API client structure) +- Design offline-first architecture with network fallbacks -[//]: # (- `lib/models/` - Data models for API responses) -[//]: # (- Current Dio configuration and interceptors) +## Always Check First: -[//]: # (- Authentication patterns in use) +- `lib/core/network/` or `lib/services/` - Existing API client structure -[//]: # (- Error handling strategies already implemented) +- `lib/models/` - Data models for API responses -[//]: # () -[//]: # (## Implementation Focus:) +- Current Dio configuration and interceptors -[//]: # (- Create type-safe API clients with proper error types) +- Authentication patterns in use -[//]: # (- Implement proper HTTP status code handling) +- Error handling strategies already implemented -[//]: # (- Design cacheable API responses for offline support) -[//]: # (- Use proper request/response logging for debugging) +## Implementation Focus: -[//]: # (- Handle API versioning and endpoint configuration) +- Create type-safe API clients with proper error types -[//]: # (- Implement proper connection testing for service validation) +- Implement proper HTTP status code handling -[//]: # () -[//]: # (## Error Handling Patterns:) +- Design cacheable API responses for offline support -[//]: # (- Network connectivity errors) +- Use proper request/response logging for debugging -[//]: # (- API authentication failures (401, 403)) +- Handle API versioning and endpoint configuration -[//]: # (- Service unavailability scenarios (500, 503)) +- Implement proper connection testing for service validation -[//]: # (- Invalid credentials or token errors) -[//]: # (- Rate limiting and throttling responses (429)) +## Error Handling Patterns: -[//]: # (- Timeout and connection errors) +- Network connectivity errors -[//]: # (- Request validation errors (400, 422)) +- API authentication failures (401, 403) -[//]: # () -[//]: # (## Authentication Strategies:) +- Service unavailability scenarios (500, 503) -[//]: # (- JWT token management (access + refresh tokens)) +- Invalid credentials or token errors -[//]: # (- API key authentication in headers) +- Rate limiting and throttling responses (429) -[//]: # (- OAuth 2.0 flow implementation) +- Timeout and connection errors -[//]: # (- Token storage and retrieval (secure storage)) +- Request validation errors (400, 422) -[//]: # (- Automatic token refresh on 401) -[//]: # (- Credential validation and testing) +## Authentication Strategies: -[//]: # () -[//]: # (## Best Practices:) +- JWT token management (access + refresh tokens) -[//]: # (- Use Dio for HTTP client with proper configuration) +- API key authentication in headers -[//]: # (- Implement request/response interceptors) +- OAuth 2.0 flow implementation -[//]: # (- Create custom exceptions for different error types) +- Token storage and retrieval (secure storage) -[//]: # (- Use proper JSON serialization with generated models) +- Automatic token refresh on 401 -[//]: # (- Implement proper base URL and endpoint management) +- Credential validation and testing -[//]: # (- Design testable API clients with dependency injection) -[//]: # (- Handle multipart/form-data for file uploads) +## Best Practices: -[//]: # (- Implement proper request cancellation) +- Use Dio for HTTP client with proper configuration -[//]: # (- Use connection pooling for better performance) \ No newline at end of file +- Implement request/response interceptors + +- Create custom exceptions for different error types + +- Use proper JSON serialization with generated models + +- Implement proper base URL and endpoint management + +- Design testable API clients with dependency injection + +- Handle multipart/form-data for file uploads + +- Implement proper request cancellation + +- Use connection pooling for better performance \ No newline at end of file diff --git a/.claude/agents/architecture-expert.md b/.claude/agents/architecture-expert.md index 6be9b33..d31de9c 100644 --- a/.claude/agents/architecture-expert.md +++ b/.claude/agents/architecture-expert.md @@ -1,229 +1,229 @@ -[//]: # (---) +--- -[//]: # (name: architecture-expert) +name: architecture-expert -[//]: # (description: Clean architecture and project structure specialist. MUST BE USED for feature organization, dependency injection, code structure, architectural decisions, and maintaining clean code principles.) +description: Clean architecture and project structure specialist. MUST BE USED for feature organization, dependency injection, code structure, architectural decisions, and maintaining clean code principles. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are a software architecture expert specializing in:) -[//]: # (- Clean architecture implementation in Flutter) +You are a software architecture expert specializing in: -[//]: # (- Feature-first project organization) +- Clean architecture implementation in Flutter -[//]: # (- Dependency injection with GetIt) +- Feature-first project organization -[//]: # (- Repository pattern and data layer abstraction) +- Dependency injection with GetIt -[//]: # (- SOLID principles and design patterns) +- Repository pattern and data layer abstraction -[//]: # (- Code organization and module separation) +- SOLID principles and design patterns -[//]: # () -[//]: # (## Key Responsibilities:) +- Code organization and module separation -[//]: # (- Design scalable feature-first architecture) -[//]: # (- Implement proper separation of concerns) +## Key Responsibilities: -[//]: # (- Create maintainable dependency injection setup) +- Design scalable feature-first architecture -[//]: # (- Ensure proper abstraction layers (data, domain, presentation)) +- Implement proper separation of concerns -[//]: # (- Design testable architecture patterns) +- Create maintainable dependency injection setup -[//]: # (- Maintain consistency with existing project structure) +- Ensure proper abstraction layers (data, domain, presentation) -[//]: # () -[//]: # (## Architecture Patterns:) +- Design testable architecture patterns -[//]: # (- **Feature-First Structure**: Organize by features, not by layer) +- Maintain consistency with existing project structure -[//]: # (- **Clean Architecture**: Data → Domain → Presentation layers) -[//]: # (- **Repository Pattern**: Abstract data sources (API + local cache)) +## Architecture Patterns: -[//]: # (- **Provider Pattern**: Riverpod for state management) +- **Feature-First Structure**: Organize by features, not by layer -[//]: # (- **Service Layer**: Business logic and use cases) +- **Clean Architecture**: Data → Domain → Presentation layers -[//]: # () -[//]: # (## Always Check First:) +- **Repository Pattern**: Abstract data sources (API + local cache) -[//]: # (- `lib/` - Current project structure and organization) +- **Provider Pattern**: Riverpod for state management -[//]: # (- `lib/core/` - Shared utilities and dependency injection) +- **Service Layer**: Business logic and use cases -[//]: # (- `lib/features/` - Feature-specific organization patterns) -[//]: # (- Existing dependency injection setup) +## Always Check First: -[//]: # (- Current repository and service patterns) +- `lib/` - Current project structure and organization -[//]: # () -[//]: # (## Structural Guidelines:) +- `lib/core/` - Shared utilities and dependency injection -[//]: # (```) +- `lib/features/` - Feature-specific organization patterns -[//]: # (lib/) +- Existing dependency injection setup -[//]: # ( core/) +- Current repository and service patterns -[//]: # ( di/ # Dependency injection setup) -[//]: # ( constants/ # App-wide constants) +## Structural Guidelines: -[//]: # ( theme/ # Material 3 theme configuration) +``` -[//]: # ( utils/ # Shared utilities) +lib/ -[//]: # ( widgets/ # Reusable widgets) + core/ -[//]: # ( network/ # HTTP client configuration) + di/ # Dependency injection setup -[//]: # ( errors/ # Custom exception classes) + constants/ # App-wide constants -[//]: # ( features/) + theme/ # Material 3 theme configuration -[//]: # ( feature_name/) + utils/ # Shared utilities -[//]: # ( data/) + widgets/ # Reusable widgets -[//]: # ( datasources/ # API + local data sources) + network/ # HTTP client configuration -[//]: # ( models/ # Data transfer objects) + errors/ # Custom exception classes -[//]: # ( repositories/ # Repository implementations) + features/ -[//]: # ( domain/) + feature_name/ -[//]: # ( entities/ # Business entities) + data/ -[//]: # ( repositories/ # Repository interfaces) + datasources/ # API + local data sources -[//]: # ( usecases/ # Business logic) + models/ # Data transfer objects -[//]: # ( presentation/) + repositories/ # Repository implementations -[//]: # ( providers/ # Riverpod providers) + domain/ -[//]: # ( pages/ # UI screens) + entities/ # Business entities -[//]: # ( widgets/ # Feature-specific widgets) + repositories/ # Repository interfaces -[//]: # ( shared/) + usecases/ # Business logic -[//]: # ( widgets/ # Cross-feature reusable widgets) + presentation/ -[//]: # ( models/ # Shared data models) + providers/ # Riverpod providers -[//]: # (```) + pages/ # UI screens -[//]: # () -[//]: # (## Design Principles:) + widgets/ # Feature-specific widgets -[//]: # (- **Single Responsibility**: Each class has one reason to change) + shared/ -[//]: # (- **Dependency Inversion**: Depend on abstractions, not concretions) + widgets/ # Cross-feature reusable widgets -[//]: # (- **Interface Segregation**: Small, focused interfaces) + models/ # Shared data models -[//]: # (- **Don't Repeat Yourself**: Shared logic in core utilities) +``` -[//]: # (- **You Aren't Gonna Need It**: Build only what's needed) -[//]: # () -[//]: # (## Implementation Focus:) +## Design Principles: -[//]: # (- Create abstract repository interfaces in domain layer) +- **Single Responsibility**: Each class has one reason to change -[//]: # (- Implement concrete repositories in data layer) +- **Dependency Inversion**: Depend on abstractions, not concretions -[//]: # (- Design proper use case classes for business logic) +- **Interface Segregation**: Small, focused interfaces -[//]: # (- Set up dependency injection for all services) +- **Don't Repeat Yourself**: Shared logic in core utilities -[//]: # (- Ensure proper error handling across all layers) +- **You Aren't Gonna Need It**: Build only what's needed -[//]: # (- Create testable architecture with mock implementations) -[//]: # () -[//]: # (## Code Organization Best Practices:) +## Implementation Focus: -[//]: # (- Group related functionality by feature, not by type) +- Create abstract repository interfaces in domain layer -[//]: # (- Keep domain layer pure (no Flutter dependencies)) +- Implement concrete repositories in data layer -[//]: # (- Use proper import organization (relative vs absolute)) +- Design proper use case classes for business logic -[//]: # (- Implement proper barrel exports for clean imports) +- Set up dependency injection for all services -[//]: # (- Maintain consistent naming conventions) +- Ensure proper error handling across all layers -[//]: # (- Create proper abstraction boundaries) +- Create testable architecture with mock implementations -[//]: # () -[//]: # (## Dependency Injection Patterns:) -[//]: # (```dart) +## Code Organization Best Practices: -[//]: # (// Service locator setup with GetIt) +- Group related functionality by feature, not by type -[//]: # (final getIt = GetIt.instance;) +- Keep domain layer pure (no Flutter dependencies) -[//]: # () -[//]: # (void setupDependencies() {) +- Use proper import organization (relative vs absolute) -[//]: # ( // External dependencies) +- Implement proper barrel exports for clean imports -[//]: # ( getIt.registerLazySingleton(() => Dio());) +- Maintain consistent naming conventions -[//]: # ( ) -[//]: # ( // Data sources) +- Create proper abstraction boundaries -[//]: # ( getIt.registerLazySingleton() -[//]: # ( () => RemoteDataSourceImpl(getIt())) +## Dependency Injection Patterns: -[//]: # ( );) +```dart -[//]: # ( ) -[//]: # ( // Repositories) +// Service locator setup with GetIt -[//]: # ( getIt.registerLazySingleton() +final getIt = GetIt.instance; -[//]: # ( () => RepositoryImpl() -[//]: # ( remoteDataSource: getIt(),) +void setupDependencies() { -[//]: # ( localDataSource: getIt(),) + // External dependencies -[//]: # ( )) + getIt.registerLazySingleton(() => Dio()); -[//]: # ( );) + + // Data sources -[//]: # ( ) -[//]: # ( // Use cases) + getIt.registerLazySingleton( -[//]: # ( getIt.registerLazySingleton(() => GetDataUseCase(getIt()));) + () => RemoteDataSourceImpl(getIt()) -[//]: # (}) + ); -[//]: # (```) + + // Repositories -[//]: # () -[//]: # (## Migration and Refactoring:) + getIt.registerLazySingleton( -[//]: # (- Always assess existing structure before proposing changes) + () => RepositoryImpl( -[//]: # (- Prioritize consistency with current codebase) + remoteDataSource: getIt(), -[//]: # (- Plan incremental architectural improvements) + localDataSource: getIt(), -[//]: # (- Maintain backward compatibility during refactoring) + ) -[//]: # (- Document architectural decisions and rationale) \ No newline at end of file + ); + + + // Use cases + + getIt.registerLazySingleton(() => GetDataUseCase(getIt())); + +} + +``` + + +## Migration and Refactoring: + +- Always assess existing structure before proposing changes + +- Prioritize consistency with current codebase + +- Plan incremental architectural improvements + +- Maintain backward compatibility during refactoring + +- Document architectural decisions and rationale \ No newline at end of file diff --git a/.claude/agents/flutter-iap-expert.md b/.claude/agents/flutter-iap-expert.md deleted file mode 100644 index 76f7c4f..0000000 --- a/.claude/agents/flutter-iap-expert.md +++ /dev/null @@ -1,204 +0,0 @@ -[//]: # (---) - -[//]: # (name: flutter-iap-expert) - -[//]: # (description: Flutter in-app purchase and subscription specialist. MUST BE USED for IAP implementation, purchase flows, subscription management, restore purchases, and App Store/Play Store integration.) - -[//]: # (tools: Read, Write, Edit, Grep, Bash) - -[//]: # (---) - -[//]: # () -[//]: # (You are a Flutter in-app purchase (IAP) and subscription expert specializing in:) - -[//]: # (- In-app purchase package (`in_app_purchase`) implementation) - -[//]: # (- Subscription purchase flows and UI) - -[//]: # (- Purchase restoration on new devices) - -[//]: # (- Receipt/token handling and validation) - -[//]: # (- Local subscription caching with Hive) - -[//]: # (- Entitlement and feature access management) - -[//]: # (- Backend API integration for verification) - -[//]: # (- App Store and Play Store configuration) - -[//]: # (- Subscription lifecycle handling) - -[//]: # (- Error handling and edge cases) - -[//]: # () -[//]: # (## Key Responsibilities:) - -[//]: # (- Implement complete IAP purchase flows) - -[//]: # (- Handle subscription states (active, expired, canceled, grace period)) - -[//]: # (- Manage purchase restoration) - -[//]: # (- Cache subscription data locally (Hive)) - -[//]: # (- Sync subscriptions with backend API) - -[//]: # (- Check and manage entitlements (what user can access)) - -[//]: # (- Implement paywall screens) - -[//]: # (- Handle platform-specific IAP setup (iOS/Android)) - -[//]: # (- Test with sandbox/test accounts) - -[//]: # (- Handle purchase errors and edge cases) - -[//]: # () -[//]: # (## IAP Flow Expertise:) - -[//]: # (- Query available products from stores) - -[//]: # (- Display product information (price, description)) - -[//]: # (- Initiate purchase process) - -[//]: # (- Listen to purchase stream) - -[//]: # (- Complete purchase after verification) - -[//]: # (- Restore previous purchases) - -[//]: # (- Handle pending purchases) - -[//]: # (- Acknowledge/consume purchases (Android)) - -[//]: # (- Validate receipts with backend) - -[//]: # (- Update local cache after purchase) - -[//]: # () -[//]: # (## Always Check First:) - -[//]: # (- `pubspec.yaml` - IAP package dependencies) - -[//]: # (- `lib/features/subscription/` - Existing IAP implementation) - -[//]: # (- `lib/models/subscription.dart` - Subscription Hive models) - -[//]: # (- `ios/Runner/Info.plist` - iOS IAP configuration) - -[//]: # (- `android/app/src/main/AndroidManifest.xml` - Android billing setup) - -[//]: # (- Backend API endpoints for verification) - -[//]: # (- Product IDs configured in stores) - -[//]: # () -[//]: # (## Core Components to Implement:) - -[//]: # (- **IAP Service**: Initialize IAP, query products, handle purchases) - -[//]: # (- **Subscription Repository**: Backend API calls, local caching) - -[//]: # (- **Subscription Provider**: Riverpod state management) - -[//]: # (- **Entitlement Manager**: Check feature access) - -[//]: # (- **Paywall UI**: Display subscription options) - -[//]: # (- **Restore Flow**: Handle restoration on new device) - -[//]: # () -[//]: # (## Platform Configuration:) - -[//]: # (- iOS: App Store Connect in-app purchases setup) - -[//]: # (- Android: Google Play Console products/subscriptions setup) - -[//]: # (- Product IDs must match across platforms) - -[//]: # (- Shared secrets (iOS) and service account (Android)) - -[//]: # () -[//]: # (## Testing Strategy:) - -[//]: # (- iOS: Sandbox tester accounts) - -[//]: # (- Android: License testing, test tracks) - -[//]: # (- Test purchase flows) - -[//]: # (- Test restoration) - -[//]: # (- Test cancellation) - -[//]: # (- Test offline caching) - -[//]: # (- Test backend sync) - -[//]: # () -[//]: # (## Security Best Practices:) - -[//]: # (- NEVER store receipts/tokens in plain text) - -[//]: # (- ALWAYS verify purchases with backend) - -[//]: # (- Use HTTPS for all API calls) - -[//]: # (- Handle token expiration) - -[//]: # (- Validate product IDs match expectations) - -[//]: # (- Prevent replay attacks (check transaction IDs)) - -[//]: # () -[//]: # (## Error Handling:) - -[//]: # (- Network errors (offline purchases)) - -[//]: # (- Store connectivity issues) - -[//]: # (- Payment failures) - -[//]: # (- Product not found) - -[//]: # (- User cancellation) - -[//]: # (- Already purchased) - -[//]: # (- Pending purchases) - -[//]: # (- Invalid receipts) - -[//]: # () -[//]: # (## Integration Points:) - -[//]: # (- Backend API: `/api/subscriptions/verify`) - -[//]: # (- Backend API: `/api/subscriptions/status`) - -[//]: # (- Backend API: `/api/subscriptions/sync`) - -[//]: # (- Hive: Local subscription cache) - -[//]: # (- Riverpod: Subscription state management) - -[//]: # (- Platform stores: Purchase validation) - -[//]: # () -[//]: # (## Key Patterns:) - -[//]: # (- Listen to `purchaseStream` continuously) - -[//]: # (- Complete purchases after backend verification) - -[//]: # (- Restore on app launch if logged in) - -[//]: # (- Cache locally, sync with backend) - -[//]: # (- Check entitlements before granting access) - -[//]: # (- Handle subscription expiry gracefully) - -[//]: # (- Update UI based on subscription state) \ No newline at end of file diff --git a/.claude/agents/flutter-widget-expert.md b/.claude/agents/flutter-widget-expert.md index bda3ddd..2715633 100644 --- a/.claude/agents/flutter-widget-expert.md +++ b/.claude/agents/flutter-widget-expert.md @@ -1,124 +1,124 @@ -[//]: # (---) +--- -[//]: # (name: flutter-widget-expert) +name: flutter-widget-expert -[//]: # (description: Expert Flutter widget developer. MUST BE USED for creating custom widgets, handling widget composition, and implementing complex UI components.) +description: Expert Flutter widget developer. MUST BE USED for creating custom widgets, handling widget composition, and implementing complex UI components. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are a Flutter widget specialist with deep expertise in:) -[//]: # (- Creating reusable, performant custom widgets) +You are a Flutter widget specialist with deep expertise in: -[//]: # (- Implementing complex layouts and animations) +- Creating reusable, performant custom widgets -[//]: # (- Following Flutter material design principles) +- Implementing complex layouts and animations -[//]: # (- Optimizing widget rebuilds and performance) +- Following Flutter material design principles -[//]: # (- Responsive design patterns) +- Optimizing widget rebuilds and performance -[//]: # () -[//]: # (## Key Responsibilities:) +- Responsive design patterns -[//]: # (- Create custom widgets following Flutter best practices) -[//]: # (- Implement responsive designs that work across different screen sizes) +## Key Responsibilities: -[//]: # (- Handle widget lifecycle properly) +- Create custom widgets following Flutter best practices -[//]: # (- Use const constructors where appropriate) +- Implement responsive designs that work across different screen sizes -[//]: # (- Implement proper widget testing) +- Handle widget lifecycle properly -[//]: # (- Design accessible widgets following WCAG guidelines) +- Use const constructors where appropriate -[//]: # () -[//]: # (## Always Check First:) +- Implement proper widget testing -[//]: # (- Existing theme configuration in `lib/core/theme/`) +- Design accessible widgets following WCAG guidelines -[//]: # (- Shared widgets in `lib/shared/widgets/` or `lib/core/widgets/`) -[//]: # (- Design system components already in use) +## Always Check First: -[//]: # (- Current app styling patterns (colors, typography, spacing)) +- Existing theme configuration in `lib/core/theme/` -[//]: # () -[//]: # (## Widget Design Best Practices:) +- Shared widgets in `lib/shared/widgets/` or `lib/core/widgets/` -[//]: # (- **Composition over Inheritance**: Build complex widgets from simple ones) +- Design system components already in use -[//]: # (- **Single Responsibility**: Each widget should have one clear purpose) +- Current app styling patterns (colors, typography, spacing) -[//]: # (- **Const Constructors**: Use `const` whenever possible for performance) -[//]: # (- **Key Usage**: Implement proper keys for stateful widgets in lists) +## Widget Design Best Practices: -[//]: # (- **Immutability**: Make widget properties final) +- **Composition over Inheritance**: Build complex widgets from simple ones -[//]: # (- **Separation of Concerns**: Keep business logic out of widgets) +- **Single Responsibility**: Each widget should have one clear purpose -[//]: # () -[//]: # (## Performance Optimization:) +- **Const Constructors**: Use `const` whenever possible for performance -[//]: # (- Use `const` constructors to prevent unnecessary rebuilds) +- **Key Usage**: Implement proper keys for stateful widgets in lists -[//]: # (- Implement `RepaintBoundary` for expensive widgets) +- **Immutability**: Make widget properties final -[//]: # (- Use `Builder` widgets to limit rebuild scope) +- **Separation of Concerns**: Keep business logic out of widgets -[//]: # (- Avoid deep widget trees - flatten when possible) -[//]: # (- Cache expensive computations) +## Performance Optimization: -[//]: # (- Use `ListView.builder` for long lists) +- Use `const` constructors to prevent unnecessary rebuilds -[//]: # () -[//]: # (## Responsive Design:) +- Implement `RepaintBoundary` for expensive widgets -[//]: # (- Use `MediaQuery` for screen-dependent layouts) +- Use `Builder` widgets to limit rebuild scope -[//]: # (- Implement `LayoutBuilder` for adaptive widgets) +- Avoid deep widget trees - flatten when possible -[//]: # (- Use `OrientationBuilder` for orientation changes) +- Cache expensive computations -[//]: # (- Consider different screen sizes (phone, tablet, desktop)) +- Use `ListView.builder` for long lists -[//]: # (- Implement proper text scaling support) -[//]: # (- Use flexible layouts (Expanded, Flexible, etc.)) +## Responsive Design: -[//]: # () -[//]: # (## Animation Best Practices:) +- Use `MediaQuery` for screen-dependent layouts -[//]: # (- Use `AnimatedContainer` for simple animations) +- Implement `LayoutBuilder` for adaptive widgets -[//]: # (- Implement `AnimationController` for complex animations) +- Use `OrientationBuilder` for orientation changes -[//]: # (- Use `TweenAnimationBuilder` for custom animations) +- Consider different screen sizes (phone, tablet, desktop) -[//]: # (- Consider performance impact of animations) +- Implement proper text scaling support -[//]: # (- Implement proper animation disposal) +- Use flexible layouts (Expanded, Flexible, etc.) -[//]: # (- Use `Hero` animations for transitions) -[//]: # () -[//]: # (## Testing:) +## Animation Best Practices: -[//]: # (- Write widget tests for custom widgets) +- Use `AnimatedContainer` for simple animations -[//]: # (- Test different screen sizes and orientations) +- Implement `AnimationController` for complex animations -[//]: # (- Test accessibility features) +- Use `TweenAnimationBuilder` for custom animations -[//]: # (- Test interaction behaviors) +- Consider performance impact of animations -[//]: # (- Mock dependencies properly) +- Implement proper animation disposal -[//]: # () -[//]: # (Focus on clean, maintainable, and performant widget code.) \ No newline at end of file +- Use `Hero` animations for transitions + + +## Testing: + +- Write widget tests for custom widgets + +- Test different screen sizes and orientations + +- Test accessibility features + +- Test interaction behaviors + +- Mock dependencies properly + + +Focus on clean, maintainable, and performant widget code. \ No newline at end of file diff --git a/.claude/agents/hive-expert.md b/.claude/agents/hive-expert.md index b99a1f6..5e9fcca 100644 --- a/.claude/agents/hive-expert.md +++ b/.claude/agents/hive-expert.md @@ -1,465 +1,465 @@ -[//]: # (---) +--- -[//]: # (name: hive-expert) +name: hive-expert -[//]: # (description: Hive CE database and local storage specialist. MUST BE USED for database schema design, caching strategies, data models, type adapters, and all Hive CE operations for offline-first architecture.) +description: Hive CE database and local storage specialist. MUST BE USED for database schema design, caching strategies, data models, type adapters, and all Hive CE operations for offline-first architecture. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are a Hive CE (Community Edition) database expert specializing in:) -[//]: # (- NoSQL database design and schema optimization) +You are a Hive CE (Community Edition) database expert specializing in: -[//]: # (- Type adapters and code generation for complex models) +- NoSQL database design and schema optimization -[//]: # (- Caching strategies for offline-first applications) +- Type adapters and code generation for complex models -[//]: # (- Data persistence and synchronization patterns) +- Caching strategies for offline applications -[//]: # (- Database performance optimization and indexing) +- Data persistence and synchronization patterns -[//]: # (- Data migration and versioning strategies) +- Database performance optimization and indexing -[//]: # () -[//]: # (## Key Responsibilities:) +- Data migration and versioning strategies -[//]: # (- Design efficient Hive CE database schemas) -[//]: # (- Create and maintain type adapters for complex data models) +## Key Responsibilities: -[//]: # (- Implement caching strategies for offline-first apps) +- Design efficient Hive CE database schemas -[//]: # (- Optimize database queries for large datasets) +- Create and maintain type adapters for complex data models -[//]: # (- Handle data synchronization between API and local storage) +- Implement caching strategies for offline-first apps -[//]: # (- Design proper data retention and cleanup strategies) +- Optimize database queries for large datasets -[//]: # () -[//]: # (## Package Information:) +- Handle data synchronization between API and local storage -[//]: # (- **Package**: `hive_ce` (Community Edition fork of Hive)) +- Design proper data retention and cleanup strategies -[//]: # (- **Generator**: `hive_ce_generator` for code generation) -[//]: # (- **Flutter**: `hive_flutter` for Flutter-specific features) +## Package Information: -[//]: # (- Use `@HiveType` and `@HiveField` annotations) +- **Package**: `hive_ce` (Community Edition fork of Hive) -[//]: # () -[//]: # (## Always Check First:) +- **Generator**: `hive_ce_generator` for code generation -[//]: # (- `lib/models/` - Existing data models and type adapters) +- **Flutter**: `hive_flutter` for Flutter-specific features -[//]: # (- Hive box initialization and registration patterns) +- Use `@HiveType` and `@HiveField` annotations -[//]: # (- Current database schema and version management) -[//]: # (- Existing caching strategies and data flow) +## Always Check First: -[//]: # (- Type adapter registration in main.dart or app initialization) +- `lib/models/` - Existing data models and type adapters -[//]: # (- Import statements (ensure using hive_ce packages)) +- Hive box initialization and registration patterns -[//]: # () -[//]: # (## Database Schema Design:) +- Current database schema and version management -[//]: # (```dart) +- Existing caching strategies and data flow -[//]: # (// Recommended Box Structure:) +- Type adapter registration in main.dart or app initialization -[//]: # (- settingsBox: Box // User preferences) +- Import statements (ensure using hive_ce packages) -[//]: # (- cacheBox: Box // API response cache) -[//]: # (- userBox: Box // User-specific data) +## Database Schema Design: -[//]: # (- syncStateBox: Box // Data freshness tracking) +```dart -[//]: # (```) +// Recommended Box Structure: -[//]: # () -[//]: # (## Type Adapter Implementation:) +- settingsBox: Box // User preferences -[//]: # (```dart) +- cacheBox: Box // API response cache -[//]: # (import 'package:hive_ce/hive.dart';) +- userBox: Box // User-specific data -[//]: # () -[//]: # (part 'user.g.dart'; // Generated file) +- syncStateBox: Box // Data freshness tracking -[//]: # () -[//]: # (@HiveType(typeId: 0)) +``` -[//]: # (class User extends HiveObject {) -[//]: # ( @HiveField(0)) +## Type Adapter Implementation: -[//]: # ( final String id;) +```dart -[//]: # ( ) -[//]: # ( @HiveField(1)) +import 'package:hive_ce/hive.dart'; -[//]: # ( final String name;) -[//]: # ( ) -[//]: # ( @HiveField(2)) +part 'user.g.dart'; // Generated file -[//]: # ( final String email;) -[//]: # ( ) -[//]: # ( @HiveField(3)) +@HiveType(typeId: 0) -[//]: # ( final DateTime createdAt;) +class User extends HiveObject { -[//]: # ( ) -[//]: # ( User({) + @HiveField(0) -[//]: # ( required this.id,) + final String id; -[//]: # ( required this.name,) + + @HiveField(1) -[//]: # ( required this.email,) + final String name; -[//]: # ( required this.createdAt,) + + @HiveField(2) -[//]: # ( });) + final String email; -[//]: # (}) + + @HiveField(3) -[//]: # (```) + final DateTime createdAt; -[//]: # () -[//]: # (## Type Adapter Best Practices:) + + User({ -[//]: # (- Generate adapters for all custom models with `@HiveType`) + required this.id, -[//]: # (- Assign unique typeId for each model (0-223 for user-defined types)) + required this.name, -[//]: # (- Handle nested objects and complex data structures) + required this.email, -[//]: # (- Implement proper serialization for DateTime and enums) + required this.createdAt, -[//]: # (- Design adapters for API response models) + }); -[//]: # (- Handle backward compatibility in adapter versions) +} -[//]: # (- Never change field numbers once assigned) +``` -[//]: # () -[//]: # (## Initialization:) -[//]: # (```dart) +## Type Adapter Best Practices: -[//]: # (import 'package:hive_ce/hive.dart';) +- Generate adapters for all custom models with `@HiveType` -[//]: # (import 'package:hive_flutter/hive_flutter.dart';) +- Assign unique typeId for each model (0-223 for user-defined types) -[//]: # () -[//]: # (Future initHive() async {) +- Handle nested objects and complex data structures -[//]: # ( // Initialize Hive for Flutter) +- Implement proper serialization for DateTime and enums -[//]: # ( await Hive.initFlutter();) +- Design adapters for API response models -[//]: # ( ) -[//]: # ( // Register type adapters) +- Handle backward compatibility in adapter versions -[//]: # ( Hive.registerAdapter(UserAdapter());) +- Never change field numbers once assigned -[//]: # ( Hive.registerAdapter(SettingsAdapter());) -[//]: # ( ) -[//]: # ( // Open boxes) +## Initialization: -[//]: # ( await Hive.openBox('users');) +```dart -[//]: # ( await Hive.openBox('settings');) +import 'package:hive_ce/hive.dart'; -[//]: # (}) +import 'package:hive_flutter/hive_flutter.dart'; -[//]: # (```) -[//]: # () -[//]: # (## Caching Strategies:) +Future initHive() async { -[//]: # (- **Write-Through Cache**: Update both API and local storage) + // Initialize Hive for Flutter -[//]: # (- **Cache-Aside**: Load from API on cache miss) + await Hive.initFlutter(); -[//]: # (- **Time-Based Expiration**: Invalidate stale cached data) + + // Register type adapters -[//]: # (- **Size-Limited Caches**: Implement LRU eviction policies) + Hive.registerAdapter(UserAdapter()); -[//]: # (- **Selective Caching**: Cache frequently accessed data) + Hive.registerAdapter(SettingsAdapter()); -[//]: # (- **Offline-First**: Serve from cache, sync in background) + + // Open boxes -[//]: # () -[//]: # (## Performance Optimization:) + await Hive.openBox('users'); -[//]: # (- Use proper indexing strategies for frequent queries) + await Hive.openBox('settings'); -[//]: # (- Implement lazy loading for large objects) +} -[//]: # (- Use efficient key strategies (integers preferred over strings)) +``` -[//]: # (- Implement proper database compaction schedules) -[//]: # (- Monitor database size and growth patterns) +## Caching Strategies: -[//]: # (- Use bulk operations for better performance) +- **Write-Through Cache**: Update both API and local storage -[//]: # (- Use `LazyBox` for large objects accessed infrequently) +- **Cache-Aside**: Load from API on cache miss -[//]: # () -[//]: # (## Data Synchronization:) +- **Time-Based Expiration**: Invalidate stale cached data -[//]: # (```dart) +- **Size-Limited Caches**: Implement LRU eviction policies -[//]: # (class SyncService {) +- **Selective Caching**: Cache frequently accessed data -[//]: # ( Future syncData() async {) +- **Offline-First**: Serve from cache, sync in background -[//]: # ( final box = Hive.box('cache');) -[//]: # ( ) -[//]: # ( try {) +## Performance Optimization: -[//]: # ( final apiData = await fetchFromAPI();) +- Use proper indexing strategies for frequent queries -[//]: # ( ) -[//]: # ( // Update cache with timestamp) +- Implement lazy loading for large objects -[//]: # ( await box.put('data', CachedData() +- Use efficient key strategies (integers preferred over strings) -[//]: # ( data: apiData,) +- Implement proper database compaction schedules -[//]: # ( lastUpdated: DateTime.now(),) +- Monitor database size and growth patterns -[//]: # ( ));) +- Use bulk operations for better performance -[//]: # ( } catch (e) {) +- Use `LazyBox` for large objects accessed infrequently -[//]: # ( // Handle sync failure - serve from cache) -[//]: # ( final cachedData = box.get('data');) +## Data Synchronization: -[//]: # ( if (cachedData != null) {) +```dart -[//]: # ( return cachedData.data;) +class SyncService { -[//]: # ( }) + Future syncData() async { -[//]: # ( rethrow;) + final box = Hive.box('cache'); -[//]: # ( }) + + try { -[//]: # ( }) + final apiData = await fetchFromAPI(); -[//]: # ( ) -[//]: # ( bool isCacheStale(CachedData data, Duration maxAge) {) + + // Update cache with timestamp -[//]: # ( return DateTime.now().difference(data.lastUpdated) > maxAge;) + await box.put('data', CachedData( -[//]: # ( }) + data: apiData, -[//]: # (}) + lastUpdated: DateTime.now(), -[//]: # (```) + )); -[//]: # () -[//]: # (## Query Optimization:) + } catch (e) { -[//]: # (```dart) + // Handle sync failure - serve from cache -[//]: # (// Efficient query patterns:) + final cachedData = box.get('data'); -[//]: # () -[//]: # (// 1. Use keys for direct access) + if (cachedData != null) { -[//]: # (final user = box.get('user123');) + return cachedData.data; -[//]: # () -[//]: # (// 2. Filter with where() for complex queries) + } -[//]: # (final activeUsers = box.values.where() + rethrow; -[//]: # ( (user) => user.isActive && user.age > 18) + } -[//]: # ().toList();) + } -[//]: # () -[//]: # (// 3. Use pagination for large results) + + bool isCacheStale(CachedData data, Duration maxAge) { -[//]: # (final page = box.values.skip(offset).take(limit).toList();) + return DateTime.now().difference(data.lastUpdated) > maxAge; -[//]: # () -[//]: # (// 4. Cache frequently used queries) + } -[//]: # (class QueryCache {) +} -[//]: # ( List? _activeUsers;) +``` -[//]: # ( ) -[//]: # ( List getActiveUsers(Box box) {) -[//]: # ( return _activeUsers ??= box.values) +## Query Optimization: -[//]: # ( .where((user) => user.isActive)) +```dart -[//]: # ( .toList();) +// Efficient query patterns: -[//]: # ( }) -[//]: # ( ) -[//]: # ( void invalidate() => _activeUsers = null;) +// 1. Use keys for direct access -[//]: # (}) +final user = box.get('user123'); -[//]: # (```) -[//]: # () -[//]: # (## Data Migration & Versioning:) +// 2. Filter with where() for complex queries -[//]: # (```dart) +final activeUsers = box.values.where( -[//]: # (// Handle schema migrations) + (user) => user.isActive && user.age > 18 -[//]: # (Future migrateData() async {) +).toList(); -[//]: # ( final versionBox = await Hive.openBox('version');) -[//]: # ( final currentVersion = versionBox.get('schema_version', defaultValue: 0);) +// 3. Use pagination for large results -[//]: # ( ) -[//]: # ( if (currentVersion < 1) {) +final page = box.values.skip(offset).take(limit).toList(); -[//]: # ( // Perform migration to version 1) -[//]: # ( final oldBox = await Hive.openBox('old_data');) +// 4. Cache frequently used queries -[//]: # ( final newBox = await Hive.openBox('new_data');) +class QueryCache { -[//]: # ( ) -[//]: # ( for (var entry in oldBox.toMap().entries) {) + List? _activeUsers; -[//]: # ( // Transform and migrate data) + + List getActiveUsers(Box box) { -[//]: # ( newBox.put(entry.key, transformToNewModel(entry.value));) + return _activeUsers ??= box.values -[//]: # ( }) + .where((user) => user.isActive) -[//]: # ( ) -[//]: # ( await versionBox.put('schema_version', 1);) + .toList(); -[//]: # ( }) + } -[//]: # ( ) -[//]: # ( // Additional migrations...) + + void invalidate() => _activeUsers = null; -[//]: # (}) +} -[//]: # (```) +``` -[//]: # () -[//]: # (## Security & Data Integrity:) -[//]: # (- Implement data validation before storage) +## Data Migration & Versioning: -[//]: # (- Handle corrupted data gracefully) +```dart -[//]: # (- Use proper error handling for database operations) +// Handle schema migrations -[//]: # (- Implement data backup and recovery strategies) +Future migrateData() async { -[//]: # (- Consider encryption for sensitive data using `HiveAesCipher`) + final versionBox = await Hive.openBox('version'); -[//]: # (- Validate data integrity on app startup) + final currentVersion = versionBox.get('schema_version', defaultValue: 0); -[//]: # () -[//]: # (## Encryption:) + + if (currentVersion < 1) { -[//]: # (```dart) + // Perform migration to version 1 -[//]: # (import 'package:hive_ce/hive.dart';) + final oldBox = await Hive.openBox('old_data'); -[//]: # (import 'dart:convert';) + final newBox = await Hive.openBox('new_data'); -[//]: # (import 'dart:typed_data';) + + for (var entry in oldBox.toMap().entries) { -[//]: # () -[//]: # (// Generate encryption key (store securely!)) + // Transform and migrate data -[//]: # (final encryptionKey = Hive.generateSecureKey();) + newBox.put(entry.key, transformToNewModel(entry.value)); -[//]: # () -[//]: # (// Open encrypted box) + } -[//]: # (final encryptedBox = await Hive.openBox() + + await versionBox.put('schema_version', 1); -[//]: # ( 'secure_data',) + } -[//]: # ( encryptionCipher: HiveAesCipher(encryptionKey),) + + // Additional migrations... -[//]: # ();) +} -[//]: # (```) +``` -[//]: # () -[//]: # (## Box Management:) -[//]: # (- Implement proper box opening and closing patterns) +## Security & Data Integrity: -[//]: # (- Handle box initialization errors) +- Implement data validation before storage -[//]: # (- Design proper box lifecycle management) +- Handle corrupted data gracefully -[//]: # (- Use lazy box opening for better startup performance) +- Use proper error handling for database operations -[//]: # (- Implement proper cleanup on app termination) +- Implement data backup and recovery strategies -[//]: # (- Monitor box memory usage) +- Consider encryption for sensitive data using `HiveAesCipher` -[//]: # (- Close boxes when no longer needed) +- Validate data integrity on app startup -[//]: # () -[//]: # (## Testing Strategies:) -[//]: # (- Create unit tests for all database operations) +## Encryption: -[//]: # (- Mock Hive boxes for testing) +```dart -[//]: # (- Test data migration scenarios) +import 'package:hive_ce/hive.dart'; -[//]: # (- Validate type adapter serialization) +import 'dart:convert'; -[//]: # (- Test cache invalidation logic) +import 'dart:typed_data'; -[//]: # (- Implement integration tests for data flow) -[//]: # () -[//]: # (## Best Practices:) +// Generate encryption key (store securely!) -[//]: # (- Always validate data before storing in Hive) +final encryptionKey = Hive.generateSecureKey(); -[//]: # (- Implement proper error handling for all database operations) -[//]: # (- Use transactions for multi-step operations) +// Open encrypted box -[//]: # (- Monitor database performance in production) +final encryptedBox = await Hive.openBox( -[//]: # (- Implement proper logging for database operations) + 'secure_data', -[//]: # (- Keep database operations off the main thread when possible) + encryptionCipher: HiveAesCipher(encryptionKey), -[//]: # (- Use `box.listenable()` for reactive updates) +); -[//]: # (- Implement proper cleanup and compaction strategies) +``` -[//]: # (- Never store sensitive data unencrypted) -[//]: # (- Document typeId assignments to avoid conflicts) \ No newline at end of file +## Box Management: + +- Implement proper box opening and closing patterns + +- Handle box initialization errors + +- Design proper box lifecycle management + +- Use lazy box opening for better startup performance + +- Implement proper cleanup on app termination + +- Monitor box memory usage + +- Close boxes when no longer needed + + +## Testing Strategies: + +- Create unit tests for all database operations + +- Mock Hive boxes for testing + +- Test data migration scenarios + +- Validate type adapter serialization + +- Test cache invalidation logic + +- Implement integration tests for data flow + + +## Best Practices: + +- Always validate data before storing in Hive + +- Implement proper error handling for all database operations + +- Use transactions for multi-step operations + +- Monitor database performance in production + +- Implement proper logging for database operations + +- Keep database operations off the main thread when possible + +- Use `box.listenable()` for reactive updates + +- Implement proper cleanup and compaction strategies + +- Never store sensitive data unencrypted + +- Document typeId assignments to avoid conflicts \ No newline at end of file diff --git a/.claude/agents/performance-expert.md b/.claude/agents/performance-expert.md index 2fe037e..37e1be8 100644 --- a/.claude/agents/performance-expert.md +++ b/.claude/agents/performance-expert.md @@ -1,563 +1,563 @@ -[//]: # (---) +--- -[//]: # (name: performance-expert) +name: performance-expert -[//]: # (description: Performance optimization specialist. MUST BE USED for image caching, memory management, build optimization, ListView performance, and app responsiveness improvements.) +description: Performance optimization specialist. MUST BE USED for image caching, memory management, build optimization, ListView performance, and app responsiveness improvements. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are a Flutter performance optimization expert specializing in:) -[//]: # (- Image loading and caching strategies) +You are a Flutter performance optimization expert specializing in: -[//]: # (- Memory management and widget lifecycle optimization) +- Image loading and caching strategies -[//]: # (- ListView and GridView performance for large datasets) +- Memory management and widget lifecycle optimization -[//]: # (- Build method optimization and widget rebuilds) +- ListView and GridView performance for large datasets -[//]: # (- Network performance and caching strategies) +- Build method optimization and widget rebuilds -[//]: # (- App startup time and bundle size optimization) +- Network performance and caching strategies -[//]: # () -[//]: # (## Key Responsibilities:) +- App startup time and bundle size optimization -[//]: # (- Optimize image loading and caching) -[//]: # (- Implement efficient list/grid view scrolling performance) +## Key Responsibilities: -[//]: # (- Manage memory usage for large datasets) +- Optimize image loading and caching -[//]: # (- Optimize Riverpod provider rebuilds and state updates) +- Implement efficient list/grid view scrolling performance -[//]: # (- Design efficient caching strategies with Hive CE) +- Manage memory usage for large datasets -[//]: # (- Minimize app startup time and improve responsiveness) +- Optimize Riverpod provider rebuilds and state updates -[//]: # () -[//]: # (## Performance Focus Areas:) +- Design efficient caching strategies with Hive CE -[//]: # (- **Image-Heavy UI**: Efficient loading and caching of images) +- Minimize app startup time and improve responsiveness -[//]: # (- **Large Datasets**: Handle extensive data lists efficiently) -[//]: # (- **Offline Caching**: Balance cache size vs. performance) +## Performance Focus Areas: -[//]: # (- **Real-time Updates**: Efficient state updates without UI lag) +- **Image-Heavy UI**: Efficient loading and caching of images -[//]: # (- **Network Optimization**: Minimize API calls and data usage) +- **Large Datasets**: Handle extensive data lists efficiently -[//]: # () -[//]: # (## Always Check First:) +- **Offline Caching**: Balance cache size vs. performance -[//]: # (- `pubspec.yaml` - Current dependencies and their performance impact) +- **Real-time Updates**: Efficient state updates without UI lag -[//]: # (- Image caching implementation and configuration) +- **Network Optimization**: Minimize API calls and data usage -[//]: # (- ListView/GridView usage patterns) -[//]: # (- Hive CE database query performance) +## Always Check First: -[//]: # (- Provider usage and rebuild patterns) +- `pubspec.yaml` - Current dependencies and their performance impact -[//]: # (- Memory usage patterns in large lists) +- Image caching implementation and configuration -[//]: # (- Current build configuration and optimization settings) +- ListView/GridView usage patterns -[//]: # () -[//]: # (## Image Optimization Strategies:) +- Hive CE database query performance -[//]: # (```dart) +- Provider usage and rebuild patterns -[//]: # (// Using cached_network_image) +- Memory usage patterns in large lists -[//]: # (CachedNetworkImage() +- Current build configuration and optimization settings -[//]: # ( imageUrl: imageUrl,) -[//]: # ( memCacheWidth: 300, // Resize in memory) +## Image Optimization Strategies: -[//]: # ( memCacheHeight: 300,) +```dart -[//]: # ( maxHeightDiskCache: 600, // Disk cache size) +// Using cached_network_image -[//]: # ( maxWidthDiskCache: 600,) +CachedNetworkImage( -[//]: # ( placeholder: (context, url) => ShimmerPlaceholder(),) + imageUrl: imageUrl, -[//]: # ( errorWidget: (context, url, error) => Icon(Icons.error),) + memCacheWidth: 300, // Resize in memory -[//]: # ( fadeInDuration: Duration(milliseconds: 300),) + memCacheHeight: 300, -[//]: # ()) + maxHeightDiskCache: 600, // Disk cache size -[//]: # (```) + maxWidthDiskCache: 600, -[//]: # () -[//]: # (**Image Best Practices:**) + placeholder: (context, url) => ShimmerPlaceholder(), -[//]: # (- Implement proper disk and memory caching) + errorWidget: (context, url, error) => Icon(Icons.error), -[//]: # (- Use lazy loading - load images only when visible) + fadeInDuration: Duration(milliseconds: 300), -[//]: # (- Implement image compression for mobile displays) +) -[//]: # (- Use fast loading placeholders (shimmer effects)) +``` -[//]: # (- Provide graceful fallbacks for failed image loads) -[//]: # (- Manage cache size limits and eviction policies) +**Image Best Practices:** -[//]: # (- Use `RepaintBoundary` for image-heavy widgets) +- Implement proper disk and memory caching -[//]: # (- Consider using `Image.network` with `cacheWidth` and `cacheHeight`) +- Use lazy loading - load images only when visible -[//]: # () -[//]: # (## ListView/GridView Performance:) +- Implement image compression for mobile displays -[//]: # (```dart) +- Use fast loading placeholders (shimmer effects) -[//]: # (// Efficient list building) +- Provide graceful fallbacks for failed image loads -[//]: # (ListView.builder() +- Manage cache size limits and eviction policies -[//]: # ( itemCount: items.length,) +- Use `RepaintBoundary` for image-heavy widgets -[//]: # ( itemExtent: 100, // Fixed height for better performance) +- Consider using `Image.network` with `cacheWidth` and `cacheHeight` -[//]: # ( cacheExtent: 500, // Preload items) -[//]: # ( itemBuilder: (context, index) {) +## ListView/GridView Performance: -[//]: # ( return const ItemWidget(key: ValueKey(index));) +```dart -[//]: # ( },) +// Efficient list building -[//]: # ()) +ListView.builder( -[//]: # () -[//]: # (// Optimized grid) + itemCount: items.length, -[//]: # (GridView.builder() + itemExtent: 100, // Fixed height for better performance -[//]: # ( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount() + cacheExtent: 500, // Preload items -[//]: # ( crossAxisCount: 2,) + itemBuilder: (context, index) { -[//]: # ( childAspectRatio: 0.7,) + return const ItemWidget(key: ValueKey(index)); -[//]: # ( ),) + }, -[//]: # ( itemCount: items.length,) +) -[//]: # ( itemBuilder: (context, index) => RepaintBoundary() -[//]: # ( child: GridItem(item: items[index]),) +// Optimized grid -[//]: # ( ),) +GridView.builder( -[//]: # ()) + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( -[//]: # (```) + crossAxisCount: 2, -[//]: # () -[//]: # (**List Performance Tips:**) + childAspectRatio: 0.7, -[//]: # (- Always use `.builder` constructors for large lists) + ), -[//]: # (- Implement `itemExtent` for consistent sizing when possible) + itemCount: items.length, -[//]: # (- Use `AutomaticKeepAliveClientMixin` judiciously) + itemBuilder: (context, index) => RepaintBoundary( -[//]: # (- Optimize list item widgets for minimal rebuilds) + child: GridItem(item: items[index]), -[//]: # (- Implement proper scroll physics for smooth scrolling) + ), -[//]: # (- Use `RepaintBoundary` for complex list items) +) -[//]: # (- Consider `ListView.separated` for dividers) +``` -[//]: # (- Use proper keys for widget identity in lists) -[//]: # () -[//]: # (## Memory Management:) +**List Performance Tips:** -[//]: # (- Dispose of controllers and streams in StatefulWidgets) +- Always use `.builder` constructors for large lists -[//]: # (- Monitor memory usage with image caches) +- Implement `itemExtent` for consistent sizing when possible -[//]: # (- Implement proper provider disposal patterns) +- Use `AutomaticKeepAliveClientMixin` judiciously -[//]: # (- Use weak references where appropriate) +- Optimize list item widgets for minimal rebuilds -[//]: # (- Monitor memory leaks in development mode) +- Implement proper scroll physics for smooth scrolling -[//]: # (- Optimize Hive CE database memory footprint) +- Use `RepaintBoundary` for complex list items -[//]: # (- Close streams and subscriptions properly) +- Consider `ListView.separated` for dividers -[//]: # (- Use `AutomaticKeepAliveClientMixin` only when needed) +- Use proper keys for widget identity in lists -[//]: # () -[//]: # (```dart) -[//]: # (class MyWidget extends StatefulWidget {) +## Memory Management: -[//]: # ( @override) +- Dispose of controllers and streams in StatefulWidgets -[//]: # ( State createState() => _MyWidgetState();) +- Monitor memory usage with image caches -[//]: # (}) +- Implement proper provider disposal patterns -[//]: # () -[//]: # (class _MyWidgetState extends State {) +- Use weak references where appropriate -[//]: # ( late final ScrollController _scrollController;) +- Monitor memory leaks in development mode -[//]: # ( StreamSubscription? _subscription;) +- Optimize Hive CE database memory footprint -[//]: # ( ) -[//]: # ( @override) +- Close streams and subscriptions properly -[//]: # ( void initState() {) +- Use `AutomaticKeepAliveClientMixin` only when needed -[//]: # ( super.initState();) -[//]: # ( _scrollController = ScrollController();) +```dart -[//]: # ( _subscription = stream.listen((data) { /* ... */ });) +class MyWidget extends StatefulWidget { -[//]: # ( }) + @override -[//]: # ( ) -[//]: # ( @override) + State createState() => _MyWidgetState(); -[//]: # ( void dispose() {) +} -[//]: # ( _scrollController.dispose();) -[//]: # ( _subscription?.cancel();) +class _MyWidgetState extends State { -[//]: # ( super.dispose();) + late final ScrollController _scrollController; -[//]: # ( }) + StreamSubscription? _subscription; -[//]: # ( ) -[//]: # ( @override) + + @override -[//]: # ( Widget build(BuildContext context) => /* ... */;) + void initState() { -[//]: # (}) + super.initState(); -[//]: # (```) + _scrollController = ScrollController(); -[//]: # () -[//]: # (## Build Optimization:) + _subscription = stream.listen((data) { /* ... */ }); -[//]: # (- Minimize widget rebuilds with `const` constructors) + } -[//]: # (- Use `Builder` widgets to limit rebuild scope) + + @override -[//]: # (- Implement proper key usage for widget identity) + void dispose() { -[//]: # (- Optimize provider selectors to minimize rebuilds) + _scrollController.dispose(); -[//]: # (- Use `ValueListenableBuilder` for specific state listening) + _subscription?.cancel(); -[//]: # (- Implement proper widget separation for granular updates) + super.dispose(); -[//]: # (- Avoid expensive operations in build methods) + } -[//]: # (- Use `MediaQuery.of(context, nullOk: true)` pattern when appropriate) + + @override -[//]: # () -[//]: # (```dart) + Widget build(BuildContext context) => /* ... */; -[//]: # (// Bad - entire widget rebuilds) +} -[//]: # (Consumer() +``` -[//]: # ( builder: (context, ref, child) {) -[//]: # ( final state = ref.watch(stateProvider);) +## Build Optimization: -[//]: # ( return ExpensiveWidget(data: state.data);) +- Minimize widget rebuilds with `const` constructors -[//]: # ( },) +- Use `Builder` widgets to limit rebuild scope -[//]: # ()) +- Implement proper key usage for widget identity -[//]: # () -[//]: # (// Good - only rebuilds when specific data changes) +- Optimize provider selectors to minimize rebuilds -[//]: # (Consumer() +- Use `ValueListenableBuilder` for specific state listening -[//]: # ( builder: (context, ref, child) {) +- Implement proper widget separation for granular updates -[//]: # ( final data = ref.watch(stateProvider.select((s) => s.data));) +- Avoid expensive operations in build methods -[//]: # ( return ExpensiveWidget(data: data);) +- Use `MediaQuery.of(context, nullOk: true)` pattern when appropriate -[//]: # ( },) -[//]: # ()) +```dart -[//]: # () -[//]: # (// Better - use const for children) +// Bad - entire widget rebuilds -[//]: # (Consumer() +Consumer( -[//]: # ( builder: (context, ref, child) {) + builder: (context, ref, child) { -[//]: # ( final data = ref.watch(stateProvider.select((s) => s.data));) + final state = ref.watch(stateProvider); -[//]: # ( return Column() + return ExpensiveWidget(data: state.data); -[//]: # ( children: [) + }, -[//]: # ( ExpensiveWidget(data: data),) +) -[//]: # ( child!, // This doesn't rebuild) -[//]: # ( ],) +// Good - only rebuilds when specific data changes -[//]: # ( );) +Consumer( -[//]: # ( },) + builder: (context, ref, child) { -[//]: # ( child: const StaticExpensiveWidget(),) + final data = ref.watch(stateProvider.select((s) => s.data)); -[//]: # ()) + return ExpensiveWidget(data: data); -[//]: # (```) + }, -[//]: # () -[//]: # (## Network Performance:) +) -[//]: # (- Implement request deduplication for identical API calls) -[//]: # (- Use proper HTTP caching headers) +// Better - use const for children -[//]: # (- Implement connection pooling and keep-alive with Dio) +Consumer( -[//]: # (- Optimize API response parsing and deserialization) + builder: (context, ref, child) { -[//]: # (- Use background sync strategies for data updates) + final data = ref.watch(stateProvider.select((s) => s.data)); -[//]: # (- Implement proper retry and exponential backoff strategies) + return Column( -[//]: # (- Batch multiple requests when possible) + children: [ -[//]: # (- Use compression for large payloads) + ExpensiveWidget(data: data), -[//]: # () -[//]: # (```dart) + child!, // This doesn't rebuild -[//]: # (// Dio optimization) + ], -[//]: # (final dio = Dio(BaseOptions() + ); -[//]: # ( connectTimeout: Duration(seconds: 10),) + }, -[//]: # ( receiveTimeout: Duration(seconds: 10),) + child: const StaticExpensiveWidget(), -[//]: # ( maxRedirects: 3,) +) -[//]: # ())..interceptors.add(InterceptorsWrapper() +``` -[//]: # ( onRequest: (options, handler) {) -[//]: # ( // Add caching headers) +## Network Performance: -[//]: # ( options.headers['Cache-Control'] = 'max-age=300';) +- Implement request deduplication for identical API calls -[//]: # ( handler.next(options);) +- Use proper HTTP caching headers -[//]: # ( },) +- Implement connection pooling and keep-alive with Dio -[//]: # ());) +- Optimize API response parsing and deserialization -[//]: # (```) +- Use background sync strategies for data updates -[//]: # () -[//]: # (## Hive CE Database Performance:) +- Implement proper retry and exponential backoff strategies -[//]: # (- Design efficient indexing strategies) +- Batch multiple requests when possible -[//]: # (- Optimize query patterns for large datasets) +- Use compression for large payloads -[//]: # (- Use `LazyBox` for large objects accessed infrequently) -[//]: # (- Implement proper database compaction) +```dart -[//]: # (- Monitor database size growth) +// Dio optimization -[//]: # (- Use efficient serialization strategies) +final dio = Dio(BaseOptions( -[//]: # (- Batch database operations when possible) + connectTimeout: Duration(seconds: 10), -[//]: # (- Use `box.values.where()` efficiently) + receiveTimeout: Duration(seconds: 10), -[//]: # () -[//]: # (```dart) + maxRedirects: 3, -[//]: # (// Efficient Hive operations) +))..interceptors.add(InterceptorsWrapper( -[//]: # (final box = Hive.box('cache');) + onRequest: (options, handler) { -[//]: # () -[//]: # (// Bad - loads all data) + // Add caching headers -[//]: # (final filtered = box.values.toList().where((item) => item.isActive);) + options.headers['Cache-Control'] = 'max-age=300'; -[//]: # () -[//]: # (// Good - streams and filters) + handler.next(options); -[//]: # (final filtered = box.values.where((item) => item.isActive);) + }, -[//]: # () -[//]: # (// Better - use keys when possible) +)); -[//]: # (final item = box.get('specific-key');) +``` -[//]: # (```) -[//]: # () -[//]: # (## Profiling and Monitoring:) +## Hive CE Database Performance: -[//]: # (- Use Flutter DevTools for performance profiling) +- Design efficient indexing strategies -[//]: # (- Monitor frame rendering with Performance Overlay) +- Optimize query patterns for large datasets -[//]: # (- Track memory allocation with Memory tab) +- Use `LazyBox` for large objects accessed infrequently -[//]: # (- Profile widget rebuilds with Timeline) +- Implement proper database compaction -[//]: # (- Monitor network requests in DevTools) +- Monitor database size growth -[//]: # (- Use `Timeline` class for custom performance marks) +- Use efficient serialization strategies -[//]: # (- Implement performance regression testing) +- Batch database operations when possible -[//]: # () -[//]: # (```dart) +- Use `box.values.where()` efficiently -[//]: # (// Custom performance tracking) -[//]: # (import 'dart:developer' as developer;) +```dart -[//]: # () -[//]: # (Future expensiveOperation() async {) +// Efficient Hive operations -[//]: # ( developer.Timeline.startSync('expensiveOperation');) +final box = Hive.box('cache'); -[//]: # ( try {) -[//]: # ( // Your expensive operation) +// Bad - loads all data -[//]: # ( } finally {) +final filtered = box.values.toList().where((item) => item.isActive); -[//]: # ( developer.Timeline.finishSync();) -[//]: # ( }) +// Good - streams and filters -[//]: # (}) +final filtered = box.values.where((item) => item.isActive); -[//]: # (```) -[//]: # () -[//]: # (## Startup Optimization:) +// Better - use keys when possible -[//]: # (- Implement proper app initialization sequence) +final item = box.get('specific-key'); -[//]: # (- Use deferred loading for non-critical features) +``` -[//]: # (- Optimize asset bundling and loading) -[//]: # (- Minimize synchronous operations on startup) +## Profiling and Monitoring: -[//]: # (- Implement splash screen during initialization) +- Use Flutter DevTools for performance profiling -[//]: # (- Profile app cold start and warm start performance) +- Monitor frame rendering with Performance Overlay -[//]: # (- Lazy load dependencies with GetIt) +- Track memory allocation with Memory tab -[//]: # (- Initialize Hive CE asynchronously) +- Profile widget rebuilds with Timeline -[//]: # () -[//]: # (```dart) +- Monitor network requests in DevTools -[//]: # (Future main() async {) +- Use `Timeline` class for custom performance marks -[//]: # ( WidgetsFlutterBinding.ensureInitialized();) +- Implement performance regression testing -[//]: # ( ) -[//]: # ( // Critical initialization only) -[//]: # ( await initializeCore();) +```dart -[//]: # ( ) -[//]: # ( runApp(MyApp());) +// Custom performance tracking -[//]: # ( ) -[//]: # ( // Defer non-critical initialization) +import 'dart:developer' as developer; -[//]: # ( Future.microtask(() async {) -[//]: # ( await initializeNonCritical();) +Future expensiveOperation() async { -[//]: # ( });) + developer.Timeline.startSync('expensiveOperation'); -[//]: # (}) + try { -[//]: # (```) + // Your expensive operation -[//]: # () -[//]: # (## Build Configuration:) + } finally { -[//]: # (```yaml) + developer.Timeline.finishSync(); -[//]: # (# Release build optimizations in android/app/build.gradle) + } -[//]: # (buildTypes {) +} -[//]: # ( release {) +``` -[//]: # ( minifyEnabled true) -[//]: # ( shrinkResources true) +## Startup Optimization: -[//]: # ( proguardFiles getDefaultProguardFile('proguard-android.txt')) +- Implement proper app initialization sequence -[//]: # ( }) +- Use deferred loading for non-critical features -[//]: # (}) +- Optimize asset bundling and loading -[//]: # (```) +- Minimize synchronous operations on startup -[//]: # () -[//]: # (## Best Practices:) +- Implement splash screen during initialization -[//]: # (- Always measure performance before and after optimizations) +- Profile app cold start and warm start performance -[//]: # (- Use Flutter DevTools for accurate profiling) +- Lazy load dependencies with GetIt -[//]: # (- Implement performance regression testing) +- Initialize Hive CE asynchronously -[//]: # (- Document performance decisions and trade-offs) -[//]: # (- Monitor production performance metrics) +```dart -[//]: # (- Keep performance optimization maintainable) +Future main() async { -[//]: # (- Focus on user-perceived performance) + WidgetsFlutterBinding.ensureInitialized(); -[//]: # (- Test on real devices, not just emulators) + + // Critical initialization only -[//]: # (- Consider different device capabilities) + await initializeCore(); -[//]: # (- Profile in release mode, not debug mode) \ No newline at end of file + + runApp(MyApp()); + + + // Defer non-critical initialization + + Future.microtask(() async { + + await initializeNonCritical(); + + }); + +} + +``` + + +## Build Configuration: + +```yaml + +# Release build optimizations in android/app/build.gradle + +buildTypes { + + release { + + minifyEnabled true + + shrinkResources true + + proguardFiles getDefaultProguardFile('proguard-android.txt') + + } + +} + +``` + + +## Best Practices: + +- Always measure performance before and after optimizations + +- Use Flutter DevTools for accurate profiling + +- Implement performance regression testing + +- Document performance decisions and trade-offs + +- Monitor production performance metrics + +- Keep performance optimization maintainable + +- Focus on user-perceived performance + +- Test on real devices, not just emulators + +- Consider different device capabilities + +- Profile in release mode, not debug mode \ No newline at end of file diff --git a/.claude/agents/riverpod-expert.md b/.claude/agents/riverpod-expert.md index a6377bc..48fbb5f 100644 --- a/.claude/agents/riverpod-expert.md +++ b/.claude/agents/riverpod-expert.md @@ -1,990 +1,990 @@ -[//]: # (---) +--- -[//]: # (name: riverpod-expert) +name: riverpod-expert -[//]: # (description: Riverpod state management specialist. MUST BE USED for all state management, providers, and reactive programming tasks. Focuses on modern Riverpod 3.0 with code generation.) +description: Riverpod state management specialist. MUST BE USED for all state management, providers, and reactive programming tasks. Focuses on modern Riverpod 3.0 with code generation. -[//]: # (tools: Read, Write, Edit, Grep, Bash) +tools: Read, Write, Edit, Grep, Bash -[//]: # (---) +--- -[//]: # () -[//]: # (You are a Riverpod 3.0 expert specializing in:) -[//]: # (- Modern code generation with `@riverpod` annotation) +You are a Riverpod 3.0 expert specializing in: -[//]: # (- Creating providers with Notifier, AsyncNotifier, and StreamNotifier) +- Modern code generation with `@riverpod` annotation -[//]: # (- Implementing proper state management patterns) +- Creating providers with Notifier, AsyncNotifier, and StreamNotifier -[//]: # (- Handling async operations and loading states) +- Implementing proper state management patterns -[//]: # (- Testing providers and state logic) +- Handling async operations and loading states -[//]: # (- Provider composition and dependencies) +- Testing providers and state logic -[//]: # () -[//]: # (## Key Philosophy:) +- Provider composition and dependencies -[//]: # (**Code generation with `@riverpod` is the recommended approach.** It provides:) -[//]: # (- Type safety with compile-time checking) +## Key Philosophy: -[//]: # (- Less boilerplate code) +**Code generation with `@riverpod` is the recommended approach.** It provides: -[//]: # (- Automatic provider type selection) +- Type safety with compile-time checking -[//]: # (- Better hot-reload support) +- Less boilerplate code -[//]: # (- Simpler syntax without manual provider declarations) +- Automatic provider type selection -[//]: # () -[//]: # (## Modern Provider Types (Code Generation):) +- Better hot-reload support -[//]: # () -[//]: # (### Using `@riverpod` Annotation:) +- Simpler syntax without manual provider declarations -[//]: # (When using code generation, you don't manually choose provider types. Instead, write functions or classes with `@riverpod`, and Riverpod automatically generates the appropriate provider.) -[//]: # () -[//]: # (```dart) +## Modern Provider Types (Code Generation): -[//]: # (import 'package:riverpod_annotation/riverpod_annotation.dart';) -[//]: # () -[//]: # (part 'providers.g.dart';) +### Using `@riverpod` Annotation: -[//]: # () -[//]: # (// Simple immutable value) +When using code generation, you don't manually choose provider types. Instead, write functions or classes with `@riverpod`, and Riverpod automatically generates the appropriate provider. -[//]: # (@riverpod) -[//]: # (String userName(Ref ref) => 'John Doe';) +```dart -[//]: # () -[//]: # (// Async data fetching) +import 'package:riverpod_annotation/riverpod_annotation.dart'; -[//]: # (@riverpod) -[//]: # (Future user(Ref ref, String userId) async {) +part 'providers.g.dart'; -[//]: # ( final response = await http.get('api/user/$userId');) -[//]: # ( return User.fromJson(response);) +// Simple immutable value -[//]: # (}) +@riverpod -[//]: # () -[//]: # (// Stream of data) +String userName(Ref ref) => 'John Doe'; -[//]: # (@riverpod) -[//]: # (Stream messages(Ref ref) {) +// Async data fetching -[//]: # ( return ref.watch(webSocketProvider).stream;) +@riverpod -[//]: # (}) +Future user(Ref ref, String userId) async { -[//]: # () -[//]: # (// Mutable state with methods (Notifier)) + final response = await http.get('api/user/$userId'); -[//]: # (@riverpod) + return User.fromJson(response); -[//]: # (class Counter extends _$Counter {) +} -[//]: # ( @override) -[//]: # ( int build() => 0;) +// Stream of data -[//]: # ( ) -[//]: # ( void increment() => state++;) +@riverpod -[//]: # ( void decrement() => state--;) +Stream messages(Ref ref) { -[//]: # (}) + return ref.watch(webSocketProvider).stream; -[//]: # () -[//]: # (// Async state with initialization (AsyncNotifier)) +} -[//]: # (@riverpod) -[//]: # (class UserProfile extends _$UserProfile {) +// Mutable state with methods (Notifier) -[//]: # ( @override) +@riverpod -[//]: # ( Future build() async {) +class Counter extends _$Counter { -[//]: # ( return await ref.read(userRepositoryProvider).fetchUser();) + @override -[//]: # ( }) + int build() => 0; -[//]: # ( ) -[//]: # ( Future updateName(String name) async {) + + void increment() => state++; -[//]: # ( state = const AsyncValue.loading();) + void decrement() => state--; -[//]: # ( state = await AsyncValue.guard(() async {) +} -[//]: # ( return await ref.read(userRepositoryProvider).updateUser(name);) -[//]: # ( });) +// Async state with initialization (AsyncNotifier) -[//]: # ( }) +@riverpod -[//]: # (}) +class UserProfile extends _$UserProfile { -[//]: # () -[//]: # (// Stream state (StreamNotifier)) + @override -[//]: # (@riverpod) + Future build() async { -[//]: # (class ChatMessages extends _$ChatMessages {) + return await ref.read(userRepositoryProvider).fetchUser(); -[//]: # ( @override) + } -[//]: # ( Stream build() {) + + Future updateName(String name) async { -[//]: # ( return ref.watch(chatServiceProvider).messagesStream();) + state = const AsyncValue.loading(); -[//]: # ( }) + state = await AsyncValue.guard(() async { -[//]: # ( ) -[//]: # ( Future sendMessage(String text) async {) + return await ref.read(userRepositoryProvider).updateUser(name); -[//]: # ( await ref.read(chatServiceProvider).send(text);) + }); -[//]: # ( }) + } -[//]: # (}) +} -[//]: # (```) -[//]: # () -[//]: # (## Without Code Generation (Not Recommended):) +// Stream state (StreamNotifier) -[//]: # () -[//]: # (If you're not using code generation, you can still use basic providers:) +@riverpod -[//]: # () -[//]: # (```dart) +class ChatMessages extends _$ChatMessages { -[//]: # (// Simple immutable value) + @override -[//]: # (final userNameProvider = Provider((ref) => 'John Doe');) + Stream build() { -[//]: # () -[//]: # (// Async data) + return ref.watch(chatServiceProvider).messagesStream(); -[//]: # (final userProvider = FutureProvider.family((ref, userId) async {) + } -[//]: # ( final response = await http.get('api/user/$userId');) + + Future sendMessage(String text) async { -[//]: # ( return User.fromJson(response);) + await ref.read(chatServiceProvider).send(text); -[//]: # (});) + } -[//]: # () -[//]: # (// Stream) +} -[//]: # (final messagesProvider = StreamProvider((ref) {) +``` -[//]: # ( return ref.watch(webSocketProvider).stream;) -[//]: # (});) +## Without Code Generation (Not Recommended): -[//]: # () -[//]: # (// Mutable state (Notifier) - manual declaration) -[//]: # (class Counter extends Notifier {) +If you're not using code generation, you can still use basic providers: -[//]: # ( @override) -[//]: # ( int build() => 0;) +```dart -[//]: # ( ) -[//]: # ( void increment() => state++;) +// Simple immutable value -[//]: # (}) +final userNameProvider = Provider((ref) => 'John Doe'); -[//]: # () -[//]: # (final counterProvider = NotifierProvider(Counter.new);) -[//]: # (```) +// Async data -[//]: # () -[//]: # (**Note:** `StateNotifier`, `ChangeNotifierProvider`, and `StateProvider` are now **deprecated/discouraged**. Use `Notifier` and `AsyncNotifier` instead.) +final userProvider = FutureProvider.family((ref, userId) async { -[//]: # () -[//]: # (## Always Check First:) + final response = await http.get('api/user/$userId'); -[//]: # (- `pubspec.yaml` - Ensure code generation packages are installed) + return User.fromJson(response); -[//]: # (- Existing provider patterns and organization) +}); -[//]: # (- Whether code generation is already set up) -[//]: # (- Current Riverpod version (target 3.0+)) +// Stream -[//]: # () -[//]: # (## Setup Requirements:) +final messagesProvider = StreamProvider((ref) { -[//]: # () -[//]: # (### pubspec.yaml:) + return ref.watch(webSocketProvider).stream; -[//]: # (```yaml) +}); -[//]: # (dependencies:) -[//]: # ( flutter_riverpod: ^3.0.0) +// Mutable state (Notifier) - manual declaration -[//]: # ( riverpod_annotation: ^3.0.0) +class Counter extends Notifier { -[//]: # () -[//]: # (dev_dependencies:) + @override -[//]: # ( build_runner: ^2.4.0) + int build() => 0; -[//]: # ( riverpod_generator: ^3.0.0) + + void increment() => state++; -[//]: # ( riverpod_lint: ^3.0.0) +} -[//]: # ( custom_lint: ^0.6.0) -[//]: # (```) +final counterProvider = NotifierProvider(Counter.new); -[//]: # () -[//]: # (### Enable riverpod_lint:) +``` -[//]: # (Create `analysis_options.yaml`:) -[//]: # (```yaml) +**Note:** `StateNotifier`, `ChangeNotifierProvider`, and `StateProvider` are now **deprecated/discouraged**. Use `Notifier` and `AsyncNotifier` instead. -[//]: # (analyzer:) -[//]: # ( plugins:) +## Always Check First: -[//]: # ( - custom_lint) +- `pubspec.yaml` - Ensure code generation packages are installed -[//]: # (```) +- Existing provider patterns and organization -[//]: # () -[//]: # (### Run Code Generator:) +- Whether code generation is already set up -[//]: # (```bash) +- Current Riverpod version (target 3.0+) -[//]: # (dart run build_runner watch -d) -[//]: # (```) +## Setup Requirements: -[//]: # () -[//]: # (## Provider Organization:) -[//]: # () -[//]: # (```) +### pubspec.yaml: -[//]: # (lib/) +```yaml -[//]: # ( features/) +dependencies: -[//]: # ( auth/) + flutter_riverpod: ^3.0.0 -[//]: # ( providers/) + riverpod_annotation: ^3.0.0 -[//]: # ( auth_provider.dart # Auth state with methods) -[//]: # ( auth_repository_provider.dart # Dependency injection) +dev_dependencies: -[//]: # ( models/) + build_runner: ^2.4.0 -[//]: # ( ...) + riverpod_generator: ^3.0.0 -[//]: # (```) + riverpod_lint: ^3.0.0 -[//]: # () -[//]: # (## Key Patterns:) + custom_lint: ^0.6.0 -[//]: # () -[//]: # (### 1. Dependency Injection:) +``` -[//]: # (```dart) -[//]: # (// Provide dependencies) +### Enable riverpod_lint: -[//]: # (@riverpod) +Create `analysis_options.yaml`: -[//]: # (AuthRepository authRepository(Ref ref) {) +```yaml -[//]: # ( return AuthRepositoryImpl() +analyzer: -[//]: # ( api: ref.watch(apiClientProvider),) + plugins: -[//]: # ( storage: ref.watch(secureStorageProvider),) + - custom_lint -[//]: # ( );) +``` -[//]: # (}) -[//]: # () -[//]: # (// Use in other providers) +### Run Code Generator: -[//]: # (@riverpod) +```bash -[//]: # (class Auth extends _$Auth {) +dart run build_runner watch -d -[//]: # ( @override) +``` -[//]: # ( Future build() async {) -[//]: # ( return await ref.read(authRepositoryProvider).getCurrentUser();) +## Provider Organization: -[//]: # ( }) -[//]: # ( ) -[//]: # ( Future login(String email, String password) async {) +``` -[//]: # ( state = const AsyncValue.loading();) +lib/ -[//]: # ( state = await AsyncValue.guard(() async {) + features/ -[//]: # ( return await ref.read(authRepositoryProvider).login(email, password);) + auth/ -[//]: # ( });) + providers/ -[//]: # ( }) + auth_provider.dart # Auth state with methods -[//]: # (}) + auth_repository_provider.dart # Dependency injection -[//]: # (```) + models/ -[//]: # () -[//]: # (### 2. Provider Parameters (Family):) + ... -[//]: # (```dart) +``` -[//]: # (// Parameters are just function parameters!) -[//]: # (@riverpod) +## Key Patterns: -[//]: # (Future post(Ref ref, String postId) async {) -[//]: # ( return await ref.read(apiProvider).getPost(postId);) +### 1. Dependency Injection: -[//]: # (}) +```dart -[//]: # () -[//]: # (// Multiple parameters, named, optional, defaults - all supported!) +// Provide dependencies -[//]: # (@riverpod) +@riverpod -[//]: # (Future posts() +AuthRepository authRepository(Ref ref) { -[//]: # ( Ref ref, {) + return AuthRepositoryImpl( -[//]: # ( int page = 1,) + api: ref.watch(apiClientProvider), -[//]: # ( int limit = 20,) + storage: ref.watch(secureStorageProvider), -[//]: # ( String? category,) + ); -[//]: # (}) async {) +} -[//]: # ( return await ref.read(apiProvider).getPosts() -[//]: # ( page: page,) +// Use in other providers -[//]: # ( limit: limit,) +@riverpod -[//]: # ( category: category,) +class Auth extends _$Auth { -[//]: # ( );) + @override -[//]: # (}) + Future build() async { -[//]: # () -[//]: # (// Usage in widgets) + return await ref.read(authRepositoryProvider).getCurrentUser(); -[//]: # (final post = ref.watch(postProvider('post-123'));) + } -[//]: # (final posts = ref.watch(postsProvider(page: 2, category: 'tech'));) + + Future login(String email, String password) async { -[//]: # (```) + state = const AsyncValue.loading(); -[//]: # () -[//]: # (### 3. Loading States:) + state = await AsyncValue.guard(() async { -[//]: # (```dart) + return await ref.read(authRepositoryProvider).login(email, password); -[//]: # (// In widgets - using .when()) + }); -[//]: # (ref.watch(userProvider).when() + } -[//]: # ( data: (user) => UserView(user),) +} -[//]: # ( loading: () => CircularProgressIndicator(),) +``` -[//]: # ( error: (error, stack) => ErrorView(error),) -[//]: # ();) +### 2. Provider Parameters (Family): -[//]: # () -[//]: # (// Or pattern matching (Dart 3.0+)) +```dart -[//]: # (final userState = ref.watch(userProvider);) +// Parameters are just function parameters! -[//]: # (switch (userState) {) +@riverpod -[//]: # ( case AsyncData(:final value):) +Future post(Ref ref, String postId) async { -[//]: # ( return UserView(value);) + return await ref.read(apiProvider).getPost(postId); -[//]: # ( case AsyncError(:final error):) +} -[//]: # ( return ErrorView(error);) -[//]: # ( case AsyncLoading():) +// Multiple parameters, named, optional, defaults - all supported! -[//]: # ( return CircularProgressIndicator();) +@riverpod -[//]: # (}) +Future posts( -[//]: # () -[//]: # (// Check states directly) + Ref ref, { -[//]: # (if (userState.isLoading) return LoadingWidget();) + int page = 1, -[//]: # (if (userState.hasError) return ErrorWidget(userState.error);) + int limit = 20, -[//]: # (final user = userState.value!;) + String? category, -[//]: # (```) +}) async { -[//]: # () -[//]: # (### 4. Provider Composition:) + return await ref.read(apiProvider).getPosts( -[//]: # (```dart) + page: page, -[//]: # (// Depend on other providers) + limit: limit, -[//]: # (@riverpod) + category: category, -[//]: # (Future dashboard(Ref ref) async {) + ); -[//]: # ( // Wait for multiple providers) +} -[//]: # ( final user = await ref.watch(userProvider.future);) -[//]: # ( final posts = await ref.watch(userPostsProvider.future);) +// Usage in widgets -[//]: # ( final stats = await ref.watch(statsProvider.future);) +final post = ref.watch(postProvider('post-123')); -[//]: # ( ) -[//]: # ( return Dashboard(user: user, posts: posts, stats: stats);) +final posts = ref.watch(postsProvider(page: 2, category: 'tech')); -[//]: # (}) +``` -[//]: # () -[//]: # (// Watch and react to changes) -[//]: # (@riverpod) +### 3. Loading States: -[//]: # (class FilteredPosts extends _$FilteredPosts {) +```dart -[//]: # ( @override) +// In widgets - using .when() -[//]: # ( List build() {) +ref.watch(userProvider).when( -[//]: # ( final posts = ref.watch(postsProvider).value ?? [];) + data: (user) => UserView(user), -[//]: # ( final filter = ref.watch(filterProvider);) + loading: () => CircularProgressIndicator(), -[//]: # ( ) -[//]: # ( return posts.where((post) => post.category == filter).toList();) + error: (error, stack) => ErrorView(error), -[//]: # ( }) +); -[//]: # (}) -[//]: # (```) +// Or pattern matching (Dart 3.0+) -[//]: # () -[//]: # (### 5. Selective Watching (Performance):) +final userState = ref.watch(userProvider); -[//]: # (```dart) +switch (userState) { -[//]: # (// Bad - rebuilds on any user change) + case AsyncData(:final value): -[//]: # (final user = ref.watch(userProvider);) + return UserView(value); -[//]: # () -[//]: # (// Good - rebuilds only when name changes) + case AsyncError(:final error): -[//]: # (final name = ref.watch(userProvider.select((user) => user.name));) + return ErrorView(error); -[//]: # () -[//]: # (// In AsyncNotifier) + case AsyncLoading(): -[//]: # (@riverpod) + return CircularProgressIndicator(); -[//]: # (class Example extends _$Example {) +} -[//]: # ( @override) -[//]: # ( String build() {) +// Check states directly -[//]: # ( // Only rebuild when user name changes) +if (userState.isLoading) return LoadingWidget(); -[//]: # ( final userName = ref.watch() +if (userState.hasError) return ErrorWidget(userState.error); -[//]: # ( userProvider.select((async) => async.value?.name)) +final user = userState.value!; -[//]: # ( );) +``` -[//]: # ( return userName ?? 'Anonymous';) -[//]: # ( }) +### 4. Provider Composition: -[//]: # (}) +```dart -[//]: # (```) +// Depend on other providers -[//]: # () -[//]: # (### 6. Invalidation and Refresh:) +@riverpod -[//]: # (```dart) +Future dashboard(Ref ref) async { -[//]: # (// Invalidate provider) + // Wait for multiple providers -[//]: # (ref.invalidate(userProvider);) + final user = await ref.watch(userProvider.future); -[//]: # () -[//]: # (// Refresh (invalidate and re-read)) + final posts = await ref.watch(userPostsProvider.future); -[//]: # (ref.refresh(userProvider);) + final stats = await ref.watch(statsProvider.future); -[//]: # () -[//]: # (// In AsyncNotifier with custom refresh) + + return Dashboard(user: user, posts: posts, stats: stats); -[//]: # (@riverpod) +} -[//]: # (class Posts extends _$Posts {) -[//]: # ( @override) +// Watch and react to changes -[//]: # ( Future build() => _fetch();) +@riverpod -[//]: # ( ) -[//]: # ( Future refresh() async {) +class FilteredPosts extends _$FilteredPosts { -[//]: # ( state = const AsyncValue.loading();) + @override -[//]: # ( state = await AsyncValue.guard(_fetch);) + List build() { -[//]: # ( }) + final posts = ref.watch(postsProvider).value ?? []; -[//]: # ( ) -[//]: # ( Future _fetch() async {) + final filter = ref.watch(filterProvider); -[//]: # ( return await ref.read(apiProvider).getPosts();) + + return posts.where((post) => post.category == filter).toList(); -[//]: # ( }) + } -[//]: # (}) +} -[//]: # (```) +``` -[//]: # () -[//]: # (### 7. AutoDispose (Riverpod 3.0):) -[//]: # (```dart) +### 5. Selective Watching (Performance): -[//]: # (// By default, generated providers are autoDispose) +```dart -[//]: # (@riverpod) +// Bad - rebuilds on any user change -[//]: # (String example1(Ref ref) => 'auto disposed';) +final user = ref.watch(userProvider); -[//]: # () -[//]: # (// Keep alive if needed) -[//]: # (@Riverpod(keepAlive: true)) +// Good - rebuilds only when name changes -[//]: # (String example2(Ref ref) => 'kept alive';) +final name = ref.watch(userProvider.select((user) => user.name)); -[//]: # () -[//]: # (// Check if provider is still mounted) -[//]: # (@riverpod) +// In AsyncNotifier -[//]: # (class TodoList extends _$TodoList {) +@riverpod -[//]: # ( @override) +class Example extends _$Example { -[//]: # ( List build() => [];) + @override -[//]: # ( ) -[//]: # ( Future addTodo(Todo todo) async {) + String build() { -[//]: # ( await api.saveTodo(todo);) + // Only rebuild when user name changes -[//]: # ( ) -[//]: # ( // Check if still mounted after async operation) + final userName = ref.watch( -[//]: # ( if (!ref.mounted) return;) + userProvider.select((async) => async.value?.name) -[//]: # ( ) -[//]: # ( state = [...state, todo];) + ); -[//]: # ( }) + return userName ?? 'Anonymous'; -[//]: # (}) + } -[//]: # (```) +} -[//]: # () -[//]: # (## Consumer Widgets:) +``` -[//]: # () -[//]: # (### ConsumerWidget:) -[//]: # (```dart) +### 6. Invalidation and Refresh: -[//]: # (class MyWidget extends ConsumerWidget {) +```dart -[//]: # ( @override) +// Invalidate provider -[//]: # ( Widget build(BuildContext context, WidgetRef ref) {) +ref.invalidate(userProvider); -[//]: # ( final count = ref.watch(counterProvider);) -[//]: # ( return Text('$count');) +// Refresh (invalidate and re-read) -[//]: # ( }) +ref.refresh(userProvider); -[//]: # (}) -[//]: # (```) +// In AsyncNotifier with custom refresh -[//]: # () -[//]: # (### ConsumerStatefulWidget:) +@riverpod -[//]: # (```dart) +class Posts extends _$Posts { -[//]: # (class MyWidget extends ConsumerStatefulWidget {) + @override -[//]: # ( @override) + Future build() => _fetch(); -[//]: # ( ConsumerState createState() => _MyWidgetState();) + + Future refresh() async { -[//]: # (}) + state = const AsyncValue.loading(); -[//]: # () -[//]: # (class _MyWidgetState extends ConsumerState {) + state = await AsyncValue.guard(_fetch); -[//]: # ( @override) + } -[//]: # ( void initState() {) + + Future _fetch() async { -[//]: # ( super.initState();) + return await ref.read(apiProvider).getPosts(); -[//]: # ( // ref is available in all lifecycle methods) + } -[//]: # ( ref.read(dataProvider.notifier).loadData();) +} -[//]: # ( }) +``` -[//]: # ( ) -[//]: # ( @override) -[//]: # ( Widget build(BuildContext context) {) +### 7. AutoDispose (Riverpod 3.0): -[//]: # ( final data = ref.watch(dataProvider);) +```dart -[//]: # ( return Text('$data');) +// By default, generated providers are autoDispose -[//]: # ( }) +@riverpod -[//]: # (}) +String example1(Ref ref) => 'auto disposed'; -[//]: # (```) -[//]: # () -[//]: # (### Consumer (for optimization):) +// Keep alive if needed -[//]: # (```dart) +@Riverpod(keepAlive: true) -[//]: # (Column() +String example2(Ref ref) => 'kept alive'; -[//]: # ( children: [) -[//]: # ( const Text('Static content'),) +// Check if provider is still mounted -[//]: # ( Consumer() +@riverpod -[//]: # ( builder: (context, ref, child) {) +class TodoList extends _$TodoList { -[//]: # ( final count = ref.watch(counterProvider);) + @override -[//]: # ( return Text('$count');) + List build() => []; -[//]: # ( },) + + Future addTodo(Todo todo) async { -[//]: # ( ),) + await api.saveTodo(todo); -[//]: # ( const Text('More static content'),) + + // Check if still mounted after async operation -[//]: # ( ],) + if (!ref.mounted) return; -[//]: # ()) + + state = [...state, todo]; -[//]: # (```) + } -[//]: # () -[//]: # (## Testing:) +} -[//]: # () -[//]: # (```dart) +``` -[//]: # (test('counter increments', () {) -[//]: # ( final container = ProviderContainer();) +## Consumer Widgets: -[//]: # ( addTearDown(container.dispose);) -[//]: # ( ) -[//]: # ( expect(container.read(counterProvider), 0);) +### ConsumerWidget: -[//]: # ( container.read(counterProvider.notifier).increment();) +```dart -[//]: # ( expect(container.read(counterProvider), 1);) +class MyWidget extends ConsumerWidget { -[//]: # (});) + @override -[//]: # () -[//]: # (// Async provider testing) + Widget build(BuildContext context, WidgetRef ref) { -[//]: # (test('fetches user', () async {) + final count = ref.watch(counterProvider); -[//]: # ( final container = ProviderContainer() + return Text('$count'); -[//]: # ( overrides: [) + } -[//]: # ( authRepositoryProvider.overrideWithValue(MockAuthRepository()),) +} -[//]: # ( ],) +``` -[//]: # ( );) -[//]: # ( addTearDown(container.dispose);) +### ConsumerStatefulWidget: -[//]: # ( ) -[//]: # ( final user = await container.read(userProvider.future);) +```dart -[//]: # ( expect(user.name, 'Test User');) +class MyWidget extends ConsumerStatefulWidget { -[//]: # (});) + @override -[//]: # () -[//]: # (// Widget testing) + ConsumerState createState() => _MyWidgetState(); -[//]: # (testWidgets('displays user name', (tester) async {) +} -[//]: # ( await tester.pumpWidget() -[//]: # ( ProviderScope() +class _MyWidgetState extends ConsumerState { -[//]: # ( overrides: [) + @override -[//]: # ( userProvider.overrideWith((ref) => User(name: 'Test')),) + void initState() { -[//]: # ( ],) + super.initState(); -[//]: # ( child: MaterialApp(home: UserScreen()),) + // ref is available in all lifecycle methods -[//]: # ( ),) + ref.read(dataProvider.notifier).loadData(); -[//]: # ( );) + } -[//]: # ( ) -[//]: # ( expect(find.text('Test'), findsOneWidget);) + + @override -[//]: # (});) + Widget build(BuildContext context) { -[//]: # (```) + final data = ref.watch(dataProvider); -[//]: # () -[//]: # (## Common Patterns:) + return Text('$data'); -[//]: # () -[//]: # (### Pagination:) + } -[//]: # (```dart) +} -[//]: # (@riverpod) +``` -[//]: # (class PostList extends _$PostList {) -[//]: # ( @override) +### Consumer (for optimization): -[//]: # ( Future build() => _fetchPage(0);) +```dart -[//]: # ( ) -[//]: # ( int _page = 0;) +Column( -[//]: # ( ) -[//]: # ( Future loadMore() async {) + children: [ -[//]: # ( final currentPosts = state.value ?? [];) + const Text('Static content'), -[//]: # ( _page++;) + Consumer( -[//]: # ( ) -[//]: # ( state = const AsyncValue.loading();) + builder: (context, ref, child) { -[//]: # ( state = await AsyncValue.guard(() async {) + final count = ref.watch(counterProvider); -[//]: # ( final newPosts = await _fetchPage(_page);) + return Text('$count'); -[//]: # ( return [...currentPosts, ...newPosts];) + }, -[//]: # ( });) + ), -[//]: # ( }) + const Text('More static content'), -[//]: # ( ) -[//]: # ( Future _fetchPage(int page) async {) + ], -[//]: # ( return await ref.read(apiProvider).getPosts(page: page);) +) -[//]: # ( }) +``` -[//]: # (}) -[//]: # (```) +## Testing: -[//]: # () -[//]: # (### Form State:) -[//]: # (```dart) +```dart -[//]: # (@riverpod) +test('counter increments', () { -[//]: # (class LoginForm extends _$LoginForm {) + final container = ProviderContainer(); -[//]: # ( @override) + addTearDown(container.dispose); -[//]: # ( LoginFormState build() => LoginFormState();) + + expect(container.read(counterProvider), 0); -[//]: # ( ) -[//]: # ( void setEmail(String email) {) + container.read(counterProvider.notifier).increment(); -[//]: # ( state = state.copyWith(email: email);) + expect(container.read(counterProvider), 1); -[//]: # ( }) +}); -[//]: # ( ) -[//]: # ( void setPassword(String password) {) -[//]: # ( state = state.copyWith(password: password);) +// Async provider testing -[//]: # ( }) +test('fetches user', () async { -[//]: # ( ) -[//]: # ( Future submit() async {) + final container = ProviderContainer( -[//]: # ( if (!state.isValid) return;) + overrides: [ -[//]: # ( ) -[//]: # ( state = state.copyWith(isLoading: true);) + authRepositoryProvider.overrideWithValue(MockAuthRepository()), -[//]: # ( try {) + ], -[//]: # ( await ref.read(authRepositoryProvider).login() + ); -[//]: # ( state.email,) + addTearDown(container.dispose); -[//]: # ( state.password,) + + final user = await container.read(userProvider.future); -[//]: # ( );) + expect(user.name, 'Test User'); -[//]: # ( state = state.copyWith(isLoading: false, isSuccess: true);) +}); -[//]: # ( } catch (e) {) -[//]: # ( state = state.copyWith() +// Widget testing -[//]: # ( isLoading: false,) +testWidgets('displays user name', (tester) async { -[//]: # ( error: e.toString(),) + await tester.pumpWidget( -[//]: # ( );) + ProviderScope( -[//]: # ( }) + overrides: [ -[//]: # ( }) + userProvider.overrideWith((ref) => User(name: 'Test')), -[//]: # (}) + ], -[//]: # (```) + child: MaterialApp(home: UserScreen()), -[//]: # () -[//]: # (## Important Notes:) + ), -[//]: # () -[//]: # (### Deprecated/Discouraged Providers:) + ); -[//]: # (- ❌ `StateNotifierProvider` → Use `NotifierProvider` with `@riverpod class`) + + expect(find.text('Test'), findsOneWidget); -[//]: # (- ❌ `ChangeNotifierProvider` → Use `NotifierProvider` with `@riverpod class`) +}); -[//]: # (- ❌ `StateProvider` → Use `NotifierProvider` for simple mutable state) +``` -[//]: # () -[//]: # (### Riverpod 3.0 Changes:) -[//]: # (- **Unified Ref**: No more `FutureProviderRef`, `StreamProviderRef`, etc. Just `Ref`) +## Common Patterns: -[//]: # (- **Simplified Notifier**: No more separate `FamilyNotifier`, `AutoDisposeNotifier` classes) -[//]: # (- **Automatic Retry**: Failed providers automatically retry with exponential backoff) +### Pagination: -[//]: # (- **ref.mounted**: Check if provider is still alive after async operations) +```dart -[//]: # () -[//]: # (### Best Practices:) +@riverpod -[//]: # (- **Always use code generation** for new projects) +class PostList extends _$PostList { -[//]: # (- Use `@riverpod` annotation for all providers) + @override -[//]: # (- Keep providers in dedicated `providers/` folders) + Future build() => _fetchPage(0); -[//]: # (- Use `Notifier`/`AsyncNotifier` for mutable state with methods) + + int _page = 0; -[//]: # (- Use simple `@riverpod` functions for computed/fetched immutable data) + + Future loadMore() async { -[//]: # (- Always check `ref.mounted` after async operations in Notifiers) + final currentPosts = state.value ?? []; -[//]: # (- Use `AsyncValue.guard()` for proper error handling) + _page++; -[//]: # (- Leverage provider composition to avoid duplication) + + state = const AsyncValue.loading(); -[//]: # (- Use `.select()` to optimize rebuilds) + state = await AsyncValue.guard(() async { -[//]: # (- Write tests for business logic in providers) + final newPosts = await _fetchPage(_page); -[//]: # () -[//]: # (### Migration from Old Riverpod:) + return [...currentPosts, ...newPosts]; -[//]: # (If migrating from older Riverpod code:) + }); -[//]: # (1. Add code generation packages to `pubspec.yaml`) + } -[//]: # (2. Convert `StateNotifierProvider` to `@riverpod class ... extends _$... { @override ... }`) + + Future _fetchPage(int page) async { -[//]: # (3. Convert `StateProvider` to `@riverpod class` with simple state) + return await ref.read(apiProvider).getPosts(page: page); -[//]: # (4. Replace manual family with function parameters) + } -[//]: # (5. Update `Ref` to just `Ref`) +} -[//]: # (6. Use `AsyncValue.guard()` instead of try-catch for async operations) \ No newline at end of file +``` + + +### Form State: + +```dart + +@riverpod + +class LoginForm extends _$LoginForm { + + @override + + LoginFormState build() => LoginFormState(); + + + void setEmail(String email) { + + state = state.copyWith(email: email); + + } + + + void setPassword(String password) { + + state = state.copyWith(password: password); + + } + + + Future submit() async { + + if (!state.isValid) return; + + + state = state.copyWith(isLoading: true); + + try { + + await ref.read(authRepositoryProvider).login( + + state.email, + + state.password, + + ); + + state = state.copyWith(isLoading: false, isSuccess: true); + + } catch (e) { + + state = state.copyWith( + + isLoading: false, + + error: e.toString(), + + ); + + } + + } + +} + +``` + + +## Important Notes: + + +### Deprecated/Discouraged Providers: + +- ❌ `StateNotifierProvider` → Use `NotifierProvider` with `@riverpod class` + +- ❌ `ChangeNotifierProvider` → Use `NotifierProvider` with `@riverpod class` + +- ❌ `StateProvider` → Use `NotifierProvider` for simple mutable state + + +### Riverpod 3.0 Changes: + +- **Unified Ref**: No more `FutureProviderRef`, `StreamProviderRef`, etc. Just `Ref` + +- **Simplified Notifier**: No more separate `FamilyNotifier`, `AutoDisposeNotifier` classes + +- **Automatic Retry**: Failed providers automatically retry with exponential backoff + +- **ref.mounted**: Check if provider is still alive after async operations + + +### Best Practices: + +- **Always use code generation** for new projects + +- Use `@riverpod` annotation for all providers + +- Keep providers in dedicated `providers/` folders + +- Use `Notifier`/`AsyncNotifier` for mutable state with methods + +- Use simple `@riverpod` functions for computed/fetched immutable data + +- Always check `ref.mounted` after async operations in Notifiers + +- Use `AsyncValue.guard()` for proper error handling + +- Leverage provider composition to avoid duplication + +- Use `.select()` to optimize rebuilds + +- Write tests for business logic in providers + + +### Migration from Old Riverpod: + +If migrating from older Riverpod code: + +1. Add code generation packages to `pubspec.yaml` + +2. Convert `StateNotifierProvider` to `@riverpod class ... extends _$... { @override ... }` + +3. Convert `StateProvider` to `@riverpod class` with simple state + +4. Replace manual family with function parameters + +5. Update `Ref` to just `Ref` + +6. Use `AsyncValue.guard()` instead of try-catch for async operations \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 0d4babd..488c7e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ You have access to these expert subagents - USE THEM PROACTIVELY: ## Flutter Best Practices - Use Flutter 3.x features and Material 3 design - Implement clean architecture with Riverpod for state management -- Use Hive for local database and offline-first functionality +- Use Hive for local database and offline functionality - Follow proper dependency injection with Riverpod DI - Implement proper error handling and user feedback - Follow iOS and Android platform-specific design guidelines @@ -1704,7 +1704,7 @@ class ProductsPage extends ConsumerWidget { --- -## Offline-First Strategy +## Offline Strategy ### Data Sync Flow ```dart @@ -2164,7 +2164,7 @@ end - [ ] Matches HTML reference design - [ ] Follows clean architecture - [ ] Proper error handling -- [ ] Offline-first approach +- [ ] Online-first approach - [ ] Performance optimized - [ ] Proper state management with Riverpod - [ ] Hive models properly defined diff --git a/HIVE_SETUP.md b/HIVE_SETUP.md new file mode 100644 index 0000000..8c39724 --- /dev/null +++ b/HIVE_SETUP.md @@ -0,0 +1,522 @@ +# Hive CE Database Setup - Worker App + +## Overview + +The Worker Flutter app now has a complete Hive CE (Community Edition) database setup for offline-first functionality. This document provides a comprehensive summary of the setup. + +## What Was Created + +### 1. Core Files + +#### `/lib/core/constants/storage_constants.dart` +Centralized constants for Hive database: +- **HiveBoxNames**: All box names (13 boxes total) +- **HiveTypeIds**: Type adapter IDs (0-223 range, 32 IDs assigned) +- **HiveKeys**: Storage keys for settings and cache +- **CacheDuration**: Default cache expiration times +- **HiveDatabaseConfig**: Database configuration settings + +#### `/lib/core/database/hive_service.dart` +Main database service handling: +- Hive initialization and configuration +- Type adapter registration (auto-generated) +- Box opening and management +- Database migrations and versioning +- Encryption support +- Maintenance and compaction +- Cleanup operations + +#### `/lib/core/database/database_manager.dart` +High-level database operations: +- Generic CRUD operations +- Cache management with expiration +- Sync state tracking +- Settings operations +- Offline queue management +- Database statistics + +#### `/lib/core/database/hive_initializer.dart` +Simple initialization API: +- Easy app startup initialization +- Database reset functionality +- Logout data cleanup +- Statistics helpers + +#### `/lib/core/database/database.dart` +Export file for convenient imports + +### 2. Models + +#### `/lib/core/database/models/enums.dart` +Type adapters for all enums (10 enums): +- `MemberTier` - Loyalty tiers (Gold, Platinum, Diamond) +- `UserType` - User categories (Contractor, Architect, Distributor, Broker) +- `OrderStatus` - Order states (6 statuses) +- `ProjectStatus` - Project states (5 statuses) +- `ProjectType` - Project categories (5 types) +- `TransactionType` - Loyalty transaction types (8 types) +- `GiftStatus` - Gift/reward states (5 statuses) +- `PaymentStatus` - Payment states (6 statuses) +- `NotificationType` - Notification categories (7 types) +- `PaymentMethod` - Payment methods (6 methods) + +Each enum includes: +- Extension methods for display names +- Helper properties for state checking +- Vietnamese localization + +#### `/lib/core/database/models/cached_data.dart` +Generic cache wrapper model: +- Wraps any data type with timestamp +- Expiration tracking +- Freshness checking +- Cache age calculation + +### 3. Generated Files + +#### `/lib/hive_registrar.g.dart` (Auto-generated) +Automatic adapter registration extension: +- Registers all type adapters automatically +- No manual registration needed +- Updates automatically when new models are added + +#### `/lib/core/database/models/*.g.dart` (Auto-generated) +Individual type adapters for each model and enum + +### 4. Configuration + +#### `pubspec.yaml` +Updated dependencies: +```yaml +dependencies: + hive_ce: ^2.6.0 + hive_ce_flutter: ^2.1.0 + +dev_dependencies: + hive_ce_generator: ^1.6.0 + build_runner: ^2.4.11 +``` + +#### `build.yaml` +Build runner configuration for code generation + +## Database Architecture + +### Box Structure + +The app uses 13 Hive boxes organized by functionality: + +**Encrypted Boxes (Sensitive Data):** +1. `user_box` - User profile and authentication +2. `cart_box` - Shopping cart items +3. `order_box` - Order history +4. `project_box` - Construction projects +5. `loyalty_box` - Loyalty transactions +6. `address_box` - Delivery addresses +7. `offline_queue_box` - Failed API requests + +**Non-Encrypted Boxes:** +8. `product_box` - Product catalog cache +9. `rewards_box` - Rewards catalog +10. `settings_box` - App settings +11. `cache_box` - Generic API cache +12. `sync_state_box` - Sync timestamps +13. `notification_box` - Notifications + +### Type ID Allocation + +Reserved type IDs (never change once assigned): + +**Core Models (0-9):** +- 0: UserModel (TODO) +- 1: ProductModel (TODO) +- 2: CartItemModel (TODO) +- 3: OrderModel (TODO) +- 4: ProjectModel (TODO) +- 5: LoyaltyTransactionModel (TODO) + +**Extended Models (10-19):** +- 10: OrderItemModel (TODO) +- 11: AddressModel (TODO) +- 12: CategoryModel (TODO) +- 13: RewardModel (TODO) +- 14: GiftModel (TODO) +- 15: NotificationModel (TODO) +- 16: QuoteModel (TODO) +- 17: PaymentModel (TODO) +- 18: PromotionModel (TODO) +- 19: ReferralModel (TODO) + +**Enums (20-29):** ✓ Created +- 20: MemberTier +- 21: UserType +- 22: OrderStatus +- 23: ProjectStatus +- 24: ProjectType +- 25: TransactionType +- 26: GiftStatus +- 27: PaymentStatus +- 28: NotificationType +- 29: PaymentMethod + +**Cache & Sync Models (30-39):** +- 30: CachedData ✓ Created +- 31: SyncState (TODO) +- 32: OfflineRequest (TODO) + +## How to Use + +### 1. Initialize Hive in main.dart + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'core/database/hive_initializer.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive database + await HiveInitializer.initialize( + verbose: true, // Enable logging in debug mode + ); + + runApp( + const ProviderScope( + child: MyApp(), + ), + ); +} +``` + +### 2. Basic Database Operations + +```dart +import 'package:worker/core/database/database.dart'; + +// Get database manager instance +final dbManager = DatabaseManager(); + +// Save data +await dbManager.save( + boxName: HiveBoxNames.productBox, + key: 'product_123', + value: product, +); + +// Get data +final product = dbManager.get( + boxName: HiveBoxNames.productBox, + key: 'product_123', +); + +// Get all items +final products = dbManager.getAll(boxName: HiveBoxNames.productBox); + +// Delete data +await dbManager.delete( + boxName: HiveBoxNames.productBox, + key: 'product_123', +); +``` + +### 3. Caching with Expiration + +```dart +import 'package:worker/core/database/database.dart'; + +final dbManager = DatabaseManager(); + +// Save to cache with timestamp +await dbManager.saveToCache( + key: HiveKeys.productsCacheKey, + data: products, +); + +// Get from cache (returns null if expired) +final cachedProducts = dbManager.getFromCache>( + key: HiveKeys.productsCacheKey, + maxAge: CacheDuration.products, // 6 hours +); + +if (cachedProducts == null) { + // Cache miss or expired - fetch from API + final freshProducts = await api.getProducts(); + await dbManager.saveToCache( + key: HiveKeys.productsCacheKey, + data: freshProducts, + ); +} +``` + +### 4. Offline Queue + +```dart +import 'package:worker/core/database/database.dart'; + +final dbManager = DatabaseManager(); + +// Add failed request to queue +try { + await api.createOrder(orderData); +} catch (e) { + await dbManager.addToOfflineQueue({ + 'endpoint': '/api/orders', + 'method': 'POST', + 'body': orderData, + }); +} + +// Process queue when back online +final queue = dbManager.getOfflineQueue(); +for (var i = 0; i < queue.length; i++) { + try { + await api.request(queue[i]); + await dbManager.removeFromOfflineQueue(i); + } catch (e) { + // Keep in queue for next retry + } +} +``` + +### 5. Settings Management + +```dart +import 'package:worker/core/database/database.dart'; + +final dbManager = DatabaseManager(); + +// Save setting +await dbManager.saveSetting( + key: HiveKeys.languageCode, + value: 'vi', +); + +// Get setting +final language = dbManager.getSetting( + key: HiveKeys.languageCode, + defaultValue: 'vi', +); +``` + +### 6. Sync State Tracking + +```dart +import 'package:worker/core/database/database.dart'; + +final dbManager = DatabaseManager(); + +// Update sync timestamp +await dbManager.updateSyncTime(HiveKeys.productsSyncTime); + +// Get last sync time +final lastSync = dbManager.getLastSyncTime(HiveKeys.productsSyncTime); + +// Check if needs sync +final needsSync = dbManager.needsSync( + dataType: HiveKeys.productsSyncTime, + syncInterval: Duration(hours: 6), +); + +if (needsSync) { + // Perform sync + await syncProducts(); + await dbManager.updateSyncTime(HiveKeys.productsSyncTime); +} +``` + +### 7. Logout (Clear User Data) + +```dart +import 'package:worker/core/database/hive_initializer.dart'; + +// Clear user data while keeping settings and cache +await HiveInitializer.logout(); +``` + +## Creating New Hive Models + +### Step 1: Create Model File + +```dart +// lib/features/products/data/models/product_model.dart + +import 'package:hive_ce/hive.dart'; +import 'package:worker/core/constants/storage_constants.dart'; + +part 'product_model.g.dart'; + +@HiveType(typeId: HiveTypeIds.product) // Use typeId: 1 +class ProductModel extends HiveObject { + @HiveField(0) + final String id; + + @HiveField(1) + final String name; + + @HiveField(2) + final String sku; + + @HiveField(3) + final double price; + + @HiveField(4) + final List images; + + @HiveField(5) + final String categoryId; + + @HiveField(6) + final int stock; + + ProductModel({ + required this.id, + required this.name, + required this.sku, + required this.price, + required this.images, + required this.categoryId, + required this.stock, + }); +} +``` + +### Step 2: Generate Adapter + +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +This automatically: +- Generates `product_model.g.dart` with `ProductModelAdapter` +- Updates `hive_registrar.g.dart` to register the new adapter +- No manual registration needed! + +### Step 3: Use the Model + +```dart +import 'package:worker/core/database/database.dart'; + +final product = ProductModel( + id: '123', + name: 'Ceramic Tile', + sku: 'TILE-001', + price: 299000, + images: ['image1.jpg'], + categoryId: 'cat_1', + stock: 100, +); + +final dbManager = DatabaseManager(); +await dbManager.save( + boxName: HiveBoxNames.productBox, + key: product.id, + value: product, +); +``` + +## Important Rules + +1. **Never change Type IDs** - Once assigned, they are permanent +2. **Never change Field numbers** - Breaks existing data +3. **Run build_runner** after creating/modifying models +4. **Use the auto-generated registrar** - Don't manually register adapters +5. **Always use try-catch** around Hive operations +6. **Check box is open** before accessing it +7. **Use DatabaseManager** for high-level operations +8. **Set appropriate cache durations** for different data types + +## Features + +✓ **Offline-First**: All data stored locally and synced +✓ **Type-Safe**: Strong typing with generated adapters +✓ **Fast**: Optimized NoSQL database +✓ **Encrypted**: Optional AES encryption for sensitive data +✓ **Auto-Maintenance**: Compaction and cleanup +✓ **Migration Support**: Schema versioning built-in +✓ **Cache Management**: Automatic expiration handling +✓ **Offline Queue**: Failed request retry system +✓ **Sync Tracking**: Data freshness monitoring +✓ **Statistics**: Debug utilities for monitoring + +## Next Steps + +### To Complete the Database Setup: + +1. **Create Model Classes** for the TODO items (typeIds 0-19, 31-32) +2. **Run build_runner** to generate adapters +3. **Implement sync logic** in repository layers +4. **Add encryption** in production (if needed) +5. **Test migrations** when schema changes +6. **Monitor database size** in production + +### Models to Create: + +Priority 1 (Core): +- UserModel (typeId: 0) +- ProductModel (typeId: 1) +- CartItemModel (typeId: 2) +- OrderModel (typeId: 3) + +Priority 2 (Extended): +- ProjectModel (typeId: 4) +- LoyaltyTransactionModel (typeId: 5) +- AddressModel (typeId: 11) +- NotificationModel (typeId: 15) + +Priority 3 (Additional): +- OrderItemModel (typeId: 10) +- CategoryModel (typeId: 12) +- RewardModel (typeId: 13) +- GiftModel (typeId: 14) +- QuoteModel (typeId: 16) +- PaymentModel (typeId: 17) +- PromotionModel (typeId: 18) +- ReferralModel (typeId: 19) + +## Troubleshooting + +### Build Runner Fails +```bash +# Clean and rebuild +flutter clean +flutter pub get +dart run build_runner clean +dart run build_runner build --delete-conflicting-outputs +``` + +### Box Not Found Error +- Ensure `HiveInitializer.initialize()` is called in main.dart +- Check box name matches `HiveBoxNames` constant + +### Adapter Not Registered +- Run build_runner to generate adapters +- Check `hive_registrar.g.dart` includes the adapter +- Ensure `Hive.registerAdapters()` is called in HiveService + +### Data Corruption +- Enable backups before migrations +- Test migrations on copy of production data +- Validate data before saving + +## Resources + +- Hive CE Documentation: https://github.com/IO-Design-Team/hive_ce +- Project README: `/lib/core/database/README.md` +- Storage Constants: `/lib/core/constants/storage_constants.dart` +- Type Adapter Registry: See README.md for complete list + +## Summary + +The Hive CE database is now fully configured and ready to use. All enum type adapters are created and registered automatically. Future models will follow the same pattern - just create the model file with annotations and run build_runner to generate adapters. + +The database supports offline-first functionality with: +- 13 pre-configured boxes +- 32 reserved type IDs +- Auto-generated adapter registration +- Cache management with expiration +- Offline request queuing +- Sync state tracking +- Maintenance and cleanup + +Start creating models and building the offline-first features! diff --git a/LOCALIZATION.md b/LOCALIZATION.md new file mode 100644 index 0000000..2dcc066 --- /dev/null +++ b/LOCALIZATION.md @@ -0,0 +1,760 @@ +# Localization Guide - Worker Mobile App + +Complete guide for managing translations and localization in the Worker Mobile App. + +## Overview + +The Worker app supports **Vietnamese** (primary) and **English** (secondary) languages with **450+ translation keys** covering all UI elements, messages, and user interactions. + +### Key Features + +- **Primary Language**: Vietnamese (`vi_VN`) +- **Secondary Language**: English (`en_US`) +- **Translation Keys**: 450+ comprehensive translations +- **Auto-generation**: Flutter's `gen-l10n` tool +- **Type Safety**: Fully type-safe localization API +- **Fallback Support**: Automatic fallback to Vietnamese if device locale is unsupported +- **Pluralization**: Full ICU message format support +- **Parameterized Strings**: Support for dynamic values +- **Helper Extensions**: Convenient access utilities +- **Date/Time Formatting**: Locale-specific formatting + +## Configuration + +### l10n.yaml + +```yaml +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-dir: lib/generated/l10n +nullable-getter: false +``` + +### pubspec.yaml + +```yaml +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + +flutter: + generate: true +``` + +## File Structure + +``` +lib/ + l10n/ + app_en.arb # English translations (template) + app_vi.arb # Vietnamese translations (PRIMARY) + generated/l10n/ # Auto-generated (DO NOT EDIT) + app_localizations.dart # Generated base class + app_localizations_en.dart # Generated English implementation + app_localizations_vi.dart # Generated Vietnamese implementation + core/ + utils/ + l10n_extensions.dart # Helper extensions for easy access + +l10n.yaml # Localization configuration +LOCALIZATION.md # This guide +``` + +## Translation Coverage + +### Comprehensive Feature Coverage (450+ Keys) + +| Category | Keys | Examples | +|----------|------|----------| +| **Authentication** | 25+ | login, phone, verifyOTP, enterOTP, resendOTP, register, logout | +| **Navigation** | 10+ | home, products, loyalty, account, more, backToHome, goToHomePage | +| **Common Actions** | 30+ | save, cancel, delete, edit, search, filter, confirm, apply, clear, refresh, share, copy | +| **Status Labels** | 20+ | pending, processing, shipping, completed, cancelled, active, inactive, expired, draft, sent, accepted, rejected | +| **Form Labels** | 30+ | name, email, password, address, street, city, district, ward, postalCode, company, taxId, dateOfBirth, gender | +| **User Types** | 5+ | contractor, architect, distributor, broker, selectUserType | +| **Loyalty System** | 50+ | points, diamond, platinum, gold, rewards, referral, tierBenefits, pointsMultiplier, specialOffers, exclusiveDiscounts | +| **Products & Shopping** | 35+ | product, price, addToCart, cart, checkout, sku, brand, model, specification, availability, newArrival, bestSeller | +| **Cart & Checkout** | 30+ | cartEmpty, updateQuantity, removeFromCart, clearCart, proceedToCheckout, orderSummary, selectAddress, selectPaymentMethod | +| **Orders & Payments** | 40+ | orders, orderNumber, orderStatus, paymentMethod, deliveryAddress, trackOrder, cancelOrder, orderTimeline, trackingNumber | +| **Projects & Quotes** | 45+ | projects, createProject, quotes, budget, progress, client, location, projectPhotos, projectDocuments, quoteItems | +| **Account & Profile** | 40+ | profile, editProfile, addresses, changePassword, uploadAvatar, passwordStrength, enableNotifications, selectLanguage | +| **Loyalty Transactions** | 20+ | transactionType, earnPoints, redeemPoints, bonusPoints, refundPoints, pointsExpiry, disputeTransaction | +| **Gifts & Rewards** | 25+ | myGifts, activeGifts, usedGifts, expiredGifts, giftDetails, rewardCategory, vouchers, pointsCost, expiryDate | +| **Referral Program** | 15+ | referralInvite, referralReward, shareYourCode, friendRegisters, bothGetRewards, totalReferrals | +| **Validation Messages** | 20+ | fieldRequired, invalidEmail, invalidPhone, passwordTooShort, passwordsNotMatch, incorrectPassword | +| **Error Messages** | 15+ | error, networkError, serverError, sessionExpired, notFound, unauthorized, connectionError, syncFailed | +| **Success Messages** | 15+ | success, savedSuccessfully, updatedSuccessfully, deletedSuccessfully, redeemSuccessful, photoUploaded | +| **Loading States** | 10+ | loading, loadingData, processing, pleaseWait, syncInProgress, syncCompleted | +| **Empty States** | 15+ | noData, noResults, noProductsFound, noOrdersYet, noProjectsYet, noNotifications, noGiftsYet | +| **Date & Time** | 20+ | today, yesterday, thisWeek, thisMonth, dateRange, from, to, minutesAgo, hoursAgo, daysAgo, justNow | +| **Notifications** | 25+ | notifications, markAsRead, markAllAsRead, deleteNotification, clearNotifications, unreadNotifications | +| **Chat** | 20+ | chat, sendMessage, typeMessage, typingIndicator, online, offline, messageRead, messageDelivered | +| **Filters & Sorting** | 15+ | filterBy, sortBy, priceAscending, priceDescending, nameAscending, dateAscending, applyFilters | +| **Offline & Sync** | 15+ | offlineMode, syncData, lastSyncAt, noInternetConnection, checkConnection, retryConnection | +| **Miscellaneous** | 20+ | version, help, aboutUs, privacyPolicy, termsOfService, feedback, rateApp, comingSoon, underMaintenance | + +### Special Features + +#### Pluralization Support +- `itemsInCart` - 0/1/many items +- `ordersCount` - 0/1/many orders +- `projectsCount` - 0/1/many projects +- `daysRemaining` - 0/1/many days + +#### Parameterized Translations +- `welcomeTo(appName)` - Dynamic app name +- `otpSentTo(phone)` - Phone number +- `pointsToNextTier(points, tier)` - Points and tier +- `redeemConfirmMessage(points, reward)` - Redemption confirmation +- `orderNumberIs(orderNumber)` - Order number display +- `estimatedDeliveryDate(date)` - Delivery date + +#### Date/Time Formatting +- `formatDate` - DD/MM/YYYY (VI) or MM/DD/YYYY (EN) +- `formatDateTime` - Full date-time with locale +- `minutesAgo`, `hoursAgo`, `daysAgo`, etc. - Relative time + +#### Currency Formatting +- `formatCurrency` - Vietnamese Dong (₫) with proper grouping + +## Usage Examples + +### Basic Usage + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class LoginPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.login), + ), + body: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: l10n.phone, + hintText: l10n.enterPhone, + ), + ), + ElevatedButton( + onPressed: () {}, + child: Text(l10n.continueButton), + ), + ], + ), + ); + } +} +``` + +### Using Extension for Cleaner Code (Recommended) + +```dart +import 'package:worker/core/utils/l10n_extensions.dart'; + +class ProductCard extends StatelessWidget { + @override + Widget build(BuildContext context) { + // Much cleaner than AppLocalizations.of(context)! + return Column( + children: [ + Text(context.l10n.product), + Text(context.l10n.price), + ElevatedButton( + onPressed: () {}, + child: Text(context.l10n.addToCart), + ), + ], + ); + } +} +``` + +### Using Helper Utilities + +```dart +import 'package:worker/core/utils/l10n_extensions.dart'; + +class OrderCard extends StatelessWidget { + final Order order; + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + // Format currency + Text(L10nHelper.formatCurrency(context, order.total)), + // Vietnamese: "1.500.000 ₫" + // English: "1,500,000 ₫" + + // Format date + Text(L10nHelper.formatDate(context, order.createdAt)), + // Vietnamese: "17/10/2025" + // English: "10/17/2025" + + // Relative time + Text(L10nHelper.formatRelativeTime(context, order.createdAt)), + // Vietnamese: "5 phút trước" + // English: "5 minutes ago" + + // Status with helper + Text(L10nHelper.getOrderStatus(context, order.status)), + // Returns localized status string + + // Item count with pluralization + Text(L10nHelper.formatItemCount(context, order.itemCount)), + // Vietnamese: "3 sản phẩm" + // English: "3 items" + ], + ), + ); + } +} +``` + +### Parameterized Translations + +```dart +// Points balance with parameter +final pointsText = context.l10n.pointsBalance; +// Result: "1,000 điểm" (Vietnamese) or "1,000 points" (English) + +// OTP sent message with phone parameter +final message = AppLocalizations.of(context)!.otpSentTo('0912345678'); +// Result: "Mã OTP đã được gửi đến 0912345678" + +// Points to next tier with multiple parameters +final tierMessage = context.l10n.pointsToNextTier; +// Uses placeholders: {points} and {tier} +``` + +### Checking Current Language + +```dart +import 'package:worker/core/utils/l10n_extensions.dart'; + +class LanguageIndicator extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text('Language Code: ${context.languageCode}'), // "vi" or "en" + Text('Is Vietnamese: ${context.isVietnamese}'), // true/false + Text('Is English: ${context.isEnglish}'), // true/false + ], + ); + } +} +``` + +## Adding New Translations + +### Step 1: Add to ARB Files + +**lib/l10n/app_en.arb** (English - Template): +```json +{ + "newFeature": "New Feature", + "@newFeature": { + "description": "Description of the new feature" + } +} +``` + +**lib/l10n/app_vi.arb** (Vietnamese): +```json +{ + "newFeature": "Tính năng mới", + "@newFeature": { + "description": "Description of the new feature" + } +} +``` + +### Step 2: Regenerate Localization Files + +```bash +flutter gen-l10n +``` + +### Step 3: Use in Code + +```dart +Text(context.l10n.newFeature) +``` + +## Parameterized Translations + +### Simple Parameter + +**ARB File:** +```json +{ + "welcome": "Welcome, {name}!", + "@welcome": { + "description": "Welcome message", + "placeholders": { + "name": { + "type": "String", + "example": "John" + } + } + } +} +``` + +**Usage:** +```dart +Text(context.l10n.welcome('John')) +// Result: "Welcome, John!" +``` + +### Multiple Parameters + +**ARB File:** +```json +{ + "orderSummary": "Order #{orderNumber} for {amount}", + "@orderSummary": { + "description": "Order summary text", + "placeholders": { + "orderNumber": { + "type": "String", + "example": "12345" + }, + "amount": { + "type": "String", + "example": "100,000 ₫" + } + } + } +} +``` + +**Usage:** +```dart +Text(context.l10n.orderSummary('12345', '100,000 ₫')) +``` + +### Number Parameters + +**ARB File:** +```json +{ + "itemCount": "{count} items", + "@itemCount": { + "description": "Number of items", + "placeholders": { + "count": { + "type": "int", + "example": "5" + } + } + } +} +``` + +**Usage:** +```dart +Text(context.l10n.itemCount(5)) +// Result: "5 items" +``` + +## Pluralization + +Flutter's localization supports pluralization with the ICU message format: + +**ARB File:** +```json +{ + "itemCountPlural": "{count,plural, =0{No items} =1{1 item} other{{count} items}}", + "@itemCountPlural": { + "description": "Item count with pluralization", + "placeholders": { + "count": { + "type": "int" + } + } + } +} +``` + +**Usage:** +```dart +Text(context.l10n.itemCountPlural(0)) // "No items" +Text(context.l10n.itemCountPlural(1)) // "1 item" +Text(context.l10n.itemCountPlural(5)) // "5 items" +``` + +## Date & Time Formatting + +Use the `intl` package for locale-aware date/time formatting: + +```dart +import 'package:intl/intl.dart'; + +// Format date based on current locale +final now = DateTime.now(); +final locale = Localizations.localeOf(context).toString(); + +// Vietnamese: "17/10/2025" +// English: "10/17/2025" +final dateFormatter = DateFormat.yMd(locale); +final formattedDate = dateFormatter.format(now); + +// Vietnamese: "17 tháng 10, 2025" +// English: "October 17, 2025" +final longDateFormatter = DateFormat.yMMMMd(locale); +final formattedLongDate = longDateFormatter.format(now); +``` + +## Changing Language at Runtime + +### Create Language Provider + +```dart +// lib/core/providers/language_provider.dart +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final languageProvider = StateNotifierProvider((ref) { + return LanguageNotifier(); +}); + +class LanguageNotifier extends StateNotifier { + LanguageNotifier() : super(const Locale('vi', 'VN')) { + _loadSavedLanguage(); + } + + Future _loadSavedLanguage() async { + final prefs = await SharedPreferences.getInstance(); + final languageCode = prefs.getString('language_code') ?? 'vi'; + final countryCode = prefs.getString('country_code') ?? 'VN'; + state = Locale(languageCode, countryCode); + } + + Future setLanguage(Locale locale) async { + state = locale; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('language_code', locale.languageCode); + await prefs.setString('country_code', locale.countryCode ?? ''); + } + + void setVietnamese() => setLanguage(const Locale('vi', 'VN')); + void setEnglish() => setLanguage(const Locale('en', 'US')); +} +``` + +### Update WorkerApp + +```dart +class WorkerApp extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final locale = ref.watch(languageProvider); + + return MaterialApp( + locale: locale, + // ... other configurations + ); + } +} +``` + +### Language Selector Widget + +```dart +class LanguageSelector extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final currentLocale = ref.watch(languageProvider); + + return DropdownButton( + value: currentLocale, + items: const [ + DropdownMenuItem( + value: Locale('vi', 'VN'), + child: Text('Tiếng Việt'), + ), + DropdownMenuItem( + value: Locale('en', 'US'), + child: Text('English'), + ), + ], + onChanged: (locale) { + if (locale != null) { + ref.read(languageProvider.notifier).setLanguage(locale); + } + }, + ); + } +} +``` + +## Best Practices + +### 1. Naming Conventions + +- Use **camelCase** for translation keys +- Be descriptive but concise +- Group related translations with prefixes (e.g., `order*`, `payment*`) +- Avoid abbreviations + +**Good:** +```json +{ + "loginButton": "Login", + "orderNumber": "Order Number", + "paymentMethod": "Payment Method" +} +``` + +**Bad:** +```json +{ + "btn_login": "Login", + "ord_num": "Order Number", + "pay_meth": "Payment Method" +} +``` + +### 2. Reserved Keywords + +Avoid Dart reserved keywords as translation keys: + +- `continue` → Use `continueButton` instead +- `switch` → Use `switchButton` instead +- `class` → Use `className` instead +- `return` → Use `returnButton` instead + +### 3. Context in Descriptions + +Always add `@` descriptions to provide context: + +```json +{ + "save": "Save", + "@save": { + "description": "Button label to save changes" + } +} +``` + +### 4. Consistent Formatting + +Maintain consistent capitalization and punctuation: + +**Vietnamese:** +- Sentence case for labels +- No period at the end of single phrases +- Use full Vietnamese diacritics + +**English:** +- Title Case for buttons and headers +- Sentence case for descriptions +- Consistent use of punctuation + +### 5. Placeholder Examples + +Always provide example values for placeholders: + +```json +{ + "greeting": "Hello, {name}!", + "@greeting": { + "description": "Greeting message", + "placeholders": { + "name": { + "type": "String", + "example": "John" + } + } + } +} +``` + +## Testing Localizations + +### Widget Tests + +```dart +testWidgets('Login page shows Vietnamese translations', (tester) async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('vi', 'VN'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: LoginPage(), + ), + ); + + expect(find.text('Đăng nhập'), findsOneWidget); + expect(find.text('Số điện thoại'), findsOneWidget); +}); + +testWidgets('Login page shows English translations', (tester) async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en', 'US'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: LoginPage(), + ), + ); + + expect(find.text('Login'), findsOneWidget); + expect(find.text('Phone Number'), findsOneWidget); +}); +``` + +### Translation Completeness Test + +```dart +void main() { + test('All Vietnamese translations match English keys', () { + final enFile = File('lib/l10n/app_en.arb'); + final viFile = File('lib/l10n/app_vi.arb'); + + final enJson = jsonDecode(enFile.readAsStringSync()); + final viJson = jsonDecode(viFile.readAsStringSync()); + + final enKeys = enJson.keys.where((k) => !k.startsWith('@')).toList(); + final viKeys = viJson.keys.where((k) => !k.startsWith('@')).toList(); + + expect(enKeys.length, viKeys.length); + for (final key in enKeys) { + expect(viKeys.contains(key), isTrue, reason: 'Missing key: $key'); + } + }); +} +``` + +## Troubleshooting + +### Issue: "AppLocalizations not found" + +**Solution:** Run the code generator: +```bash +flutter gen-l10n +``` + +### Issue: "Duplicate keys in ARB file" + +**Solution:** Each key must be unique within an ARB file. Check for duplicates. + +### Issue: "Invalid placeholder type" + +**Solution:** Supported types are: `String`, `num`, `int`, `double`, `DateTime`, `Object` + +### Issue: "Translations not updating" + +**Solution:** +1. Run `flutter gen-l10n` +2. Hot restart (not hot reload) the app +3. Clear build cache if needed: `flutter clean` + +## Translation Workflow + +### For Developers + +1. **Add English translation** to `app_en.arb` +2. **Add Vietnamese translation** to `app_vi.arb` +3. **Run code generator**: `flutter gen-l10n` +4. **Use in code**: `context.l10n.newKey` +5. **Test both languages** + +### For Translators + +1. **Review** the English ARB file (`app_en.arb`) +2. **Translate** each key to Vietnamese in `app_vi.arb` +3. **Maintain** the same structure and placeholders +4. **Add** `@key` descriptions if needed +5. **Test** context and meaning + +## Resources + +- [Flutter Internationalization](https://docs.flutter.dev/development/accessibility-and-localization/internationalization) +- [ARB File Format](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification) +- [ICU Message Format](https://unicode-org.github.io/icu/userguide/format_parse/messages/) +- [Intl Package](https://pub.dev/packages/intl) + +## Translation Statistics + +- **Total Translation Keys**: 450+ +- **Languages**: 2 (Vietnamese, English) +- **Coverage**: 100% (Both languages fully translated) +- **Parameterized Keys**: 20+ +- **Pluralization Keys**: 10+ +- **Categories**: 26 major categories +- **Helper Functions**: 15+ utility methods + +## Quick Reference Table + +| Category | Key Count | Examples | +|----------|-----------|----------| +| Authentication | 25+ | login, phone, verifyOTP, register, logout | +| Navigation | 10+ | home, products, loyalty, account, more | +| Common Actions | 30+ | save, cancel, delete, edit, search, filter | +| Status Labels | 20+ | pending, completed, active, expired | +| Form Labels | 30+ | name, email, address, company, taxId | +| User Types | 5+ | contractor, architect, distributor, broker | +| Loyalty System | 50+ | points, rewards, referral, tierBenefits | +| Products | 35+ | product, price, cart, sku, brand | +| Cart & Checkout | 30+ | cartEmpty, updateQuantity, orderSummary | +| Orders & Payments | 40+ | orderNumber, payment, trackOrder | +| Projects & Quotes | 45+ | projectName, budget, quotes | +| Account & Profile | 40+ | profile, settings, addresses | +| Loyalty Transactions | 20+ | earnPoints, redeemPoints, bonusPoints | +| Gifts & Rewards | 25+ | myGifts, activeGifts, rewardCategory | +| Referral Program | 15+ | referralInvite, shareYourCode | +| Validation Messages | 20+ | fieldRequired, invalidEmail | +| Error Messages | 15+ | error, networkError, sessionExpired | +| Success Messages | 15+ | success, savedSuccessfully | +| Loading States | 10+ | loading, processing, syncInProgress | +| Empty States | 15+ | noData, noResults, noProductsFound | +| Date & Time | 20+ | today, yesterday, minutesAgo | +| Notifications | 25+ | notifications, markAsRead | +| Chat | 20+ | chat, sendMessage, typingIndicator | +| Filters & Sorting | 15+ | filterBy, sortBy, applyFilters | +| Offline & Sync | 15+ | offlineMode, syncData, lastSyncAt | +| Miscellaneous | 20+ | version, help, feedback, comingSoon | + +--- + +## Summary + +The Worker app localization system provides: + +- **Comprehensive Coverage**: 450+ translation keys across 26 categories +- **Full Bilingual Support**: Vietnamese (primary) and English (secondary) +- **Advanced Features**: Pluralization, parameterization, date/time formatting +- **Developer-Friendly**: Helper extensions and utilities for easy integration +- **Type-Safe**: Flutter's code generation ensures compile-time safety +- **Maintainable**: Clear organization and documentation + +### Key Files + +- `/Users/ssg/project/worker/lib/l10n/app_vi.arb` - Vietnamese translations +- `/Users/ssg/project/worker/lib/l10n/app_en.arb` - English translations +- `/Users/ssg/project/worker/lib/core/utils/l10n_extensions.dart` - Helper utilities +- `/Users/ssg/project/worker/l10n.yaml` - Configuration +- `/Users/ssg/project/worker/LOCALIZATION.md` - This documentation + +--- + +**Last Updated**: October 17, 2025 +**Version**: 1.0.0 +**Languages Supported**: Vietnamese (Primary), English (Secondary) +**Total Translation Keys**: 450+ +**Maintained By**: Worker App Development Team diff --git a/RIVERPOD_SETUP.md b/RIVERPOD_SETUP.md new file mode 100644 index 0000000..5335bd3 --- /dev/null +++ b/RIVERPOD_SETUP.md @@ -0,0 +1,626 @@ +# Riverpod 3.0 Setup - Worker Flutter App + +## Overview + +This document provides a complete guide to the Riverpod 3.0 state management setup for the Worker Flutter app. + +## What's Configured + +### 1. Dependencies (pubspec.yaml) + +**Production Dependencies:** +- `flutter_riverpod: ^3.0.0` - Main Riverpod package +- `riverpod_annotation: ^3.0.0` - Annotations for code generation + +**Development Dependencies:** +- `build_runner: ^2.4.11` - Code generation runner +- `riverpod_generator: ^3.0.0` - Generates provider code from annotations +- `riverpod_lint: ^3.0.0` - Riverpod-specific linting rules +- `custom_lint: ^0.7.0` - Required for riverpod_lint + +### 2. Build Configuration (build.yaml) + +Configured to generate code for: +- `**_provider.dart` files +- Files in `**/providers/` directories +- Files in `**/notifiers/` directories + +### 3. Analysis Options (analysis_options.yaml) + +Configured with: +- Custom lint plugin enabled +- Exclusion of generated files (*.g.dart, *.freezed.dart) +- Riverpod-specific lint rules +- Comprehensive code quality rules + +### 4. App Initialization (main.dart) + +Wrapped with `ProviderScope`: +```dart +void main() { + runApp( + const ProviderScope( + child: MyApp(), + ), + ); +} +``` + +## Directory Structure + +``` +lib/core/providers/ +├── connectivity_provider.dart # Network connectivity monitoring +├── provider_examples.dart # Comprehensive Riverpod 3.0 examples +└── README.md # Provider architecture documentation +``` + +## Quick Start + +### 1. Install Dependencies + +```bash +flutter pub get +``` + +### 2. Generate Provider Code + +```bash +# One-time generation +dart run build_runner build --delete-conflicting-outputs + +# Watch mode (auto-regenerates on file changes) +dart run build_runner watch -d +``` + +### 3. Use the Setup Script + +```bash +chmod +x scripts/setup_riverpod.sh +./scripts/setup_riverpod.sh +``` + +## Core Providers + +### Connectivity Provider + +Location: `/lib/core/providers/connectivity_provider.dart` + +**Purpose:** Monitor network connectivity status across the app. + +**Providers Available:** + +1. **connectivityProvider** - Connectivity instance + ```dart + final connectivity = ref.watch(connectivityProvider); + ``` + +2. **connectivityStreamProvider** - Real-time connectivity stream + ```dart + final status = ref.watch(connectivityStreamProvider); + status.when( + data: (status) => Text('Status: $status'), + loading: () => CircularProgressIndicator(), + error: (e, _) => Text('Error: $e'), + ); + ``` + +3. **currentConnectivityProvider** - One-time connectivity check + ```dart + final status = await ref.read(currentConnectivityProvider.future); + ``` + +4. **isOnlineProvider** - Boolean online/offline stream + ```dart + final isOnline = ref.watch(isOnlineProvider); + ``` + +**Usage Example:** + +```dart +class MyWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final connectivityState = ref.watch(connectivityStreamProvider); + + return connectivityState.when( + data: (status) { + if (status == ConnectivityStatus.offline) { + return OfflineBanner(); + } + return OnlineContent(); + }, + loading: () => LoadingIndicator(), + error: (error, _) => ErrorWidget(error), + ); + } +} +``` + +## Riverpod 3.0 Key Features + +### 1. @riverpod Annotation (Code Generation) + +The modern, recommended approach: + +```dart +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'my_provider.g.dart'; + +// Simple value +@riverpod +String greeting(GreetingRef ref) => 'Hello'; + +// Async value +@riverpod +Future user(UserRef ref, String id) async { + return await fetchUser(id); +} + +// Mutable state +@riverpod +class Counter extends _$Counter { + @override + int build() => 0; + + void increment() => state++; +} +``` + +### 2. Unified Ref Type + +No more separate `FutureProviderRef`, `StreamProviderRef`, etc. - just `Ref`: + +```dart +@riverpod +Future example(ExampleRef ref) async { + ref.watch(provider1); + ref.read(provider2); + ref.listen(provider3, (prev, next) {}); +} +``` + +### 3. Family as Function Parameters + +```dart +// Simple parameter +@riverpod +Future user(UserRef ref, String id) async { + return await fetchUser(id); +} + +// Multiple parameters with named, optional, defaults +@riverpod +Future> posts( + PostsRef ref, { + required String userId, + int page = 1, + int limit = 20, + String? category, +}) async { + return await fetchPosts(userId, page, limit, category); +} + +// Usage +ref.watch(userProvider('user123')); +ref.watch(postsProvider(userId: 'user123', page: 2)); +``` + +### 4. AutoDispose vs KeepAlive + +```dart +// AutoDispose (default) - cleaned up when not watched +@riverpod +String autoExample(AutoExampleRef ref) => 'Auto disposed'; + +// KeepAlive - stays alive until app closes +@Riverpod(keepAlive: true) +String keepExample(KeepExampleRef ref) => 'Kept alive'; +``` + +### 5. ref.mounted Check + +New in Riverpod 3.0 - check if provider is still alive after async operations: + +```dart +@riverpod +class DataManager extends _$DataManager { + @override + String build() => 'Initial'; + + Future updateData() async { + await Future.delayed(Duration(seconds: 2)); + + // Check if provider is still mounted + if (!ref.mounted) return; + + state = 'Updated'; + } +} +``` + +### 6. AsyncValue.guard() for Error Handling + +```dart +@riverpod +class UserProfile extends _$UserProfile { + @override + Future build() async => await fetchUser(); + + Future update(String name) async { + state = const AsyncValue.loading(); + + // AsyncValue.guard catches errors automatically + state = await AsyncValue.guard(() async { + return await updateUser(name); + }); + } +} +``` + +## Provider Patterns + +### 1. Simple Provider (Immutable Value) + +```dart +@riverpod +String appVersion(AppVersionRef ref) => '1.0.0'; + +@riverpod +int pointsMultiplier(PointsMultiplierRef ref) { + final tier = ref.watch(userTierProvider); + return tier == 'diamond' ? 3 : 2; +} +``` + +### 2. FutureProvider (Async Data) + +```dart +@riverpod +Future currentUser(CurrentUserRef ref) async { + final token = await ref.watch(authTokenProvider.future); + return await fetchUser(token); +} +``` + +### 3. StreamProvider (Real-time Data) + +```dart +@riverpod +Stream> chatMessages(ChatMessagesRef ref, String roomId) { + return ref.watch(webSocketProvider).messages(roomId); +} +``` + +### 4. Notifier (Mutable State) + +```dart +@riverpod +class Cart extends _$Cart { + @override + List build() => []; + + void addItem(Product product) { + state = [...state, CartItem.fromProduct(product)]; + } + + void removeItem(String id) { + state = state.where((item) => item.id != id).toList(); + } + + void clear() { + state = []; + } +} +``` + +### 5. AsyncNotifier (Async Mutable State) + +```dart +@riverpod +class UserProfile extends _$UserProfile { + @override + Future build() async { + return await ref.read(userRepositoryProvider).getCurrentUser(); + } + + Future updateName(String name) async { + state = const AsyncValue.loading(); + + state = await AsyncValue.guard(() async { + final updated = await ref.read(userRepositoryProvider).updateName(name); + return updated; + }); + } + + Future refresh() async { + ref.invalidateSelf(); + } +} +``` + +### 6. StreamNotifier (Stream Mutable State) + +```dart +@riverpod +class LiveChat extends _$LiveChat { + @override + Stream> build(String roomId) { + return ref.watch(chatServiceProvider).messagesStream(roomId); + } + + Future sendMessage(String text) async { + await ref.read(chatServiceProvider).send(roomId, text); + } +} +``` + +## Usage in Widgets + +### ConsumerWidget + +```dart +class ProductList extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final products = ref.watch(productsProvider); + + return products.when( + data: (list) => ListView.builder( + itemCount: list.length, + itemBuilder: (context, index) => ProductCard(list[index]), + ), + loading: () => CircularProgressIndicator(), + error: (error, stack) => ErrorView(error), + ); + } +} +``` + +### ConsumerStatefulWidget + +```dart +class OrderPage extends ConsumerStatefulWidget { + @override + ConsumerState createState() => _OrderPageState(); +} + +class _OrderPageState extends ConsumerState { + @override + void initState() { + super.initState(); + // Can use ref in all lifecycle methods + Future.microtask( + () => ref.read(ordersProvider.notifier).loadOrders(), + ); + } + + @override + Widget build(BuildContext context) { + final orders = ref.watch(ordersProvider); + return OrderList(orders); + } +} +``` + +### Consumer (Optimization) + +```dart +Column( + children: [ + const StaticHeader(), + Consumer( + builder: (context, ref, child) { + final count = ref.watch(cartCountProvider); + return CartBadge(count); + }, + ), + ], +) +``` + +## Performance Optimization + +### Use .select() to Watch Specific Fields + +```dart +// Bad - rebuilds on any user change +final user = ref.watch(userProvider); + +// Good - rebuilds only when name changes +final name = ref.watch(userProvider.select((user) => user.name)); + +// Good with AsyncValue +final userName = ref.watch( + userProfileProvider.select((async) => async.value?.name), +); +``` + +### Provider Composition + +```dart +@riverpod +Future dashboard(DashboardRef ref) async { + // Depend on other providers + final user = await ref.watch(userProvider.future); + final stats = await ref.watch(statsProvider.future); + final orders = await ref.watch(recentOrdersProvider.future); + + return Dashboard( + user: user, + stats: stats, + recentOrders: orders, + ); +} +``` + +## Testing + +### Unit Testing Providers + +```dart +test('counter increments', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + expect(container.read(counterProvider), 0); + container.read(counterProvider.notifier).increment(); + expect(container.read(counterProvider), 1); +}); + +test('async provider fetches data', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + final user = await container.read(userProvider.future); + expect(user.name, 'John Doe'); +}); +``` + +### Widget Testing + +```dart +testWidgets('displays user name', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + userProvider.overrideWith((ref) => User(name: 'Test User')), + ], + child: MaterialApp(home: UserScreen()), + ), + ); + + expect(find.text('Test User'), findsOneWidget); +}); +``` + +### Mocking Providers + +```dart +testWidgets('handles loading state', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + userProvider.overrideWith((ref) { + return Future.delayed( + Duration(seconds: 10), + () => User(name: 'Test'), + ); + }), + ], + child: MaterialApp(home: UserScreen()), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); +}); +``` + +## Linting + +### Run Riverpod Lint + +```bash +# Check for Riverpod-specific issues +dart run custom_lint + +# Auto-fix issues +dart run custom_lint --fix +``` + +### Riverpod Lint Rules Enabled + +- `provider_dependencies` - Ensure proper dependency usage +- `scoped_providers_should_specify_dependencies` - Scoped provider safety +- `avoid_public_notifier_properties` - Encapsulation +- `avoid_ref_read_inside_build` - Performance (don't use ref.read in build) +- `avoid_manual_providers_as_generated_provider_dependency` - Use generated providers +- `functional_ref` - Proper ref usage +- `notifier_build` - Proper Notifier implementation + +## Common Issues & Solutions + +### Issue 1: Generated files not found + +**Solution:** +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +### Issue 2: Provider not updating + +**Solution:** Check if you're using `ref.watch()` not `ref.read()` in build method. + +### Issue 3: Memory leaks + +**Solution:** Use autoDispose (default) for providers that should clean up. Only use keepAlive for global state. + +### Issue 4: Too many rebuilds + +**Solution:** Use `.select()` to watch specific fields instead of entire objects. + +## Migration from Riverpod 2.x + +### StateNotifierProvider → Notifier + +```dart +// Old (2.x) +class Counter extends StateNotifier { + Counter() : super(0); + void increment() => state++; +} +final counterProvider = StateNotifierProvider(Counter.new); + +// New (3.0) +@riverpod +class Counter extends _$Counter { + @override + int build() => 0; + void increment() => state++; +} +``` + +### Provider.family → Function Parameters + +```dart +// Old (2.x) +final userProvider = FutureProvider.family((ref, id) async { + return fetchUser(id); +}); + +// New (3.0) +@riverpod +Future user(UserRef ref, String id) async { + return fetchUser(id); +} +``` + +## Examples + +Comprehensive examples are available in: +- `/lib/core/providers/provider_examples.dart` - All Riverpod 3.0 patterns +- `/lib/core/providers/connectivity_provider.dart` - Real-world connectivity monitoring + +## Resources + +- [Riverpod Documentation](https://riverpod.dev) +- [Code Generation Guide](https://riverpod.dev/docs/concepts/about_code_generation) +- [Migration Guide](https://riverpod.dev/docs/migration/from_state_notifier) +- [Provider Examples](./lib/core/providers/provider_examples.dart) + +## Next Steps + +1. Run `flutter pub get` to install dependencies +2. Run `dart run build_runner watch -d` to start code generation +3. Create feature-specific providers in `lib/features/*/presentation/providers/` +4. Follow the patterns in `provider_examples.dart` +5. Use connectivity_provider as a reference for real-world implementation + +## Support + +For questions or issues: +1. Check provider_examples.dart for patterns +2. Review the Riverpod documentation +3. Run custom_lint to catch common mistakes +4. Use ref.watch() in build methods, ref.read() in event handlers diff --git a/RIVERPOD_SUMMARY.md b/RIVERPOD_SUMMARY.md new file mode 100644 index 0000000..970d735 --- /dev/null +++ b/RIVERPOD_SUMMARY.md @@ -0,0 +1,551 @@ +# Riverpod 3.0 Setup Summary - Worker Flutter App + +## ✅ Setup Complete! + +Riverpod 3.0 with code generation has been successfully configured for the Worker Flutter app. + +## What Was Configured + +### 1. Dependencies Updated (pubspec.yaml) + +**Production:** +- `flutter_riverpod: ^3.0.0` - Core Riverpod package for Flutter +- `riverpod_annotation: ^3.0.0` - Annotations for code generation + +**Development:** +- `build_runner: ^2.4.11` - Code generation engine +- `riverpod_generator: ^3.0.0` - Generates provider code +- `riverpod_lint: ^3.0.0` - Riverpod-specific linting +- `custom_lint: ^0.8.0` - Required for riverpod_lint + +### 2. Build Configuration (build.yaml) + +✅ Configured to auto-generate code for: +- `**_provider.dart` files +- `**/providers/*.dart` directories +- `**/notifiers/*.dart` directories + +### 3. Linting (analysis_options.yaml) + +✅ Enabled custom_lint with Riverpod rules: +- `provider_dependencies` - Proper dependency tracking +- `avoid_ref_read_inside_build` - Performance optimization +- `avoid_public_notifier_properties` - Encapsulation +- `functional_ref` - Proper ref usage +- `notifier_build` - Correct Notifier implementation +- And more... + +### 4. App Initialization (main.dart) + +✅ Wrapped with ProviderScope: +```dart +void main() { + runApp( + const ProviderScope( + child: MyApp(), + ), + ); +} +``` + +### 5. Core Providers Created + +#### **connectivity_provider.dart** - Network Monitoring + +Four providers for connectivity management: + +1. **connectivityProvider** - Connectivity instance +2. **connectivityStreamProvider** - Real-time connectivity stream +3. **currentConnectivityProvider** - One-time connectivity check +4. **isOnlineProvider** - Boolean online/offline stream + +**Usage Example:** +```dart +class MyWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final connectivityState = ref.watch(connectivityStreamProvider); + + return connectivityState.when( + data: (status) { + if (status == ConnectivityStatus.offline) { + return OfflineBanner(); + } + return OnlineContent(); + }, + loading: () => CircularProgressIndicator(), + error: (error, _) => ErrorView(error), + ); + } +} +``` + +#### **provider_examples.dart** - Comprehensive Examples + +Complete examples of all Riverpod 3.0 patterns: +- ✅ Simple providers (immutable values) +- ✅ Async providers (FutureProvider pattern) +- ✅ Stream providers +- ✅ Notifier (mutable state with methods) +- ✅ AsyncNotifier (async mutable state) +- ✅ StreamNotifier (stream mutable state) +- ✅ Family (parameters as function arguments) +- ✅ Provider composition +- ✅ AutoDispose vs KeepAlive +- ✅ Lifecycle hooks +- ✅ Error handling with AsyncValue.guard() +- ✅ ref.mounted checks +- ✅ Invalidation and refresh + +### 6. Documentation + +✅ **RIVERPOD_SETUP.md** - Complete setup guide with: +- Installation instructions +- Code generation commands +- Usage patterns and examples +- Testing strategies +- Migration guide from Riverpod 2.x +- Common issues and solutions + +✅ **lib/core/providers/README.md** - Provider architecture documentation + +✅ **scripts/setup_riverpod.sh** - Automated setup script + +## Directory Structure + +``` +lib/core/providers/ +├── connectivity_provider.dart # Network monitoring provider +├── connectivity_provider.g.dart # ✅ Generated code +├── provider_examples.dart # All Riverpod 3.0 patterns +├── provider_examples.g.dart # ✅ Generated code +└── README.md # Architecture docs + +scripts/ +└── setup_riverpod.sh # Automated setup script + +Root files: +├── build.yaml # Build configuration +├── analysis_options.yaml # Linting configuration +├── RIVERPOD_SETUP.md # Complete guide +└── RIVERPOD_SUMMARY.md # This file +``` + +## ✅ Verification + +All code generation completed successfully: +- ✅ connectivity_provider.g.dart generated +- ✅ provider_examples.g.dart generated +- ✅ No Riverpod-related errors in flutter analyze +- ✅ Dependencies installed +- ✅ ProviderScope configured + +## Quick Commands + +### Install Dependencies +```bash +flutter pub get +``` + +### Generate Provider Code +```bash +# One-time generation +dart run build_runner build --delete-conflicting-outputs + +# Watch mode (recommended during development) +dart run build_runner watch -d + +# Clean and rebuild +dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs +``` + +### Run Linting +```bash +# Check for Riverpod issues +dart run custom_lint + +# Auto-fix issues +dart run custom_lint --fix +``` + +### Analyze Code +```bash +flutter analyze +``` + +### Use Setup Script +```bash +./scripts/setup_riverpod.sh +``` + +## Riverpod 3.0 Key Features + +### 1. @riverpod Annotation +```dart +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'my_provider.g.dart'; + +@riverpod +String myValue(MyValueRef ref) => 'Hello'; +``` + +### 2. Family as Function Parameters +```dart +@riverpod +Future user(UserRef ref, String userId) async { + return await fetchUser(userId); +} + +// Usage +ref.watch(userProvider('user123')); +``` + +### 3. Notifier for Mutable State +```dart +@riverpod +class Counter extends _$Counter { + @override + int build() => 0; + + void increment() => state++; +} + +// Usage +ref.watch(counterProvider); // Get state +ref.read(counterProvider.notifier).increment(); // Call method +``` + +### 4. AsyncNotifier for Async Mutable State +```dart +@riverpod +class UserProfile extends _$UserProfile { + @override + Future build() async => await fetchUser(); + + Future update(String name) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + return await updateUser(name); + }); + } +} +``` + +### 5. Unified Ref Type +```dart +// All providers use the same Ref type +@riverpod +Future example(ExampleRef ref) async { + ref.watch(provider1); + ref.read(provider2); + ref.listen(provider3, (prev, next) {}); +} +``` + +### 6. ref.mounted Check +```dart +@riverpod +class Example extends _$Example { + @override + String build() => 'Initial'; + + Future update() async { + await Future.delayed(Duration(seconds: 2)); + + // Check if still mounted + if (!ref.mounted) return; + + state = 'Updated'; + } +} +``` + +## Usage in Widgets + +### ConsumerWidget +```dart +class MyWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### ConsumerStatefulWidget +```dart +class MyWidget extends ConsumerStatefulWidget { + @override + ConsumerState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends ConsumerState { + @override + Widget build(BuildContext context) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### Consumer (for optimization) +```dart +Consumer( + builder: (context, ref, child) { + final count = ref.watch(counterProvider); + return Text('$count'); + }, +) +``` + +## Best Practices + +### ✅ DO + +1. **Use .select() for optimization** + ```dart + final name = ref.watch(userProvider.select((user) => user.name)); + ``` + +2. **Use AsyncValue.guard() for error handling** + ```dart + state = await AsyncValue.guard(() async { + return await api.call(); + }); + ``` + +3. **Check ref.mounted after async operations** + ```dart + await Future.delayed(Duration(seconds: 1)); + if (!ref.mounted) return; + state = newValue; + ``` + +4. **Use autoDispose by default** + ```dart + @riverpod // autoDispose by default + String example(ExampleRef ref) => 'value'; + ``` + +5. **Keep providers in dedicated directories** + ``` + lib/features/auth/presentation/providers/ + lib/features/products/presentation/providers/ + ``` + +### ❌ DON'T + +1. **Don't use ref.read() in build methods** + ```dart + // BAD + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.read(myProvider); // ❌ + return Text(value); + } + + // GOOD + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(myProvider); // ✅ + return Text(value); + } + ``` + +2. **Don't use StateNotifierProvider** (deprecated in Riverpod 3.0) + ```dart + // Use Notifier instead + @riverpod + class Counter extends _$Counter { + @override + int build() => 0; + void increment() => state++; + } + ``` + +3. **Don't forget the part directive** + ```dart + // Required! + part 'my_provider.g.dart'; + ``` + +## Next Steps + +### 1. For Feature Development + +Create providers in feature-specific directories: + +``` +lib/features/auth/presentation/providers/ + ├── auth_provider.dart + ├── auth_provider.g.dart # Generated + ├── login_form_provider.dart + └── login_form_provider.g.dart # Generated +``` + +### 2. Provider Template + +```dart +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'my_provider.g.dart'; + +@riverpod +class MyFeature extends _$MyFeature { + @override + MyState build() { + // Initialize + return MyState.initial(); + } + + void updateState() { + // Modify state + state = state.copyWith(/* ... */); + } +} +``` + +### 3. Run Code Generation + +After creating a provider: +```bash +dart run build_runner watch -d +``` + +### 4. Use in Widgets + +```dart +class MyScreen extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(myFeatureProvider); + + return Column( + children: [ + Text(state.value), + ElevatedButton( + onPressed: () { + ref.read(myFeatureProvider.notifier).updateState(); + }, + child: Text('Update'), + ), + ], + ); + } +} +``` + +## Testing + +### Unit Test Example +```dart +test('counter increments', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + expect(container.read(counterProvider), 0); + container.read(counterProvider.notifier).increment(); + expect(container.read(counterProvider), 1); +}); +``` + +### Widget Test Example +```dart +testWidgets('displays user name', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + userProvider.overrideWith((ref) => User(name: 'Test')), + ], + child: MaterialApp(home: UserScreen()), + ), + ); + + expect(find.text('Test'), findsOneWidget); +}); +``` + +## Examples Reference + +All Riverpod 3.0 patterns are documented with working examples in: +📄 **lib/core/providers/provider_examples.dart** + +This file includes: +- ✅ 11 different provider patterns +- ✅ Real code examples (not pseudocode) +- ✅ Detailed comments explaining each pattern +- ✅ Usage examples in comments +- ✅ Migration notes from Riverpod 2.x + +## Connectivity Provider + +The connectivity provider is a real-world example showing: +- ✅ Simple provider (Connectivity instance) +- ✅ Stream provider (connectivity changes) +- ✅ Future provider (one-time check) +- ✅ Derived provider (isOnline boolean) +- ✅ Proper documentation +- ✅ Usage examples + +Use it as a template for creating your own providers! + +## Resources + +- 📚 [Riverpod Documentation](https://riverpod.dev) +- 📚 [Code Generation Guide](https://riverpod.dev/docs/concepts/about_code_generation) +- 📚 [Migration Guide](https://riverpod.dev/docs/migration/from_state_notifier) +- 📄 [Provider Examples](./lib/core/providers/provider_examples.dart) +- 📄 [Connectivity Provider](./lib/core/providers/connectivity_provider.dart) +- 📄 [Complete Setup Guide](./RIVERPOD_SETUP.md) + +## Support & Help + +If you encounter issues: + +1. **Check examples** in provider_examples.dart +2. **Review documentation** in RIVERPOD_SETUP.md +3. **Run linting** with `dart run custom_lint` +4. **Check generated files** (*.g.dart) exist +5. **Verify part directive** is present in provider files +6. **Ensure ProviderScope** wraps the app in main.dart + +## Common Issues & Solutions + +### Issue: "Target of URI doesn't exist" +**Solution:** Run code generation: +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +### Issue: "Classes can only mix in mixins" +**Solution:** Make sure the part directive is correct: +```dart +part 'my_provider.g.dart'; // Must match filename +``` + +### Issue: Provider not updating +**Solution:** Use ref.watch() in build, ref.read() in callbacks + +### Issue: Too many rebuilds +**Solution:** Use .select() to watch specific fields + +## Conclusion + +✅ Riverpod 3.0 with code generation is fully configured and ready to use! + +**Key Benefits:** +- ✅ Type-safe state management +- ✅ Less boilerplate with code generation +- ✅ Automatic provider type selection +- ✅ Better hot-reload support +- ✅ Comprehensive linting +- ✅ Excellent documentation + +**You can now:** +1. Create providers using @riverpod annotation +2. Use connectivity monitoring immediately +3. Reference provider_examples.dart for patterns +4. Start building feature-specific providers +5. Test providers with ProviderContainer + +**Happy coding! 🚀** diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..71ff5da 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,28 +1,138 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +# Analysis options for Worker Flutter app +# This file configures static analysis and linting -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +analyzer: + # Enable custom_lint for Riverpod linting + plugins: + - custom_lint -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "**/*.config.dart" + - build/** + - lib/generated/** + + errors: + # Treat these as warnings instead of errors + todo: ignore + deprecated_member_use: warning + deprecated_member_use_from_same_package: warning + + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + # Core rules for code quality + - always_declare_return_types + - always_use_package_imports + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_catching_errors + - avoid_dynamic_calls + - avoid_empty_else + - avoid_print + - avoid_relative_lib_imports + - avoid_returning_null_for_void + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + - constant_identifier_names + - curly_braces_in_flow_control_structures + - depend_on_referenced_packages + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + - file_names + - hash_and_equals + - implementation_imports + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - no_duplicate_case_values + - no_logic_in_create_state + - non_constant_identifier_names + - null_closures + - overridden_fields + - package_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_contains + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_function_declarations_over_variables + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - recursive_getters + - require_trailing_commas + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - use_build_context_synchronously + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_rethrow_when_possible + - use_super_parameters + - valid_regexps + - void_checks + +# Custom lint configuration for Riverpod +custom_lint: + rules: + # Riverpod specific rules + - provider_dependencies + - scoped_providers_should_specify_dependencies + - avoid_public_notifier_properties + - avoid_ref_read_inside_build + - avoid_manual_providers_as_generated_provider_dependency + - functional_ref + - notifier_build diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..a59759c --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..38a3adf --- /dev/null +++ b/build.yaml @@ -0,0 +1,47 @@ +# Build configuration for code generation +# This file configures build_runner for Riverpod, Freezed, Hive CE, and JSON serialization + +targets: + $default: + builders: + # Hive CE type adapter generation is automatic - no configuration needed + + # Riverpod code generation + riverpod_generator: + generate_for: + - lib/**_provider.dart + - lib/**/providers/*.dart + - lib/**/notifiers/*.dart + - lib/core/network/*.dart + options: + # Generate providers with proper naming + provider_name_prefix: "" + + # Freezed code generation for immutable models + freezed: + generate_for: + - lib/**_model.dart + - lib/**/models/*.dart + - lib/**/entities/*.dart + options: + union_key: 'type' + union_value_case: 'snake' + + # JSON serialization + json_serializable: + generate_for: + - lib/**_model.dart + - lib/**/models/*.dart + options: + any_map: false + checked: true + create_factory: true + create_to_json: true + disallow_unrecognized_keys: false + explicit_to_json: true + field_rename: snake + generic_argument_factories: true + ignore_unannotated: false + include_if_null: false + +# Global options - removed as runs_before is not supported in this context diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..ced8076 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,145 @@ +PODS: + - connectivity_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleMLKit/BarcodeScanning (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 5.0.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Logger + - GTMSessionFetcher/Core (3.5.0) + - image_picker_ios (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + - MLImage (1.0.0-beta5) + - MLKitBarcodeScanning (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) + - GoogleUtilitiesComponents (~> 1.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - mobile_scanner (5.2.3): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 6.0.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - Flutter (from `Flutter`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + +SPEC REPOS: + trunk: + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GoogleUtilitiesComponents + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + Flutter: + :path: Flutter + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + +SPEC CHECKSUMS: + connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d1fa775..19c6a87 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + AB1F84BC849C548E4DA2D9A4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,14 +42,19 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 91556F9FB5687521C1BD424F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,13 +62,25 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A2165E7BD4BCB2253391F0B0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B234409A1C87269651420659 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 61A54C58DE898B1B550583E8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AB1F84BC849C548E4DA2D9A4 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +113,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + D39C332D04678D8C49EEA401 /* Pods */, + E0C416BADC6D23D3F5D8CCA9 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +142,29 @@ path = Runner; sourceTree = ""; }; + D39C332D04678D8C49EEA401 /* Pods */ = { + isa = PBXGroup; + children = ( + A2165E7BD4BCB2253391F0B0 /* Pods-Runner.debug.xcconfig */, + 91556F9FB5687521C1BD424F /* Pods-Runner.release.xcconfig */, + B234409A1C87269651420659 /* Pods-Runner.profile.xcconfig */, + C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */, + 01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */, + 18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + E0C416BADC6D23D3F5D8CCA9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */, + 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 271526C7C38B821FB8FDADDB /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 61A54C58DE898B1B550583E8 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 6FF008E9F6081D18F1331B43 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 87A368DCCAF54E5C8DB99361 /* [CP] Embed Pods Frameworks */, + 195ED60A171403A63E86B757 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -222,6 +271,45 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 195ED60A171403A63E86B757 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 271526C7C38B821FB8FDADDB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +326,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 6FF008E9F6081D18F1331B43 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 87A368DCCAF54E5C8DB99361 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -379,6 +506,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +524,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +540,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 5994b6a..85759b3 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -28,6 +28,10 @@ LaunchScreen UIMainStoryboardFile Main + NSCameraUsageDescription + This app needs camera access to scan QR codes + NSPhotoLibraryUsageDescription + This app needs photos access to get QR code from photo library UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..22fcc51 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-dir: lib/generated/l10n +nullable-getter: false diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..62ffce8 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,353 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:worker/core/theme/app_theme.dart'; +import 'package:worker/generated/l10n/app_localizations.dart'; + +/// Root application widget for Worker Mobile App +/// +/// This is the main app widget that: +/// - Sets up Material 3 theme configuration +/// - Configures localization for Vietnamese and English +/// - Provides router configuration (to be implemented) +/// - Integrates with Riverpod for state management +/// - Handles app-level error states +class WorkerApp extends ConsumerWidget { + const WorkerApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MaterialApp( + // ==================== App Configuration ==================== + debugShowCheckedModeBanner: false, + title: 'Worker App', + + // ==================== Theme Configuration ==================== + // Material 3 theme with brand colors (Primary Blue: #005B9A) + theme: AppTheme.lightTheme(), + darkTheme: AppTheme.darkTheme(), + themeMode: ThemeMode.light, // TODO: Make this configurable from settings + + // ==================== Localization Configuration ==================== + // Support for Vietnamese (primary) and English (secondary) + localizationsDelegates: const [ + // App-specific localizations + AppLocalizations.delegate, + + // Material Design localizations + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + + // Supported locales + supportedLocales: const [ + Locale('vi', 'VN'), // Vietnamese (primary) + Locale('en', 'US'), // English (secondary) + ], + + // Default locale (Vietnamese) + locale: const Locale('vi', 'VN'), // TODO: Make this configurable from settings + + // Locale resolution strategy + localeResolutionCallback: (locale, supportedLocales) { + // Check if the device locale is supported + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale?.languageCode) { + return supportedLocale; + } + } + + // If device locale is not supported, default to Vietnamese + return const Locale('vi', 'VN'); + }, + + // ==================== Navigation Configuration ==================== + // TODO: Replace with actual router configuration when navigation is implemented + // Options: + // 1. Use go_router for declarative routing + // 2. Use Navigator 2.0 for imperative routing + // 3. Use auto_route for type-safe routing + // + // For now, we use a placeholder home screen + home: const _PlaceholderHomePage(), + + // Alternative: Use onGenerateRoute for custom routing + // onGenerateRoute: (settings) { + // return AppRouter.onGenerateRoute(settings); + // }, + + // ==================== Material App Configuration ==================== + // Builder for additional context-dependent widgets + builder: (context, child) { + return _AppBuilder( + child: child ?? const SizedBox.shrink(), + ); + }, + ); + } +} + +/// App builder widget +/// +/// Wraps the entire app with additional widgets: +/// - Error boundary +/// - Connectivity listener +/// - Global overlays (loading, snackbars) +class _AppBuilder extends ConsumerWidget { + const _AppBuilder({ + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: Add connectivity listener + // final connectivity = ref.watch(connectivityProvider); + + // TODO: Add global loading state + // final isLoading = ref.watch(globalLoadingProvider); + + return Stack( + children: [ + // Main app content + child, + + // TODO: Add global loading overlay + // if (isLoading) + // const _GlobalLoadingOverlay(), + + // TODO: Add connectivity banner + // if (connectivity == ConnectivityStatus.offline) + // const _OfflineBanner(), + ], + ); + } +} + +/// Placeholder home page +/// +/// This is a temporary home screen that will be replaced with the actual +/// home page implementation from features/home/presentation/pages/home_page.dart +/// +/// The actual home page will include: +/// - Membership card display (Diamond/Platinum/Gold tiers) +/// - Quick action grid +/// - Bottom navigation bar +/// - Floating action button for chat +class _PlaceholderHomePage extends ConsumerWidget { + const _PlaceholderHomePage(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Worker App'), + centerTitle: true, + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App logo placeholder + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(24), + ), + child: Icon( + Icons.business_center, + size: 64, + color: theme.colorScheme.onPrimary, + ), + ), + const SizedBox(height: 32), + + // Welcome text + Text( + 'Chào mừng đến với Worker App', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Description + Text( + 'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + // Status indicators + const _StatusIndicator( + icon: Icons.check_circle, + color: Colors.green, + label: 'Hive Database: Initialized', + ), + const SizedBox(height: 12), + const _StatusIndicator( + icon: Icons.check_circle, + color: Colors.green, + label: 'Riverpod: Active', + ), + const SizedBox(height: 12), + const _StatusIndicator( + icon: Icons.check_circle, + color: Colors.green, + label: 'Material 3 Theme: Loaded', + ), + const SizedBox(height: 48), + + // Next steps card + Card( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 12), + Text( + 'Next Steps', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + const _NextStepItem( + number: '1', + text: 'Implement authentication flow', + ), + const SizedBox(height: 8), + const _NextStepItem( + number: '2', + text: 'Create home page with membership cards', + ), + const SizedBox(height: 8), + const _NextStepItem( + number: '3', + text: 'Set up navigation and routing', + ), + const SizedBox(height: 8), + const _NextStepItem( + number: '4', + text: 'Implement feature modules', + ), + ], + ), + ), + ), + ], + ), + ), + ), + + // Floating action button (will be used for chat) + floatingActionButton: FloatingActionButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Chat feature coming soon!'), + behavior: SnackBarBehavior.floating, + ), + ); + }, + child: const Icon(Icons.chat_bubble_outline), + ), + ); + } +} + +/// Status indicator widget +class _StatusIndicator extends StatelessWidget { + const _StatusIndicator({ + required this.icon, + required this.color, + required this.label, + }); + + final IconData icon; + final Color color; + final String label; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 8), + Text( + label, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ); + } +} + +/// Next step item widget +class _NextStepItem extends StatelessWidget { + const _NextStepItem({ + required this.number, + required this.text, + }); + + final String number; + final String text; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + number, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: theme.textTheme.bodyMedium, + ), + ), + ], + ); + } +} diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart new file mode 100644 index 0000000..5bdb58a --- /dev/null +++ b/lib/core/constants/api_constants.dart @@ -0,0 +1,408 @@ +/// API-related constants for the Worker app +/// +/// This file contains all API endpoints, timeouts, and network-related configurations. +/// Base URLs should be configured per environment (dev, staging, production). +class ApiConstants { + // Private constructor to prevent instantiation + ApiConstants._(); + + // ============================================================================ + // Base URLs + // ============================================================================ + + /// Base URL for development environment + static const String devBaseUrl = 'https://dev-api.worker.example.com'; + + /// Base URL for staging environment + static const String stagingBaseUrl = 'https://staging-api.worker.example.com'; + + /// Base URL for production environment + static const String prodBaseUrl = 'https://api.worker.example.com'; + + /// Current base URL (should be configured based on build flavor) + static const String baseUrl = devBaseUrl; // TODO: Configure with flavors + + /// API version prefix + static const String apiVersion = '/v1'; + + /// Full API base URL with version + static String get apiBaseUrl => '$baseUrl$apiVersion'; + + // ============================================================================ + // Timeout Configurations + // ============================================================================ + + /// Connection timeout in milliseconds (30 seconds) + static const Duration connectionTimeout = Duration(milliseconds: 30000); + + /// Receive timeout in milliseconds (30 seconds) + static const Duration receiveTimeout = Duration(milliseconds: 30000); + + /// Send timeout in milliseconds (30 seconds) + static const Duration sendTimeout = Duration(milliseconds: 30000); + + // ============================================================================ + // Retry Configurations + // ============================================================================ + + /// Maximum number of retry attempts for failed requests + static const int maxRetryAttempts = 3; + + /// Initial retry delay in milliseconds + static const Duration initialRetryDelay = Duration(milliseconds: 1000); + + /// Maximum retry delay in milliseconds + static const Duration maxRetryDelay = Duration(milliseconds: 5000); + + /// Retry delay multiplier for exponential backoff + static const double retryDelayMultiplier = 2.0; + + // ============================================================================ + // Cache Configurations + // ============================================================================ + + /// Default cache duration (1 hour) + static const Duration defaultCacheDuration = Duration(hours: 1); + + /// Products cache duration (24 hours) + static const Duration productsCacheDuration = Duration(hours: 24); + + /// Profile cache duration (1 hour) + static const Duration profileCacheDuration = Duration(hours: 1); + + /// Categories cache duration (48 hours) + static const Duration categoriesCacheDuration = Duration(hours: 48); + + /// Maximum cache size in bytes (50 MB) + static const int maxCacheSize = 50 * 1024 * 1024; + + // ============================================================================ + // Request Headers + // ============================================================================ + + /// Content-Type header for JSON requests + static const String contentTypeJson = 'application/json'; + + /// Accept header for JSON responses + static const String acceptJson = 'application/json'; + + /// Accept-Language header for Vietnamese + static const String acceptLanguageVi = 'vi'; + + /// Accept-Language header for English + static const String acceptLanguageEn = 'en'; + + // ============================================================================ + // Authentication Endpoints + // ============================================================================ + + /// Request OTP for phone number login + /// POST /auth/request-otp + /// Body: { "phone": "+84912345678" } + static const String requestOtp = '/auth/request-otp'; + + /// Verify OTP code + /// POST /auth/verify-otp + /// Body: { "phone": "+84912345678", "otp": "123456" } + static const String verifyOtp = '/auth/verify-otp'; + + /// Register new user + /// POST /auth/register + /// Body: { "name": "...", "phone": "...", "email": "...", "userType": "..." } + static const String register = '/auth/register'; + + /// Refresh access token + /// POST /auth/refresh-token + /// Headers: { "Authorization": "Bearer {refreshToken}" } + static const String refreshToken = '/auth/refresh-token'; + + /// Logout user + /// POST /auth/logout + static const String logout = '/auth/logout'; + + /// Get current user profile + /// GET /auth/me + static const String getCurrentUser = '/auth/me'; + + // ============================================================================ + // Loyalty Program Endpoints + // ============================================================================ + + /// Get loyalty points and tier information + /// GET /loyalty/points + static const String getLoyaltyPoints = '/loyalty/points'; + + /// Get loyalty points transaction history + /// GET /loyalty/transactions?page={page}&limit={limit} + static const String getPointsHistory = '/loyalty/transactions'; + + /// Get available rewards for redemption + /// GET /loyalty/rewards?category={category} + static const String getRewards = '/loyalty/rewards'; + + /// Redeem a reward + /// POST /loyalty/rewards/{rewardId}/redeem + static const String redeemReward = '/loyalty/rewards'; + + /// Get user's redeemed gifts + /// GET /loyalty/gifts?status={active|used|expired} + static const String getGifts = '/loyalty/gifts'; + + /// Get referral information + /// GET /loyalty/referral + static const String getReferralInfo = '/loyalty/referral'; + + /// Share referral link + /// POST /loyalty/referral/share + /// Body: { "method": "whatsapp|telegram|sms" } + static const String shareReferral = '/loyalty/referral/share'; + + // ============================================================================ + // Product Endpoints + // ============================================================================ + + /// Get all products with pagination + /// GET /products?page={page}&limit={limit}&category={categoryId} + static const String getProducts = '/products'; + + /// Get product details by ID + /// GET /products/{productId} + static const String getProductDetails = '/products'; + + /// Search products + /// GET /products/search?q={query}&page={page}&limit={limit} + static const String searchProducts = '/products/search'; + + /// Get product categories + /// GET /categories + static const String getCategories = '/categories'; + + /// Get products by category + /// GET /categories/{categoryId}/products + static const String getProductsByCategory = '/categories'; + + // ============================================================================ + // Order Endpoints + // ============================================================================ + + /// Create new order + /// POST /orders + /// Body: { "items": [...], "deliveryAddress": {...}, "paymentMethod": "..." } + static const String createOrder = '/orders'; + + /// Get user's orders + /// GET /orders?status={status}&page={page}&limit={limit} + static const String getOrders = '/orders'; + + /// Get order details by ID + /// GET /orders/{orderId} + static const String getOrderDetails = '/orders'; + + /// Cancel order + /// POST /orders/{orderId}/cancel + static const String cancelOrder = '/orders'; + + /// Get payment transactions + /// GET /payments?page={page}&limit={limit} + static const String getPayments = '/payments'; + + /// Get payment details + /// GET /payments/{paymentId} + static const String getPaymentDetails = '/payments'; + + // ============================================================================ + // Project Endpoints + // ============================================================================ + + /// Create new project + /// POST /projects + static const String createProject = '/projects'; + + /// Get user's projects + /// GET /projects?status={status}&page={page}&limit={limit} + static const String getProjects = '/projects'; + + /// Get project details by ID + /// GET /projects/{projectId} + static const String getProjectDetails = '/projects'; + + /// Update project + /// PUT /projects/{projectId} + static const String updateProject = '/projects'; + + /// Update project progress + /// PATCH /projects/{projectId}/progress + /// Body: { "progress": 75 } + static const String updateProjectProgress = '/projects'; + + /// Delete project + /// DELETE /projects/{projectId} + static const String deleteProject = '/projects'; + + // ============================================================================ + // Quote Endpoints + // ============================================================================ + + /// Create new quote + /// POST /quotes + static const String createQuote = '/quotes'; + + /// Get user's quotes + /// GET /quotes?status={status}&page={page}&limit={limit} + static const String getQuotes = '/quotes'; + + /// Get quote details by ID + /// GET /quotes/{quoteId} + static const String getQuoteDetails = '/quotes'; + + /// Update quote + /// PUT /quotes/{quoteId} + static const String updateQuote = '/quotes'; + + /// Send quote to client + /// POST /quotes/{quoteId}/send + /// Body: { "email": "client@example.com" } + static const String sendQuote = '/quotes'; + + /// Convert quote to order + /// POST /quotes/{quoteId}/convert + static const String convertQuoteToOrder = '/quotes'; + + // ============================================================================ + // Chat Endpoints + // ============================================================================ + + /// WebSocket endpoint for real-time chat + static const String chatWebSocket = '/ws/chat'; + + /// Get chat messages + /// GET /chat/messages?roomId={roomId}&before={messageId}&limit={limit} + static const String getChatMessages = '/chat/messages'; + + /// Send chat message + /// POST /chat/messages + /// Body: { "roomId": "...", "text": "...", "attachments": [...] } + static const String sendChatMessage = '/chat/messages'; + + /// Mark messages as read + /// POST /chat/messages/read + /// Body: { "messageIds": [...] } + static const String markMessagesAsRead = '/chat/messages/read'; + + // ============================================================================ + // Account & Profile Endpoints + // ============================================================================ + + /// Get user profile + /// GET /profile + static const String getProfile = '/profile'; + + /// Update user profile + /// PUT /profile + static const String updateProfile = '/profile'; + + /// Upload avatar + /// POST /profile/avatar + /// Form-data: { "avatar": File } + static const String uploadAvatar = '/profile/avatar'; + + /// Change password + /// POST /profile/change-password + /// Body: { "currentPassword": "...", "newPassword": "..." } + static const String changePassword = '/profile/change-password'; + + /// Get user addresses + /// GET /addresses + static const String getAddresses = '/addresses'; + + /// Add new address + /// POST /addresses + static const String addAddress = '/addresses'; + + /// Update address + /// PUT /addresses/{addressId} + static const String updateAddress = '/addresses'; + + /// Delete address + /// DELETE /addresses/{addressId} + static const String deleteAddress = '/addresses'; + + /// Set default address + /// POST /addresses/{addressId}/set-default + static const String setDefaultAddress = '/addresses'; + + // ============================================================================ + // Promotion Endpoints + // ============================================================================ + + /// Get active promotions + /// GET /promotions?category={category} + static const String getPromotions = '/promotions'; + + /// Get promotion details + /// GET /promotions/{promotionId} + static const String getPromotionDetails = '/promotions'; + + /// Claim promotion + /// POST /promotions/{promotionId}/claim + static const String claimPromotion = '/promotions'; + + // ============================================================================ + // Notification Endpoints + // ============================================================================ + + /// Get notifications + /// GET /notifications?type={type}&page={page}&limit={limit} + static const String getNotifications = '/notifications'; + + /// Mark notification as read + /// POST /notifications/{notificationId}/read + static const String markNotificationAsRead = '/notifications'; + + /// Mark all notifications as read + /// POST /notifications/read-all + static const String markAllNotificationsAsRead = '/notifications/read-all'; + + /// Clear all notifications + /// DELETE /notifications + static const String clearAllNotifications = '/notifications'; + + /// Register FCM token for push notifications + /// POST /notifications/fcm-token + /// Body: { "token": "..." } + static const String registerFcmToken = '/notifications/fcm-token'; + + // ============================================================================ + // Helper Methods + // ============================================================================ + + /// Build full URL for endpoint + /// + /// Example: + /// ```dart + /// final url = ApiConstants.buildUrl('/products', {'page': '1', 'limit': '20'}); + /// // Returns: https://api.worker.example.com/v1/products?page=1&limit=20 + /// ``` + static String buildUrl(String endpoint, [Map? queryParams]) { + final uri = Uri.parse('$apiBaseUrl$endpoint'); + if (queryParams != null && queryParams.isNotEmpty) { + return uri.replace(queryParameters: queryParams).toString(); + } + return uri.toString(); + } + + /// Build URL with path parameters + /// + /// Example: + /// ```dart + /// final url = ApiConstants.buildUrlWithParams('/products/{id}', {'id': '123'}); + /// // Returns: https://api.worker.example.com/v1/products/123 + /// ``` + static String buildUrlWithParams(String endpoint, Map params) { + String url = endpoint; + params.forEach((key, value) { + url = url.replaceAll('{$key}', value); + }); + return '$apiBaseUrl$url'; + } +} diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..05c7efc --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,521 @@ +/// Application-level constants and configurations +/// +/// This file contains app metadata, loyalty tier definitions, pagination settings, +/// and other application-wide configuration values. +library; + +// ============================================================================ +// Loyalty Member Tiers +// ============================================================================ + +/// Membership tier levels in the loyalty program +/// +/// Ordered from lowest to highest: +/// - [MemberTier.gold]: Entry level (0-999 points) +/// - [MemberTier.platinum]: Mid level (1000-4999 points) +/// - [MemberTier.diamond]: Premium level (5000+ points) +enum MemberTier { + /// Gold tier - Entry level membership + /// Requirements: 0-999 points + /// Benefits: 1x points multiplier, basic discounts + gold, + + /// Platinum tier - Mid level membership + /// Requirements: 1000-4999 points + /// Benefits: 1.5x points multiplier, priority support, special offers + platinum, + + /// Diamond tier - Premium membership + /// Requirements: 5000+ points + /// Benefits: 2x points multiplier, exclusive rewards, VIP support, early access + diamond, +} + +/// Extension methods for MemberTier enum +extension MemberTierExtension on MemberTier { + /// Get display name for the tier + String get displayName { + switch (this) { + case MemberTier.gold: + return 'Gold'; + case MemberTier.platinum: + return 'Platinum'; + case MemberTier.diamond: + return 'Diamond'; + } + } + + /// Get Vietnamese display name + String get displayNameVi { + switch (this) { + case MemberTier.gold: + return 'Vàng'; + case MemberTier.platinum: + return 'Bạc'; + case MemberTier.diamond: + return 'Kim Cương'; + } + } + + /// Get points multiplier for earning rewards + double get pointsMultiplier { + switch (this) { + case MemberTier.gold: + return 1.0; + case MemberTier.platinum: + return 1.5; + case MemberTier.diamond: + return 2.0; + } + } + + /// Get minimum points required for this tier + int get minPoints { + switch (this) { + case MemberTier.gold: + return 0; + case MemberTier.platinum: + return 1000; + case MemberTier.diamond: + return 5000; + } + } + + /// Get maximum points for this tier (null for diamond = unlimited) + int? get maxPoints { + switch (this) { + case MemberTier.gold: + return 999; + case MemberTier.platinum: + return 4999; + case MemberTier.diamond: + return null; // Unlimited + } + } + + /// Get next tier (null if already at highest tier) + MemberTier? get nextTier { + switch (this) { + case MemberTier.gold: + return MemberTier.platinum; + case MemberTier.platinum: + return MemberTier.diamond; + case MemberTier.diamond: + return null; // Already at top + } + } + + /// Get tier from points value + static MemberTier fromPoints(int points) { + if (points >= MemberTier.diamond.minPoints) { + return MemberTier.diamond; + } else if (points >= MemberTier.platinum.minPoints) { + return MemberTier.platinum; + } else { + return MemberTier.gold; + } + } +} + +// ============================================================================ +// User Types +// ============================================================================ + +/// Types of users in the Worker app +enum UserType { + /// Contractor - Construction project managers (Thầu thợ) + contractor, + + /// Architect - Design professionals (Kiến trúc sư) + architect, + + /// Distributor - Product resellers (Đại lý phân phối) + distributor, + + /// Broker - Real estate and construction brokers (Môi giới) + broker, +} + +/// Extension methods for UserType enum +extension UserTypeExtension on UserType { + /// Get display name + String get displayName { + switch (this) { + case UserType.contractor: + return 'Contractor'; + case UserType.architect: + return 'Architect'; + case UserType.distributor: + return 'Distributor'; + case UserType.broker: + return 'Broker'; + } + } + + /// Get Vietnamese display name + String get displayNameVi { + switch (this) { + case UserType.contractor: + return 'Thầu thợ'; + case UserType.architect: + return 'Kiến trúc sư'; + case UserType.distributor: + return 'Đại lý phân phối'; + case UserType.broker: + return 'Môi giới'; + } + } +} + +// ============================================================================ +// Order Status +// ============================================================================ + +/// Order lifecycle status +enum OrderStatus { + /// Order placed, awaiting processing + pending, + + /// Order is being prepared + processing, + + /// Order is out for delivery + shipping, + + /// Order delivered successfully + completed, + + /// Order cancelled by user or system + cancelled, +} + +/// Extension methods for OrderStatus enum +extension OrderStatusExtension on OrderStatus { + String get displayName { + switch (this) { + case OrderStatus.pending: + return 'Pending'; + case OrderStatus.processing: + return 'Processing'; + case OrderStatus.shipping: + return 'Shipping'; + case OrderStatus.completed: + return 'Completed'; + case OrderStatus.cancelled: + return 'Cancelled'; + } + } + + String get displayNameVi { + switch (this) { + case OrderStatus.pending: + return 'Chờ xử lý'; + case OrderStatus.processing: + return 'Đang xử lý'; + case OrderStatus.shipping: + return 'Đang giao'; + case OrderStatus.completed: + return 'Hoàn thành'; + case OrderStatus.cancelled: + return 'Đã hủy'; + } + } +} + +// ============================================================================ +// Project Status +// ============================================================================ + +/// Construction project lifecycle status +enum ProjectStatus { + /// Project in planning phase + planning, + + /// Project actively in progress + inProgress, + + /// Project completed + completed, + + /// Project on hold + onHold, + + /// Project cancelled + cancelled, +} + +extension ProjectStatusExtension on ProjectStatus { + String get displayName { + switch (this) { + case ProjectStatus.planning: + return 'Planning'; + case ProjectStatus.inProgress: + return 'In Progress'; + case ProjectStatus.completed: + return 'Completed'; + case ProjectStatus.onHold: + return 'On Hold'; + case ProjectStatus.cancelled: + return 'Cancelled'; + } + } + + String get displayNameVi { + switch (this) { + case ProjectStatus.planning: + return 'Lên kế hoạch'; + case ProjectStatus.inProgress: + return 'Đang thực hiện'; + case ProjectStatus.completed: + return 'Hoàn thành'; + case ProjectStatus.onHold: + return 'Tạm dừng'; + case ProjectStatus.cancelled: + return 'Đã hủy'; + } + } +} + +// ============================================================================ +// Project Types +// ============================================================================ + +/// Types of construction projects +enum ProjectType { + /// Residential construction + residential, + + /// Commercial construction + commercial, + + /// Industrial construction + industrial, +} + +extension ProjectTypeExtension on ProjectType { + String get displayName { + switch (this) { + case ProjectType.residential: + return 'Residential'; + case ProjectType.commercial: + return 'Commercial'; + case ProjectType.industrial: + return 'Industrial'; + } + } + + String get displayNameVi { + switch (this) { + case ProjectType.residential: + return 'Dân dụng'; + case ProjectType.commercial: + return 'Thương mại'; + case ProjectType.industrial: + return 'Công nghiệp'; + } + } +} + +// ============================================================================ +// App Metadata +// ============================================================================ + +/// Application constants +class AppConstants { + // Private constructor to prevent instantiation + AppConstants._(); + + /// Application name + static const String appName = 'Worker'; + + /// Full application name + static const String appFullName = 'Worker - EuroTile & Vasta Stone'; + + /// Application version + static const String appVersion = '1.0.0'; + + /// Build number + static const int buildNumber = 1; + + /// Company name + static const String companyName = 'EuroTile & Vasta Stone'; + + /// Support email + static const String supportEmail = 'support@worker.example.com'; + + /// Support phone number (Vietnamese format) + static const String supportPhone = '1900 xxxx'; + + /// Website URL + static const String websiteUrl = 'https://worker.example.com'; + + // ============================================================================ + // Pagination Settings + // ============================================================================ + + /// Default page size for paginated lists + static const int defaultPageSize = 20; + + /// Products page size + static const int productsPageSize = 20; + + /// Orders page size + static const int ordersPageSize = 10; + + /// Projects page size + static const int projectsPageSize = 15; + + /// Notifications page size + static const int notificationsPageSize = 25; + + /// Points history page size + static const int pointsHistoryPageSize = 20; + + /// Maximum items to load at once + static const int maxPageSize = 100; + + // ============================================================================ + // Cache Settings + // ============================================================================ + + /// Cache duration for products (in hours) + static const int productsCacheDuration = 24; + + /// Cache duration for user profile (in hours) + static const int profileCacheDuration = 1; + + /// Cache duration for categories (in hours) + static const int categoriesCacheDuration = 48; + + /// Maximum cache size (in MB) + static const int maxCacheSize = 100; + + // ============================================================================ + // OTP Settings + // ============================================================================ + + /// OTP code length (6 digits) + static const int otpLength = 6; + + /// OTP resend cooldown (in seconds) + static const int otpResendCooldown = 60; + + /// OTP validity duration (in minutes) + static const int otpValidityMinutes = 5; + + // ============================================================================ + // Referral Settings + // ============================================================================ + + /// Points earned per successful referral + static const int pointsPerReferral = 100; + + /// Points earned by the referred user on signup + static const int welcomeBonusPoints = 50; + + // ============================================================================ + // Order Settings + // ============================================================================ + + /// Minimum order amount (in VND) + static const double minOrderAmount = 100000; // 100,000 VND + + /// Free shipping threshold (in VND) + static const double freeShippingThreshold = 1000000; // 1,000,000 VND + + /// Standard shipping fee (in VND) + static const double standardShippingFee = 30000; // 30,000 VND + + /// Maximum items per order + static const int maxItemsPerOrder = 50; + + // ============================================================================ + // Image Settings + // ============================================================================ + + /// Maximum avatar size (in MB) + static const int maxAvatarSize = 5; + + /// Maximum product image size (in MB) + static const int maxProductImageSize = 3; + + /// Supported image formats + static const List supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp']; + + /// Image quality for compression (0-100) + static const int imageQuality = 85; + + // ============================================================================ + // Search Settings + // ============================================================================ + + /// Minimum search query length + static const int minSearchLength = 2; + + /// Search debounce delay (in milliseconds) + static const int searchDebounceMs = 500; + + /// Maximum search results + static const int maxSearchResults = 50; + + // ============================================================================ + // Date & Time Settings + // ============================================================================ + + /// Default date format (Vietnamese: dd/MM/yyyy) + static const String dateFormat = 'dd/MM/yyyy'; + + /// Date time format + static const String dateTimeFormat = 'dd/MM/yyyy HH:mm'; + + /// Time format (24-hour) + static const String timeFormat = 'HH:mm'; + + /// Full date time format + static const String fullDateTimeFormat = 'EEEE, dd MMMM yyyy HH:mm'; + + // ============================================================================ + // Validation Settings + // ============================================================================ + + /// Minimum password length + static const int minPasswordLength = 8; + + /// Maximum password length + static const int maxPasswordLength = 50; + + /// Minimum name length + static const int minNameLength = 2; + + /// Maximum name length + static const int maxNameLength = 100; + + /// Vietnamese phone number regex pattern + /// Matches: 0912345678, +84912345678, 84912345678 + static const String phoneRegexPattern = r'^(0|\+84|84)[3|5|7|8|9][0-9]{8}$'; + + /// Email regex pattern + static const String emailRegexPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'; + + // ============================================================================ + // Feature Flags + // ============================================================================ + + /// Enable dark mode + static const bool enableDarkMode = true; + + /// Enable biometric authentication + static const bool enableBiometric = true; + + /// Enable push notifications + static const bool enablePushNotifications = true; + + /// Enable offline mode + static const bool enableOfflineMode = true; + + /// Enable analytics + static const bool enableAnalytics = true; + + /// Enable crash reporting + static const bool enableCrashReporting = true; +} diff --git a/lib/core/constants/storage_constants.dart b/lib/core/constants/storage_constants.dart new file mode 100644 index 0000000..21e0bf7 --- /dev/null +++ b/lib/core/constants/storage_constants.dart @@ -0,0 +1,211 @@ +/// Storage constants for Hive CE (Community Edition) database +/// +/// This file contains all box names, keys, and type IDs used throughout the app +/// for local data persistence and offline-first functionality. +library; + +/// Hive Box Names +/// +/// These are the names of Hive boxes used in the application. +/// Each box stores a specific type of data for organized storage. +class HiveBoxNames { + // Private constructor to prevent instantiation + HiveBoxNames._(); + + /// User authentication and profile data + static const String userBox = 'user_box'; + + /// Product catalog and details cache + static const String productBox = 'product_box'; + + /// Shopping cart items + static const String cartBox = 'cart_box'; + + /// Order history and details + static const String orderBox = 'order_box'; + + /// Construction projects + static const String projectBox = 'project_box'; + + /// Loyalty program transactions and points history + static const String loyaltyBox = 'loyalty_box'; + + /// Rewards and gifts catalog + static const String rewardsBox = 'rewards_box'; + + /// User settings and preferences + static const String settingsBox = 'settings_box'; + + /// API response cache for offline access + static const String cacheBox = 'cache_box'; + + /// Data sync state tracking + static const String syncStateBox = 'sync_state_box'; + + /// Notifications + static const String notificationBox = 'notification_box'; + + /// Address book + static const String addressBox = 'address_box'; + + /// Offline request queue for failed API calls + static const String offlineQueueBox = 'offline_queue_box'; + + /// Get all box names for initialization + static List get allBoxes => [ + userBox, + productBox, + cartBox, + orderBox, + projectBox, + loyaltyBox, + rewardsBox, + settingsBox, + cacheBox, + syncStateBox, + notificationBox, + addressBox, + offlineQueueBox, + ]; +} + +/// Hive Type Adapter IDs +/// +/// Type IDs must be unique across the application. +/// Range 0-223 is reserved for user-defined types. +/// IMPORTANT: Never change these IDs once assigned, as it will break existing data. +class HiveTypeIds { + // Private constructor to prevent instantiation + HiveTypeIds._(); + + // Core Models (0-9) + static const int user = 0; + static const int product = 1; + static const int cartItem = 2; + static const int order = 3; + static const int project = 4; + static const int loyaltyTransaction = 5; + + // Extended Models (10-19) + static const int orderItem = 10; + static const int address = 11; + static const int category = 12; + static const int reward = 13; + static const int gift = 14; + static const int notification = 15; + static const int quote = 16; + static const int payment = 17; + static const int promotion = 18; + static const int referral = 19; + + // Enums (20-29) + static const int memberTier = 20; + static const int userType = 21; + static const int orderStatus = 22; + static const int projectStatus = 23; + static const int projectType = 24; + static const int transactionType = 25; + static const int giftStatus = 26; + static const int paymentStatus = 27; + static const int notificationType = 28; + static const int paymentMethod = 29; + + // Cache & Sync Models (30-39) + static const int cachedData = 30; + static const int syncState = 31; + static const int offlineRequest = 32; +} + +/// Hive Storage Keys +/// +/// Keys used to store and retrieve data from Hive boxes. +class HiveKeys { + // Private constructor to prevent instantiation + HiveKeys._(); + + // User Box Keys + static const String currentUser = 'current_user'; + static const String authToken = 'auth_token'; + static const String refreshToken = 'refresh_token'; + static const String isLoggedIn = 'is_logged_in'; + + // Settings Box Keys + static const String languageCode = 'language_code'; + static const String themeMode = 'theme_mode'; + static const String notificationsEnabled = 'notifications_enabled'; + static const String lastSyncTime = 'last_sync_time'; + static const String schemaVersion = 'schema_version'; + static const String encryptionEnabled = 'encryption_enabled'; + + // Cache Box Keys + static const String productsCacheKey = 'products_cache'; + static const String categoriesCacheKey = 'categories_cache'; + static const String promotionsCacheKey = 'promotions_cache'; + static const String loyaltyPointsCacheKey = 'loyalty_points_cache'; + static const String rewardsCacheKey = 'rewards_cache'; + + // Sync State Box Keys + static const String productsSyncTime = 'products_sync_time'; + static const String ordersSyncTime = 'orders_sync_time'; + static const String projectsSyncTime = 'projects_sync_time'; + static const String loyaltySyncTime = 'loyalty_sync_time'; + static const String lastFullSyncTime = 'last_full_sync_time'; + + // App State Keys + static const String firstLaunch = 'first_launch'; + static const String onboardingCompleted = 'onboarding_completed'; + static const String appVersion = 'app_version'; +} + +/// Cache Duration Constants +/// +/// Default cache expiration durations for different data types. +class CacheDuration { + // Private constructor to prevent instantiation + CacheDuration._(); + + /// Product data cache (6 hours) + static const Duration products = Duration(hours: 6); + + /// Category data cache (24 hours) + static const Duration categories = Duration(hours: 24); + + /// Loyalty points cache (1 hour) + static const Duration loyaltyPoints = Duration(hours: 1); + + /// Rewards cache (12 hours) + static const Duration rewards = Duration(hours: 12); + + /// Promotions cache (2 hours) + static const Duration promotions = Duration(hours: 2); + + /// User profile cache (30 minutes) + static const Duration userProfile = Duration(minutes: 30); + + /// Order history cache (5 minutes) + static const Duration orderHistory = Duration(minutes: 5); + + /// Projects cache (10 minutes) + static const Duration projects = Duration(minutes: 10); +} + +/// Database Configuration +class HiveDatabaseConfig { + // Private constructor to prevent instantiation + HiveDatabaseConfig._(); + + /// Current schema version for migrations + static const int currentSchemaVersion = 1; + + /// Maximum cache size in MB + static const int maxCacheSizeMB = 100; + + /// Maximum number of items in offline queue + static const int maxOfflineQueueSize = 100; + + /// Enable encryption for sensitive data + static const bool enableEncryption = false; // Set to true in production + + /// Compaction threshold (compact when box size grows by this percentage) + static const double compactionThreshold = 0.3; // 30% +} diff --git a/lib/core/constants/ui_constants.dart b/lib/core/constants/ui_constants.dart new file mode 100644 index 0000000..4e0d935 --- /dev/null +++ b/lib/core/constants/ui_constants.dart @@ -0,0 +1,467 @@ +/// UI Constants for the Worker App +/// +/// Contains spacing, sizes, border radius, elevation values, and other +/// UI-related constants used throughout the app. +library; + +import 'package:flutter/material.dart'; + +/// Spacing constants following Material Design 8dp grid system +class AppSpacing { + AppSpacing._(); + + /// Extra small spacing: 4dp + static const double xs = 4.0; + + /// Small spacing: 8dp + static const double sm = 8.0; + + /// Medium spacing: 16dp + static const double md = 16.0; + + /// Large spacing: 24dp + static const double lg = 24.0; + + /// Extra large spacing: 32dp + static const double xl = 32.0; + + /// Extra extra large spacing: 48dp + static const double xxl = 48.0; +} + +/// Border radius constants +class AppRadius { + AppRadius._(); + + /// Small radius: 4dp + static const double sm = 4.0; + + /// Medium radius: 8dp + static const double md = 8.0; + + /// Large radius: 12dp + static const double lg = 12.0; + + /// Extra large radius: 16dp + static const double xl = 16.0; + + /// Card radius: 12dp + static const double card = 12.0; + + /// Button radius: 8dp + static const double button = 8.0; + + /// Input field radius: 8dp + static const double input = 8.0; + + /// Member card radius: 16dp + static const double memberCard = 16.0; + + /// Circular radius for avatars and badges + static const double circular = 9999.0; +} + +/// Elevation constants for Material Design +class AppElevation { + AppElevation._(); + + /// No elevation + static const double none = 0.0; + + /// Low elevation: 2dp + static const double low = 2.0; + + /// Medium elevation: 4dp + static const double medium = 4.0; + + /// High elevation: 8dp + static const double high = 8.0; + + /// Card elevation: 2dp + static const double card = 2.0; + + /// Button elevation: 2dp + static const double button = 2.0; + + /// FAB elevation: 6dp + static const double fab = 6.0; + + /// Member card elevation: 8dp + static const double memberCard = 8.0; +} + +/// Icon size constants +class AppIconSize { + AppIconSize._(); + + /// Extra small icon: 16dp + static const double xs = 16.0; + + /// Small icon: 20dp + static const double sm = 20.0; + + /// Medium icon: 24dp + static const double md = 24.0; + + /// Large icon: 32dp + static const double lg = 32.0; + + /// Extra large icon: 48dp + static const double xl = 48.0; +} + +/// App bar specifications +class AppBarSpecs { + AppBarSpecs._(); + + /// Standard app bar height + static const double height = 56.0; + + /// App bar elevation + static const double elevation = 0.0; + + /// App bar icon size + static const double iconSize = AppIconSize.md; +} + +/// Bottom navigation bar specifications +class BottomNavSpecs { + BottomNavSpecs._(); + + /// Bottom nav bar height + static const double height = 72.0; + + /// Icon size for unselected state + static const double iconSize = 24.0; + + /// Icon size for selected state + static const double selectedIconSize = 28.0; + + /// Label font size + static const double labelFontSize = 12.0; + + /// Bottom nav bar elevation + static const double elevation = 8.0; +} + +/// Floating Action Button specifications +class FABSpecs { + FABSpecs._(); + + /// FAB size + static const double size = 56.0; + + /// FAB elevation + static const double elevation = 6.0; + + /// FAB icon size + static const double iconSize = 24.0; + + /// FAB position from bottom-right + static const Offset position = Offset(16, 16); +} + +/// Member card specifications +class MemberCardSpecs { + MemberCardSpecs._(); + + /// Card width (full width) + static const double width = double.infinity; + + /// Card height + static const double height = 200.0; + + /// Border radius + static const double borderRadius = AppRadius.memberCard; + + /// Card elevation + static const double elevation = AppElevation.memberCard; + + /// Card padding + static const EdgeInsets padding = EdgeInsets.all(20.0); + + /// QR code size + static const double qrSize = 80.0; + + /// QR code background size + static const double qrBackgroundSize = 90.0; + + /// Points display font size + static const double pointsFontSize = 28.0; + + /// Points display font weight + static const FontWeight pointsFontWeight = FontWeight.bold; + + /// Member ID font size + static const double memberIdFontSize = 14.0; + + /// Member name font size + static const double memberNameFontSize = 18.0; +} + +/// Button specifications +class ButtonSpecs { + ButtonSpecs._(); + + /// Button height + static const double height = 48.0; + + /// Button minimum width + static const double minWidth = 120.0; + + /// Button border radius + static const double borderRadius = AppRadius.button; + + /// Button elevation + static const double elevation = AppElevation.button; + + /// Button padding + static const EdgeInsets padding = EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.md, + ); + + /// Button font size + static const double fontSize = 16.0; + + /// Button font weight + static const FontWeight fontWeight = FontWeight.w600; +} + +/// Input field specifications +class InputFieldSpecs { + InputFieldSpecs._(); + + /// Input field height + static const double height = 56.0; + + /// Input field border radius + static const double borderRadius = AppRadius.input; + + /// Input field content padding + static const EdgeInsets contentPadding = EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.md, + ); + + /// Input field font size + static const double fontSize = 16.0; + + /// Label font size + static const double labelFontSize = 14.0; + + /// Hint font size + static const double hintFontSize = 14.0; +} + +/// Card specifications +class CardSpecs { + CardSpecs._(); + + /// Card border radius + static const double borderRadius = AppRadius.card; + + /// Card elevation + static const double elevation = AppElevation.card; + + /// Card padding + static const EdgeInsets padding = EdgeInsets.all(AppSpacing.md); + + /// Card margin + static const EdgeInsets margin = EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ); +} + +/// Product card specifications +class ProductCardSpecs { + ProductCardSpecs._(); + + /// Product image aspect ratio (width / height) + static const double imageAspectRatio = 1.0; + + /// Product card border radius + static const double borderRadius = AppRadius.card; + + /// Product card elevation + static const double elevation = AppElevation.card; + + /// Product card padding + static const EdgeInsets padding = EdgeInsets.all(AppSpacing.sm); + + /// Product name max lines + static const int nameMaxLines = 2; + + /// Product name font size + static const double nameFontSize = 14.0; + + /// Product price font size + static const double priceFontSize = 16.0; + + /// Product price font weight + static const FontWeight priceFontWeight = FontWeight.bold; +} + +/// Order card specifications +class OrderCardSpecs { + OrderCardSpecs._(); + + /// Order number font size + static const double orderNumberFontSize = 16.0; + + /// Order number font weight + static const FontWeight orderNumberFontWeight = FontWeight.w600; + + /// Order date font size + static const double dateFontSize = 12.0; + + /// Order total font size + static const double totalFontSize = 18.0; + + /// Order total font weight + static const FontWeight totalFontWeight = FontWeight.bold; +} + +/// Status badge specifications +class StatusBadgeSpecs { + StatusBadgeSpecs._(); + + /// Badge height + static const double height = 24.0; + + /// Badge border radius + static const double borderRadius = 12.0; + + /// Badge padding + static const EdgeInsets padding = EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: 2.0, + ); + + /// Badge font size + static const double fontSize = 11.0; + + /// Badge font weight + static const FontWeight fontWeight = FontWeight.w600; +} + +/// Avatar specifications +class AvatarSpecs { + AvatarSpecs._(); + + /// Small avatar size + static const double sm = 32.0; + + /// Medium avatar size + static const double md = 48.0; + + /// Large avatar size + static const double lg = 64.0; + + /// Extra large avatar size + static const double xl = 96.0; +} + +/// Animation durations +class AppDuration { + AppDuration._(); + + /// Short animation: 200ms + static const Duration short = Duration(milliseconds: 200); + + /// Medium animation: 300ms + static const Duration medium = Duration(milliseconds: 300); + + /// Long animation: 500ms + static const Duration long = Duration(milliseconds: 500); + + /// Page transition duration + static const Duration pageTransition = medium; + + /// Fade in duration + static const Duration fadeIn = medium; + + /// Shimmer animation duration + static const Duration shimmer = Duration(milliseconds: 1500); +} + +/// Grid specifications +class GridSpecs { + GridSpecs._(); + + /// Product grid cross axis count (columns) + static const int productGridColumns = 2; + + /// Product grid cross axis spacing + static const double productGridCrossSpacing = AppSpacing.md; + + /// Product grid main axis spacing + static const double productGridMainSpacing = AppSpacing.md; + + /// Quick action grid cross axis count + static const int quickActionColumns = 3; + + /// Quick action grid cross axis spacing + static const double quickActionCrossSpacing = AppSpacing.md; + + /// Quick action grid main axis spacing + static const double quickActionMainSpacing = AppSpacing.md; +} + +/// List specifications +class ListSpecs { + ListSpecs._(); + + /// List item padding + static const EdgeInsets itemPadding = EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ); + + /// List item divider height + static const double dividerHeight = 1.0; + + /// List item divider indent + static const double dividerIndent = AppSpacing.md; +} + +/// Image specifications +class ImageSpecs { + ImageSpecs._(); + + /// Product image cache width + static const int productImageCacheWidth = 400; + + /// Product image cache height + static const int productImageCacheHeight = 400; + + /// Avatar image cache width + static const int avatarImageCacheWidth = 200; + + /// Avatar image cache height + static const int avatarImageCacheHeight = 200; + + /// Banner image cache width + static const int bannerImageCacheWidth = 800; + + /// Banner image cache height + static const int bannerImageCacheHeight = 400; +} + +/// Screen breakpoints for responsive design +class Breakpoints { + Breakpoints._(); + + /// Small screen (phone) + static const double sm = 600.0; + + /// Medium screen (tablet) + static const double md = 960.0; + + /// Large screen (desktop) + static const double lg = 1280.0; + + /// Extra large screen + static const double xl = 1920.0; +} diff --git a/lib/core/database/QUICK_START.md b/lib/core/database/QUICK_START.md new file mode 100644 index 0000000..8782097 --- /dev/null +++ b/lib/core/database/QUICK_START.md @@ -0,0 +1,119 @@ +# Hive CE Quick Start Guide + +## 1. Initialize in main.dart + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'core/database/hive_initializer.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive + await HiveInitializer.initialize(verbose: true); + + runApp(const ProviderScope(child: MyApp())); +} +``` + +## 2. Save & Retrieve Data + +```dart +import 'package:worker/core/database/database.dart'; + +final dbManager = DatabaseManager(); + +// Save +await dbManager.save( + boxName: HiveBoxNames.productBox, + key: 'product_123', + value: product, +); + +// Get +final product = dbManager.get( + boxName: HiveBoxNames.productBox, + key: 'product_123', +); +``` + +## 3. Cache with Expiration + +```dart +// Save to cache +await dbManager.saveToCache( + key: HiveKeys.productsCacheKey, + data: products, +); + +// Get from cache +final cached = dbManager.getFromCache>( + key: HiveKeys.productsCacheKey, + maxAge: CacheDuration.products, // 6 hours +); + +if (cached == null) { + // Cache expired - fetch fresh data +} +``` + +## 4. Create New Model + +```dart +import 'package:hive_ce/hive.dart'; +import 'package:worker/core/constants/storage_constants.dart'; + +part 'product_model.g.dart'; + +@HiveType(typeId: HiveTypeIds.product) +class ProductModel extends HiveObject { + @HiveField(0) + final String id; + + @HiveField(1) + final String name; + + ProductModel({required this.id, required this.name}); +} +``` + +Then run: +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +## 5. Logout (Clear User Data) + +```dart +await HiveInitializer.logout(); +``` + +## Available Boxes + +- `HiveBoxNames.userBox` - User profile +- `HiveBoxNames.productBox` - Products +- `HiveBoxNames.cartBox` - Cart items +- `HiveBoxNames.orderBox` - Orders +- `HiveBoxNames.projectBox` - Projects +- `HiveBoxNames.loyaltyBox` - Loyalty data +- `HiveBoxNames.settingsBox` - Settings +- `HiveBoxNames.cacheBox` - API cache +- `HiveBoxNames.notificationBox` - Notifications + +See `/lib/core/constants/storage_constants.dart` for complete list. + +## Cache Durations + +Pre-configured expiration times: +- `CacheDuration.products` - 6 hours +- `CacheDuration.categories` - 24 hours +- `CacheDuration.loyaltyPoints` - 1 hour +- `CacheDuration.rewards` - 12 hours +- `CacheDuration.promotions` - 2 hours + +## Need More Info? + +- Full Documentation: `/lib/core/database/README.md` +- Setup Summary: `/HIVE_SETUP.md` +- Storage Constants: `/lib/core/constants/storage_constants.dart` diff --git a/lib/core/database/README.md b/lib/core/database/README.md new file mode 100644 index 0000000..327644c --- /dev/null +++ b/lib/core/database/README.md @@ -0,0 +1,478 @@ +# Hive CE Database Setup + +This directory contains the Hive CE (Community Edition) database configuration and services for the Worker Flutter app. + +## Overview + +The app uses Hive CE for offline-first local data persistence. Hive is a lightweight, fast NoSQL database written in pure Dart, perfect for Flutter applications. + +## Key Features + +- **Offline-First**: All data is stored locally and synced with the backend +- **Fast Performance**: Hive is optimized for speed with minimal overhead +- **Type-Safe**: Uses type adapters for strong typing +- **Encryption Support**: Optional AES encryption for sensitive data +- **Auto-Compaction**: Automatic database maintenance and cleanup +- **Migration Support**: Built-in schema versioning and migrations + +## File Structure + +``` +lib/core/database/ +├── README.md # This file +├── hive_service.dart # Main Hive initialization and lifecycle management +├── database_manager.dart # High-level database operations +└── models/ + ├── cached_data.dart # Generic cache wrapper model + └── enums.dart # All enum type adapters +``` + +## Setup Instructions + +### 1. Install Dependencies + +The required packages are already in `pubspec.yaml`: + +```yaml +dependencies: + hive_ce: ^2.6.0 + hive_ce_flutter: ^2.1.0 + +dev_dependencies: + hive_ce_generator: ^1.6.0 + build_runner: ^2.4.11 +``` + +Run: +```bash +flutter pub get +``` + +### 2. Generate Type Adapters + +After creating Hive models with `@HiveType` annotations, run: + +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +Or for continuous watching during development: +```bash +dart run build_runner watch --delete-conflicting-outputs +``` + +### 3. Initialize Hive in main.dart + +```dart +import 'package:flutter/material.dart'; +import 'core/database/hive_service.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive + final hiveService = HiveService(); + await hiveService.initialize(); + + runApp(const MyApp()); +} +``` + +## Creating Hive Models + +### Basic Model Example + +```dart +import 'package:hive_ce/hive.dart'; +import '../../constants/storage_constants.dart'; + +part 'user_model.g.dart'; // Generated file + +@HiveType(typeId: HiveTypeIds.user) +class UserModel extends HiveObject { + @HiveField(0) + final String id; + + @HiveField(1) + final String name; + + @HiveField(2) + final String email; + + UserModel({ + required this.id, + required this.name, + required this.email, + }); +} +``` + +### Enum Example + +```dart +@HiveType(typeId: HiveTypeIds.memberTier) +enum MemberTier { + @HiveField(0) + gold, + + @HiveField(1) + platinum, + + @HiveField(2) + diamond, +} +``` + +### Important Rules + +1. **Type IDs must be unique** across the entire app (0-223 for user types) +2. **Never change field numbers** once assigned - it will break existing data +3. **Use `part` directive** to include generated adapter file +4. **Extend HiveObject** for model classes (optional but recommended for auto-save) +5. **Register adapters** before opening boxes (handled by HiveService) + +## Box Management + +### Available Boxes + +The app uses these pre-configured boxes (see `storage_constants.dart`): + +- `user_box` - User profile and auth data (encrypted) +- `product_box` - Product catalog cache +- `cart_box` - Shopping cart items (encrypted) +- `order_box` - Order history (encrypted) +- `project_box` - Construction projects (encrypted) +- `loyalty_box` - Loyalty transactions (encrypted) +- `rewards_box` - Rewards catalog +- `settings_box` - App settings +- `cache_box` - Generic API cache +- `sync_state_box` - Sync timestamps +- `notification_box` - Notifications +- `address_box` - Delivery addresses (encrypted) +- `offline_queue_box` - Failed API requests queue (encrypted) + +### Using Boxes + +```dart +// Using DatabaseManager (recommended) +final dbManager = DatabaseManager(); + +// Save data +await dbManager.save( + boxName: HiveBoxNames.productBox, + key: 'product_123', + value: product, +); + +// Get data +final product = dbManager.get( + boxName: HiveBoxNames.productBox, + key: 'product_123', +); + +// Get all +final products = dbManager.getAll(boxName: HiveBoxNames.productBox); +``` + +## Caching Strategy + +### Save to Cache + +```dart +final dbManager = DatabaseManager(); + +await dbManager.saveToCache( + key: HiveKeys.productsCacheKey, + data: products, +); +``` + +### Get from Cache + +```dart +final products = dbManager.getFromCache>( + key: HiveKeys.productsCacheKey, + maxAge: CacheDuration.products, // 6 hours +); + +if (products == null) { + // Cache miss or expired - fetch from API + final freshProducts = await api.getProducts(); + await dbManager.saveToCache( + key: HiveKeys.productsCacheKey, + data: freshProducts, + ); +} +``` + +### Check Cache Validity + +```dart +final isValid = dbManager.isCacheValid( + key: HiveKeys.productsCacheKey, + maxAge: CacheDuration.products, +); + +if (!isValid) { + // Refresh cache +} +``` + +## Offline Queue + +Handle failed API requests when offline: + +```dart +// Add to queue when API call fails +await dbManager.addToOfflineQueue({ + 'endpoint': '/api/orders', + 'method': 'POST', + 'body': orderData, +}); + +// Process queue when back online +final queue = dbManager.getOfflineQueue(); +for (var i = 0; i < queue.length; i++) { + try { + await api.request(queue[i]); + await dbManager.removeFromOfflineQueue(i); + } catch (e) { + // Keep in queue for next retry + } +} +``` + +## Data Synchronization + +Track sync state for different data types: + +```dart +// Update sync timestamp +await dbManager.updateSyncTime(HiveKeys.productsSyncTime); + +// Get last sync time +final lastSync = dbManager.getLastSyncTime(HiveKeys.productsSyncTime); + +// Check if needs sync +final needsSync = dbManager.needsSync( + dataType: HiveKeys.productsSyncTime, + syncInterval: Duration(hours: 6), +); +``` + +## Encryption + +Enable encryption for sensitive data in `storage_constants.dart`: + +```dart +class HiveDatabaseConfig { + static const bool enableEncryption = true; +} +``` + +Generate and store encryption key securely: + +```dart +// Generate key +final encryptionKey = HiveService.generateEncryptionKey(); + +// Store securely using flutter_secure_storage +final secureStorage = FlutterSecureStorage(); +await secureStorage.write( + key: 'hive_encryption_key', + value: base64Encode(encryptionKey), +); + +// Initialize with key +final storedKey = await secureStorage.read(key: 'hive_encryption_key'); +await hiveService.initialize( + encryptionKey: base64Decode(storedKey!), +); +``` + +## Migrations + +Handle schema changes: + +```dart +// In hive_service.dart, add migration logic: + +Future _migrateToVersion(int version) async { + switch (version) { + case 2: + await _migrateV1ToV2(); + break; + } +} + +Future _migrateV1ToV2() async { + // Example: Add new field to existing data + final userBox = Hive.box(HiveBoxNames.userBox); + + for (var key in userBox.keys) { + final user = userBox.get(key); + // Update user data structure + await userBox.put(key, updatedUser); + } +} +``` + +## Database Maintenance + +### Clear Expired Cache + +```dart +await dbManager.clearExpiredCache(); +``` + +### Compact Boxes + +```dart +final hiveService = HiveService(); +// Compaction happens automatically during initialization +``` + +### Clear User Data (Logout) + +```dart +await hiveService.clearUserData(); +``` + +### Clear All Data + +```dart +await hiveService.clearAllData(); +``` + +### Get Statistics + +```dart +final stats = dbManager.getStatistics(); +dbManager.printStatistics(); +``` + +## Best Practices + +1. **Always initialize Hive before using any boxes** +2. **Use DatabaseManager for common operations** +3. **Cache frequently accessed data** +4. **Set appropriate cache expiration times** +5. **Handle errors gracefully** - Hive operations can fail +6. **Use transactions for multiple related updates** +7. **Compact boxes periodically** for optimal performance +8. **Never store large files in Hive** - use file system instead +9. **Test migrations thoroughly** before release +10. **Monitor database size** in production + +## Debugging + +### Print Box Contents + +```dart +final box = Hive.box(HiveBoxNames.productBox); +print('Box length: ${box.length}'); +print('Keys: ${box.keys}'); +print('Values: ${box.values}'); +``` + +### Check Box Location + +```dart +print('Hive path: ${Hive.box(HiveBoxNames.settingsBox).path}'); +``` + +### View Statistics + +```dart +DatabaseManager().printStatistics(); +``` + +## Troubleshooting + +### "Box not found" Error +- Ensure Hive is initialized before accessing boxes +- Check that box name is correct + +### "TypeAdapter not registered" Error +- Run `build_runner` to generate adapters +- Ensure adapter is registered in `HiveService._registerTypeAdapters()` + +### "Cannot write null values" Error +- Make fields nullable with `?` or provide default values +- Check that HiveField annotations are correct + +### Data Corruption +- Enable backup/restore functionality +- Implement data validation before saving +- Use try-catch blocks around Hive operations + +## Testing + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_flutter/hive_flutter.dart'; + +void main() { + setUp(() async { + await Hive.initFlutter(); + // Register test adapters + }); + + tearDown(() async { + await Hive.deleteFromDisk(); + }); + + test('Save and retrieve user', () async { + final box = await Hive.openBox('test_box'); + await box.put('user', UserModel(id: '1', name: 'Test')); + + final user = box.get('user'); + expect(user.name, 'Test'); + }); +} +``` + +## Resources + +- [Hive CE Documentation](https://github.com/IO-Design-Team/hive_ce) +- [Original Hive Documentation](https://docs.hivedb.dev/) +- [Flutter Offline-First Best Practices](https://flutter.dev/docs/cookbook/persistence) + +## Type Adapter Registry + +### Registered Type IDs (0-223) + +| Type ID | Model | Status | +|---------|-------|--------| +| 0 | UserModel | TODO | +| 1 | ProductModel | TODO | +| 2 | CartItemModel | TODO | +| 3 | OrderModel | TODO | +| 4 | ProjectModel | TODO | +| 5 | LoyaltyTransactionModel | TODO | +| 10 | OrderItemModel | TODO | +| 11 | AddressModel | TODO | +| 12 | CategoryModel | TODO | +| 13 | RewardModel | TODO | +| 14 | GiftModel | TODO | +| 15 | NotificationModel | TODO | +| 16 | QuoteModel | TODO | +| 17 | PaymentModel | TODO | +| 18 | PromotionModel | TODO | +| 19 | ReferralModel | TODO | +| 20 | MemberTier (enum) | Created | +| 21 | UserType (enum) | Created | +| 22 | OrderStatus (enum) | Created | +| 23 | ProjectStatus (enum) | Created | +| 24 | ProjectType (enum) | Created | +| 25 | TransactionType (enum) | Created | +| 26 | GiftStatus (enum) | Created | +| 27 | PaymentStatus (enum) | Created | +| 28 | NotificationType (enum) | Created | +| 29 | PaymentMethod (enum) | Created | +| 30 | CachedData | Created | +| 31 | SyncState | TODO | +| 32 | OfflineRequest | TODO | + +**IMPORTANT**: Never reuse or change these type IDs once assigned! diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart new file mode 100644 index 0000000..325be9d --- /dev/null +++ b/lib/core/database/database.dart @@ -0,0 +1,25 @@ +/// Hive CE Database Export +/// +/// This file provides a convenient way to import all database-related +/// classes and utilities in a single import. +/// +/// Usage: +/// ```dart +/// import 'package:worker/core/database/database.dart'; +/// ``` +library; + +// Constants +export 'package:worker/core/constants/storage_constants.dart'; + +// Services +export 'package:worker/core/database/database_manager.dart'; +export 'package:worker/core/database/hive_initializer.dart'; +export 'package:worker/core/database/hive_service.dart'; + +// Models +export 'package:worker/core/database/models/cached_data.dart'; +export 'package:worker/core/database/models/enums.dart'; + +// Auto-generated registrar +export 'package:worker/hive_registrar.g.dart'; diff --git a/lib/core/database/database_manager.dart b/lib/core/database/database_manager.dart new file mode 100644 index 0000000..fd10462 --- /dev/null +++ b/lib/core/database/database_manager.dart @@ -0,0 +1,411 @@ +import 'package:flutter/foundation.dart'; +import 'package:hive_ce/hive.dart'; + +import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/core/database/hive_service.dart'; + +/// Database Manager for common Hive operations +/// +/// Provides high-level database operations and utilities for working +/// with Hive boxes across the application. +/// +/// Features: +/// - CRUD operations with error handling +/// - Cache management with expiration +/// - Bulk operations +/// - Data validation +/// - Sync state tracking +class DatabaseManager { + DatabaseManager({HiveService? hiveService}) + : _hiveService = hiveService ?? HiveService(); + + final HiveService _hiveService; + + /// Get a box safely + Box _getBox(String boxName) { + if (!_hiveService.isBoxOpen(boxName)) { + throw HiveError('Box $boxName is not open. Initialize HiveService first.'); + } + return _hiveService.getBox(boxName); + } + + // ==================== Generic CRUD Operations ==================== + + /// Save a value to a box + Future save({ + required String boxName, + required String key, + required T value, + }) async { + try { + final box = _getBox(boxName); + await box.put(key, value); + debugPrint('DatabaseManager: Saved $key to $boxName'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error saving $key to $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Get a value from a box + T? get({ + required String boxName, + required String key, + T? defaultValue, + }) { + try { + final box = _getBox(boxName); + return box.get(key, defaultValue: defaultValue); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error getting $key from $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + return defaultValue; + } + } + + /// Delete a value from a box + Future delete({ + required String boxName, + required String key, + }) async { + try { + final box = _getBox(boxName); + await box.delete(key); + debugPrint('DatabaseManager: Deleted $key from $boxName'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error deleting $key from $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Check if a key exists in a box + bool exists({ + required String boxName, + required String key, + }) { + try { + final box = _getBox(boxName); + return box.containsKey(key); + } catch (e) { + debugPrint('DatabaseManager: Error checking $key in $boxName: $e'); + return false; + } + } + + /// Get all values from a box + List getAll({required String boxName}) { + try { + final box = _getBox(boxName); + return box.values.toList(); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error getting all from $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + return []; + } + } + + /// Save multiple values to a box + Future saveAll({ + required String boxName, + required Map entries, + }) async { + try { + final box = _getBox(boxName); + await box.putAll(entries); + debugPrint('DatabaseManager: Saved ${entries.length} items to $boxName'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error saving all to $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Clear all data from a box + Future clearBox({required String boxName}) async { + try { + final box = _getBox(boxName); + await box.clear(); + debugPrint('DatabaseManager: Cleared $boxName'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error clearing $boxName: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + // ==================== Cache Operations ==================== + + /// Save data to cache with timestamp + Future saveToCache({ + required String key, + required T data, + }) async { + try { + final cacheBox = _getBox(HiveBoxNames.cacheBox); + await cacheBox.put(key, { + 'data': data, + 'timestamp': DateTime.now().toIso8601String(), + }); + debugPrint('DatabaseManager: Cached $key'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error caching $key: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Get data from cache + /// + /// Returns null if cache is expired or doesn't exist + T? getFromCache({ + required String key, + Duration? maxAge, + }) { + try { + final cacheBox = _getBox(HiveBoxNames.cacheBox); + final cachedData = cacheBox.get(key) as Map?; + + if (cachedData == null) { + debugPrint('DatabaseManager: Cache miss for $key'); + return null; + } + + // Check if cache is expired + if (maxAge != null) { + final timestamp = DateTime.parse(cachedData['timestamp'] as String); + final age = DateTime.now().difference(timestamp); + + if (age > maxAge) { + debugPrint('DatabaseManager: Cache expired for $key (age: $age)'); + return null; + } + } + + debugPrint('DatabaseManager: Cache hit for $key'); + return cachedData['data'] as T?; + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error getting cache $key: $e'); + debugPrint('StackTrace: $stackTrace'); + return null; + } + } + + /// Check if cache is valid (exists and not expired) + bool isCacheValid({ + required String key, + Duration? maxAge, + }) { + try { + final cacheBox = _getBox(HiveBoxNames.cacheBox); + final cachedData = cacheBox.get(key) as Map?; + + if (cachedData == null) return false; + + if (maxAge != null) { + final timestamp = DateTime.parse(cachedData['timestamp'] as String); + final age = DateTime.now().difference(timestamp); + return age <= maxAge; + } + + return true; + } catch (e) { + debugPrint('DatabaseManager: Error checking cache validity $key: $e'); + return false; + } + } + + /// Clear expired cache entries + Future clearExpiredCache() async { + try { + final cacheBox = _getBox(HiveBoxNames.cacheBox); + final keysToDelete = []; + + for (final key in cacheBox.keys) { + final cachedData = cacheBox.get(key) as Map?; + if (cachedData != null) { + try { + final timestamp = DateTime.parse(cachedData['timestamp'] as String); + final age = DateTime.now().difference(timestamp); + + // Use default max age of 24 hours + if (age > const Duration(hours: 24)) { + keysToDelete.add(key as String); + } + } catch (e) { + // Invalid cache entry, mark for deletion + keysToDelete.add(key as String); + } + } + } + + for (final key in keysToDelete) { + await cacheBox.delete(key); + } + + debugPrint('DatabaseManager: Cleared ${keysToDelete.length} expired cache entries'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error clearing expired cache: $e'); + debugPrint('StackTrace: $stackTrace'); + } + } + + // ==================== Sync State Operations ==================== + + /// Update sync timestamp for a data type + Future updateSyncTime(String dataType) async { + try { + final syncBox = _getBox(HiveBoxNames.syncStateBox); + await syncBox.put(dataType, DateTime.now().toIso8601String()); + debugPrint('DatabaseManager: Updated sync time for $dataType'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error updating sync time for $dataType: $e'); + debugPrint('StackTrace: $stackTrace'); + } + } + + /// Get last sync time for a data type + DateTime? getLastSyncTime(String dataType) { + try { + final syncBox = _getBox(HiveBoxNames.syncStateBox); + final timestamp = syncBox.get(dataType); + + if (timestamp == null) return null; + + return DateTime.parse(timestamp as String); + } catch (e) { + debugPrint('DatabaseManager: Error getting sync time for $dataType: $e'); + return null; + } + } + + /// Check if data needs sync + bool needsSync({ + required String dataType, + required Duration syncInterval, + }) { + final lastSync = getLastSyncTime(dataType); + + if (lastSync == null) return true; + + final timeSinceSync = DateTime.now().difference(lastSync); + return timeSinceSync > syncInterval; + } + + // ==================== Settings Operations ==================== + + /// Save a setting + Future saveSetting({ + required String key, + required T value, + }) async { + await save( + boxName: HiveBoxNames.settingsBox, + key: key, + value: value, + ); + } + + /// Get a setting + T? getSetting({ + required String key, + T? defaultValue, + }) { + return get( + boxName: HiveBoxNames.settingsBox, + key: key, + defaultValue: defaultValue, + ); + } + + // ==================== Offline Queue Operations ==================== + + /// Add request to offline queue + Future addToOfflineQueue(Map request) async { + try { + final queueBox = _getBox(HiveBoxNames.offlineQueueBox); + + // Check queue size limit + if (queueBox.length >= HiveDatabaseConfig.maxOfflineQueueSize) { + debugPrint('DatabaseManager: Offline queue is full, removing oldest item'); + await queueBox.deleteAt(0); + } + + await queueBox.add({ + ...request, + 'timestamp': DateTime.now().toIso8601String(), + }); + + debugPrint('DatabaseManager: Added request to offline queue'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error adding to offline queue: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Get all offline queue items + List> getOfflineQueue() { + try { + final queueBox = _getBox(HiveBoxNames.offlineQueueBox); + return queueBox.values + .map((e) => Map.from(e as Map)) + .toList(); + } catch (e) { + debugPrint('DatabaseManager: Error getting offline queue: $e'); + return []; + } + } + + /// Remove item from offline queue + Future removeFromOfflineQueue(int index) async { + try { + final queueBox = _getBox(HiveBoxNames.offlineQueueBox); + await queueBox.deleteAt(index); + debugPrint('DatabaseManager: Removed item $index from offline queue'); + } catch (e, stackTrace) { + debugPrint('DatabaseManager: Error removing from offline queue: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Clear offline queue + Future clearOfflineQueue() async { + await clearBox(boxName: HiveBoxNames.offlineQueueBox); + } + + // ==================== Statistics ==================== + + /// Get database statistics + Map getStatistics() { + final stats = {}; + + for (final boxName in HiveBoxNames.allBoxes) { + try { + if (_hiveService.isBoxOpen(boxName)) { + final box = _getBox(boxName); + stats[boxName] = { + 'count': box.length, + 'keys': box.keys.length, + }; + } + } catch (e) { + stats[boxName] = {'error': e.toString()}; + } + } + + return stats; + } + + /// Print database statistics + void printStatistics() { + final stats = getStatistics(); + debugPrint('=== Hive Database Statistics ==='); + stats.forEach((boxName, data) { + debugPrint('$boxName: $data'); + }); + debugPrint('================================'); + } +} diff --git a/lib/core/database/hive_initializer.dart b/lib/core/database/hive_initializer.dart new file mode 100644 index 0000000..e2383e1 --- /dev/null +++ b/lib/core/database/hive_initializer.dart @@ -0,0 +1,115 @@ +import 'package:flutter/foundation.dart'; + +import 'package:worker/core/database/database_manager.dart'; +import 'package:worker/core/database/hive_service.dart'; + +/// Hive Database Initializer +/// +/// Provides a simple API for initializing the Hive database +/// in the main.dart file. +/// +/// Example usage: +/// ```dart +/// void main() async { +/// WidgetsFlutterBinding.ensureInitialized(); +/// +/// // Initialize Hive +/// await HiveInitializer.initialize(); +/// +/// runApp(const MyApp()); +/// } +/// ``` +class HiveInitializer { + /// Initialize Hive database + /// + /// This method should be called once during app startup. + /// It initializes Hive, registers adapters, and opens boxes. + /// + /// [enableEncryption] - Enable AES encryption for sensitive boxes + /// [encryptionKey] - Optional custom encryption key (256-bit) + /// [verbose] - Enable verbose logging for debugging + static Future initialize({ + bool enableEncryption = false, + List? encryptionKey, + bool verbose = false, + }) async { + try { + if (verbose) { + debugPrint('HiveInitializer: Starting initialization...'); + } + + // Get HiveService instance + final hiveService = HiveService(); + + // Initialize Hive + await hiveService.initialize( + encryptionKey: enableEncryption ? encryptionKey : null, + ); + + // Perform initial maintenance + if (verbose) { + debugPrint('HiveInitializer: Performing initial maintenance...'); + } + + final dbManager = DatabaseManager(); + + // Clear expired cache on app start + await dbManager.clearExpiredCache(); + + // Print statistics in debug mode + if (verbose && kDebugMode) { + dbManager.printStatistics(); + } + + if (verbose) { + debugPrint('HiveInitializer: Initialization complete'); + } + } catch (e, stackTrace) { + debugPrint('HiveInitializer: Initialization failed: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Close Hive database + /// + /// Should be called when app is terminating. + /// Usually not needed for normal app lifecycle. + static Future close() async { + final hiveService = HiveService(); + await hiveService.close(); + } + + /// Reset database (clear all data) + /// + /// WARNING: This will delete all local data! + /// Use only for logout or app reset functionality. + static Future reset() async { + final hiveService = HiveService(); + await hiveService.clearAllData(); + } + + /// Clear user data (logout) + /// + /// Clears user-specific data while preserving app settings and cache. + static Future logout() async { + final hiveService = HiveService(); + await hiveService.clearUserData(); + } + + /// Get database statistics + /// + /// Returns statistics about all Hive boxes. + static Map getStatistics() { + final dbManager = DatabaseManager(); + return dbManager.getStatistics(); + } + + /// Print database statistics (debug only) + static void printStatistics() { + if (kDebugMode) { + final dbManager = DatabaseManager(); + dbManager.printStatistics(); + } + } +} diff --git a/lib/core/database/hive_service.dart b/lib/core/database/hive_service.dart new file mode 100644 index 0000000..7bcf3f4 --- /dev/null +++ b/lib/core/database/hive_service.dart @@ -0,0 +1,409 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +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/hive_registrar.g.dart'; + +/// Hive CE (Community Edition) Database Service +/// +/// This service manages the initialization, configuration, and lifecycle +/// of the Hive database for offline-first functionality. +/// +/// Features: +/// - Box initialization and registration +/// - Type adapter registration +/// - Encryption support +/// - Database compaction +/// - Migration handling +/// - Error recovery +class HiveService { + HiveService._internal(); + + // Singleton pattern + factory HiveService() => _instance; + + static final HiveService _instance = HiveService._internal(); + + /// Indicates whether Hive has been initialized + bool _isInitialized = false; + bool get isInitialized => _isInitialized; + + /// Encryption cipher (if enabled) + HiveAesCipher? _encryptionCipher; + + /// Initialize Hive database + /// + /// This should be called once during app startup, before any + /// Hive operations are performed. + /// + /// [encryptionKey] - Optional 256-bit encryption key for secure storage + /// If not provided and encryption is enabled, a new key will be generated. + Future initialize({List? encryptionKey}) async { + if (_isInitialized) { + debugPrint('HiveService: Already initialized'); + return; + } + + try { + debugPrint('HiveService: Initializing Hive CE...'); + + // Initialize Hive for Flutter + await Hive.initFlutter(); + + // Setup encryption if enabled + if (HiveDatabaseConfig.enableEncryption) { + _encryptionCipher = HiveAesCipher( + encryptionKey ?? Hive.generateSecureKey(), + ); + debugPrint('HiveService: Encryption enabled'); + } + + // Register all type adapters + await _registerTypeAdapters(); + + // Open all boxes + await _openBoxes(); + + // Check and perform migrations if needed + await _performMigrations(); + + // Perform initial cleanup/compaction if needed + await _performMaintenance(); + + _isInitialized = true; + debugPrint('HiveService: Initialization complete'); + } catch (e, stackTrace) { + debugPrint('HiveService: Initialization failed: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Register all Hive type adapters + /// + /// Type adapters must be registered before opening boxes. + /// Uses auto-generated registrar from hive_registrar.g.dart + Future _registerTypeAdapters() async { + 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) + 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'); + debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.notificationType) ? "✓" : "✗"} NotificationType adapter'); + debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter'); + debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter'); + + // TODO: Register actual model type adapters when models are created + // These will be added to the auto-generated registrar when models are created + // Example: + // - UserModel (typeId: 0) + // - ProductModel (typeId: 1) + // - CartItemModel (typeId: 2) + // - OrderModel (typeId: 3) + // - ProjectModel (typeId: 4) + // - LoyaltyTransactionModel (typeId: 5) + // etc. + + debugPrint('HiveService: Type adapters registered successfully'); + } + + /// Open all Hive boxes + /// + /// Opens boxes for immediate access. Some boxes use encryption if enabled. + Future _openBoxes() async { + debugPrint('HiveService: Opening boxes...'); + + try { + // Open non-encrypted boxes + await Future.wait([ + // Settings and preferences (non-sensitive) + Hive.openBox(HiveBoxNames.settingsBox), + + // Cache boxes (non-sensitive) + Hive.openBox(HiveBoxNames.cacheBox), + Hive.openBox(HiveBoxNames.syncStateBox), + + // Product and catalog data (non-sensitive) + Hive.openBox(HiveBoxNames.productBox), + Hive.openBox(HiveBoxNames.rewardsBox), + + // Notification box (non-sensitive) + Hive.openBox(HiveBoxNames.notificationBox), + ]); + + // Open potentially encrypted boxes (sensitive data) + final encryptedBoxes = [ + HiveBoxNames.userBox, + HiveBoxNames.cartBox, + HiveBoxNames.orderBox, + HiveBoxNames.projectBox, + HiveBoxNames.loyaltyBox, + HiveBoxNames.addressBox, + HiveBoxNames.offlineQueueBox, + ]; + + for (final boxName in encryptedBoxes) { + await Hive.openBox( + boxName, + encryptionCipher: _encryptionCipher, + ); + } + + debugPrint('HiveService: All boxes opened successfully'); + } catch (e, stackTrace) { + debugPrint('HiveService: Error opening boxes: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Perform database migrations + /// + /// Handles schema version upgrades and data migrations. + Future _performMigrations() async { + final settingsBox = Hive.box(HiveBoxNames.settingsBox); + final currentVersion = settingsBox.get( + HiveKeys.schemaVersion, + defaultValue: 0, + ) as int; + + debugPrint('HiveService: Current schema version: $currentVersion'); + 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++) { + await _migrateToVersion(version); + } + + // Update schema version + await settingsBox.put( + HiveKeys.schemaVersion, + HiveDatabaseConfig.currentSchemaVersion, + ); + + debugPrint('HiveService: Migrations complete'); + } else { + debugPrint('HiveService: No migrations needed'); + } + } + + /// Migrate to a specific version + Future _migrateToVersion(int version) async { + debugPrint('HiveService: Migrating to version $version'); + + switch (version) { + case 1: + // Initial version - no migration needed + break; + + // Future migrations will be added here + // case 2: + // await _migrateV1ToV2(); + // break; + + default: + debugPrint('HiveService: Unknown migration version: $version'); + } + } + + /// Perform database maintenance + /// + /// Includes compaction, cleanup of expired cache, etc. + Future _performMaintenance() async { + debugPrint('HiveService: Performing maintenance...'); + + try { + // Compact boxes if needed + await _compactBoxes(); + + // Clear expired cache + await _clearExpiredCache(); + + // Limit offline queue size + await _limitOfflineQueue(); + + debugPrint('HiveService: Maintenance complete'); + } catch (e, stackTrace) { + debugPrint('HiveService: Maintenance error: $e'); + debugPrint('StackTrace: $stackTrace'); + // Don't throw - maintenance errors shouldn't prevent app startup + } + } + + /// Compact boxes to reduce file size + Future _compactBoxes() async { + for (final boxName in HiveBoxNames.allBoxes) { + try { + if (Hive.isBoxOpen(boxName)) { + final box = Hive.box(boxName); + await box.compact(); + debugPrint('HiveService: Compacted box: $boxName'); + } + } catch (e) { + debugPrint('HiveService: Error compacting box $boxName: $e'); + } + } + } + + /// Clear expired cache entries + Future _clearExpiredCache() async { + final cacheBox = Hive.box(HiveBoxNames.cacheBox); + + // TODO: Implement cache expiration logic + // This will be implemented when cache models are created + + debugPrint('HiveService: Cleared expired cache entries'); + } + + /// Limit offline queue size + Future _limitOfflineQueue() async { + final queueBox = Hive.box(HiveBoxNames.offlineQueueBox); + + if (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'); + } + } + + /// Get a box by name + /// + /// Returns an already opened box. Throws if box is not open. + Box getBox(String boxName) { + if (!Hive.isBoxOpen(boxName)) { + throw HiveError('Box $boxName is not open'); + } + return Hive.box(boxName); + } + + /// Check if a box is open + bool isBoxOpen(String boxName) { + return Hive.isBoxOpen(boxName); + } + + /// Clear all data from all boxes + /// + /// WARNING: This will delete all local data. Use with caution. + Future clearAllData() async { + debugPrint('HiveService: Clearing all data...'); + + for (final boxName in HiveBoxNames.allBoxes) { + try { + if (Hive.isBoxOpen(boxName)) { + final box = Hive.box(boxName); + await box.clear(); + debugPrint('HiveService: Cleared box: $boxName'); + } + } catch (e) { + debugPrint('HiveService: Error clearing box $boxName: $e'); + } + } + + debugPrint('HiveService: All data cleared'); + } + + /// Clear user-specific data (logout) + /// + /// Clears user data while preserving app settings and cache + Future clearUserData() async { + debugPrint('HiveService: Clearing user data...'); + + final boxesToClear = [ + HiveBoxNames.userBox, + HiveBoxNames.cartBox, + HiveBoxNames.orderBox, + HiveBoxNames.projectBox, + HiveBoxNames.loyaltyBox, + HiveBoxNames.addressBox, + HiveBoxNames.notificationBox, + ]; + + for (final boxName in boxesToClear) { + try { + if (Hive.isBoxOpen(boxName)) { + final box = Hive.box(boxName); + await box.clear(); + debugPrint('HiveService: Cleared box: $boxName'); + } + } catch (e) { + debugPrint('HiveService: Error clearing box $boxName: $e'); + } + } + + debugPrint('HiveService: User data cleared'); + } + + /// Close all boxes + /// + /// Should be called when app is terminating + Future close() async { + debugPrint('HiveService: Closing all boxes...'); + + try { + await Hive.close(); + _isInitialized = false; + debugPrint('HiveService: All boxes closed'); + } catch (e, stackTrace) { + debugPrint('HiveService: Error closing boxes: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Delete all Hive data from disk + /// + /// WARNING: This completely removes the database. Use only for testing or reset. + Future deleteFromDisk() async { + debugPrint('HiveService: Deleting database from disk...'); + + try { + // Close all boxes first + await close(); + + // Delete Hive directory + final appDocDir = await getApplicationDocumentsDirectory(); + final hiveDir = Directory('${appDocDir.path}/hive'); + + if (await hiveDir.exists()) { + await hiveDir.delete(recursive: true); + debugPrint('HiveService: Database deleted from disk'); + } + } catch (e, stackTrace) { + debugPrint('HiveService: Error deleting database: $e'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } + } + + /// Generate a secure encryption key + /// + /// Returns a 256-bit encryption key for secure box encryption. + /// Store this key securely (e.g., in flutter_secure_storage). + static List generateEncryptionKey() { + return Hive.generateSecureKey(); + } +} diff --git a/lib/core/database/models/cached_data.dart b/lib/core/database/models/cached_data.dart new file mode 100644 index 0000000..5098032 --- /dev/null +++ b/lib/core/database/models/cached_data.dart @@ -0,0 +1,79 @@ +import 'package:hive_ce/hive.dart'; + +import 'package:worker/core/constants/storage_constants.dart'; + +part 'cached_data.g.dart'; + +/// Cached Data Model +/// +/// Wrapper for caching API responses with timestamp and expiration. +/// Used for offline-first functionality and reducing API calls. +/// +/// Example usage: +/// ```dart +/// final cachedProducts = CachedData( +/// data: products, +/// lastUpdated: DateTime.now(), +/// ); +/// ``` +@HiveType(typeId: HiveTypeIds.cachedData) +class CachedData extends HiveObject { + CachedData({ + required this.data, + required this.lastUpdated, + this.expiresAt, + this.source, + }); + + /// The cached data (stored as dynamic) + @HiveField(0) + final dynamic data; + + /// When the data was last updated + @HiveField(1) + final DateTime lastUpdated; + + /// Optional expiration time + @HiveField(2) + final DateTime? expiresAt; + + /// Source of the data (e.g., 'api', 'local') + @HiveField(3) + final String? source; + + /// Check if cache is expired + bool get isExpired { + if (expiresAt == null) return false; + return DateTime.now().isAfter(expiresAt!); + } + + /// Check if cache is fresh (not expired and within max age) + bool isFresh(Duration maxAge) { + if (isExpired) return false; + final age = DateTime.now().difference(lastUpdated); + return age <= maxAge; + } + + /// Get age of cached data + Duration get age => DateTime.now().difference(lastUpdated); + + /// Create a copy with updated data + CachedData copyWith({ + dynamic data, + DateTime? lastUpdated, + DateTime? expiresAt, + String? source, + }) { + return CachedData( + data: data ?? this.data, + lastUpdated: lastUpdated ?? this.lastUpdated, + expiresAt: expiresAt ?? this.expiresAt, + source: source ?? this.source, + ); + } + + @override + String toString() { + return 'CachedData(lastUpdated: $lastUpdated, expiresAt: $expiresAt, source: $source, isExpired: $isExpired)'; + } +} diff --git a/lib/core/database/models/cached_data.g.dart b/lib/core/database/models/cached_data.g.dart new file mode 100644 index 0000000..baa7299 --- /dev/null +++ b/lib/core/database/models/cached_data.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cached_data.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CachedDataAdapter extends TypeAdapter { + @override + final typeId = 30; + + @override + CachedData read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return CachedData( + data: fields[0] as dynamic, + lastUpdated: fields[1] as DateTime, + expiresAt: fields[2] as DateTime?, + source: fields[3] as String?, + ); + } + + @override + void write(BinaryWriter writer, CachedData obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.data) + ..writeByte(1) + ..write(obj.lastUpdated) + ..writeByte(2) + ..write(obj.expiresAt) + ..writeByte(3) + ..write(obj.source); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CachedDataAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/core/database/models/enums.dart b/lib/core/database/models/enums.dart new file mode 100644 index 0000000..8858e91 --- /dev/null +++ b/lib/core/database/models/enums.dart @@ -0,0 +1,425 @@ +import 'package:hive_ce/hive.dart'; + +import 'package:worker/core/constants/storage_constants.dart'; + +part 'enums.g.dart'; + +/// Member Tier Levels +/// +/// Represents the loyalty program membership tiers. +/// Higher tiers receive more benefits and rewards. +@HiveType(typeId: HiveTypeIds.memberTier) +enum MemberTier { + /// Gold tier - Entry level membership + @HiveField(0) + gold, + + /// Platinum tier - Mid-level membership + @HiveField(1) + platinum, + + /// Diamond tier - Premium membership + @HiveField(2) + diamond, +} + +/// User Type Categories +/// +/// Represents the different types of users in the app. +@HiveType(typeId: HiveTypeIds.userType) +enum UserType { + /// Construction contractor + @HiveField(0) + contractor, + + /// Architect or designer + @HiveField(1) + architect, + + /// Product distributor + @HiveField(2) + distributor, + + /// Real estate broker + @HiveField(3) + broker, +} + +/// Order Status +/// +/// Represents the current state of an order. +@HiveType(typeId: HiveTypeIds.orderStatus) +enum OrderStatus { + /// Order placed, awaiting confirmation + @HiveField(0) + pending, + + /// Order confirmed and being processed + @HiveField(1) + processing, + + /// Order is being shipped/delivered + @HiveField(2) + shipping, + + /// Order completed successfully + @HiveField(3) + completed, + + /// Order cancelled + @HiveField(4) + cancelled, + + /// Order refunded + @HiveField(5) + refunded, +} + +/// Project Status +/// +/// Represents the current state of a construction project. +@HiveType(typeId: HiveTypeIds.projectStatus) +enum ProjectStatus { + /// Project in planning phase + @HiveField(0) + planning, + + /// Project actively in progress + @HiveField(1) + inProgress, + + /// Project on hold + @HiveField(2) + onHold, + + /// Project completed + @HiveField(3) + completed, + + /// Project cancelled + @HiveField(4) + cancelled, +} + +/// Project Type +/// +/// Represents the category of construction project. +@HiveType(typeId: HiveTypeIds.projectType) +enum ProjectType { + /// Residential building project + @HiveField(0) + residential, + + /// Commercial building project + @HiveField(1) + commercial, + + /// Industrial facility project + @HiveField(2) + industrial, + + /// Infrastructure project + @HiveField(3) + infrastructure, + + /// Renovation project + @HiveField(4) + renovation, +} + +/// Loyalty Transaction Type +/// +/// Represents the type of loyalty points transaction. +@HiveType(typeId: HiveTypeIds.transactionType) +enum TransactionType { + /// Points earned from purchase + @HiveField(0) + earnedPurchase, + + /// Points earned from referral + @HiveField(1) + earnedReferral, + + /// Points earned from promotion + @HiveField(2) + earnedPromotion, + + /// Bonus points from admin + @HiveField(3) + earnedBonus, + + /// Points redeemed for reward + @HiveField(4) + redeemedReward, + + /// Points redeemed for discount + @HiveField(5) + redeemedDiscount, + + /// Points adjusted by admin + @HiveField(6) + adjustment, + + /// Points expired + @HiveField(7) + expired, +} + +/// Gift Status +/// +/// Represents the status of a redeemed gift/reward. +@HiveType(typeId: HiveTypeIds.giftStatus) +enum GiftStatus { + /// Gift is active and can be used + @HiveField(0) + active, + + /// Gift has been used + @HiveField(1) + used, + + /// Gift has expired + @HiveField(2) + expired, + + /// Gift is reserved but not activated + @HiveField(3) + reserved, + + /// Gift has been cancelled + @HiveField(4) + cancelled, +} + +/// Payment Status +/// +/// Represents the status of a payment transaction. +@HiveType(typeId: HiveTypeIds.paymentStatus) +enum PaymentStatus { + /// Payment pending + @HiveField(0) + pending, + + /// Payment being processed + @HiveField(1) + processing, + + /// Payment completed successfully + @HiveField(2) + completed, + + /// Payment failed + @HiveField(3) + failed, + + /// Payment refunded + @HiveField(4) + refunded, + + /// Payment cancelled + @HiveField(5) + cancelled, +} + +/// Notification Type +/// +/// Represents different categories of notifications. +@HiveType(typeId: HiveTypeIds.notificationType) +enum NotificationType { + /// Order-related notification + @HiveField(0) + order, + + /// Promotion or offer notification + @HiveField(1) + promotion, + + /// System announcement + @HiveField(2) + system, + + /// Loyalty program notification + @HiveField(3) + loyalty, + + /// Project-related notification + @HiveField(4) + project, + + /// Payment notification + @HiveField(5) + payment, + + /// General message + @HiveField(6) + message, +} + +/// Payment Method +/// +/// Represents available payment methods. +@HiveType(typeId: HiveTypeIds.paymentMethod) +enum PaymentMethod { + /// Cash on delivery + @HiveField(0) + cashOnDelivery, + + /// Bank transfer + @HiveField(1) + bankTransfer, + + /// Credit/Debit card + @HiveField(2) + card, + + /// E-wallet (Momo, ZaloPay, etc.) + @HiveField(3) + eWallet, + + /// QR code payment + @HiveField(4) + qrCode, + + /// Pay later / Credit + @HiveField(5) + payLater, +} + +/// Extension methods for enums + +extension MemberTierExtension on MemberTier { + /// Get display name + String get displayName { + switch (this) { + case MemberTier.gold: + return 'Gold'; + case MemberTier.platinum: + return 'Platinum'; + case MemberTier.diamond: + return 'Diamond'; + } + } + + /// Get tier level (higher is better) + int get level { + switch (this) { + case MemberTier.gold: + return 1; + case MemberTier.platinum: + return 2; + case MemberTier.diamond: + return 3; + } + } +} + +extension UserTypeExtension on UserType { + /// Get display name (Vietnamese) + String get displayName { + switch (this) { + case UserType.contractor: + return 'Thầu thợ'; + case UserType.architect: + return 'Kiến trúc sư'; + case UserType.distributor: + return 'Đại lý phân phối'; + case UserType.broker: + return 'Môi giới'; + } + } +} + +extension OrderStatusExtension on OrderStatus { + /// Get display name (Vietnamese) + String get displayName { + switch (this) { + case OrderStatus.pending: + return 'Chờ xác nhận'; + case OrderStatus.processing: + return 'Đang xử lý'; + case OrderStatus.shipping: + return 'Đang giao hàng'; + case OrderStatus.completed: + return 'Hoàn thành'; + case OrderStatus.cancelled: + return 'Đã hủy'; + case OrderStatus.refunded: + return 'Đã hoàn tiền'; + } + } + + /// Check if order is active + bool get isActive { + return this == OrderStatus.pending || + this == OrderStatus.processing || + this == OrderStatus.shipping; + } + + /// Check if order is final + bool get isFinal { + return this == OrderStatus.completed || + this == OrderStatus.cancelled || + this == OrderStatus.refunded; + } +} + +extension ProjectStatusExtension on ProjectStatus { + /// Get display name (Vietnamese) + String get displayName { + switch (this) { + case ProjectStatus.planning: + return 'Lập kế hoạch'; + case ProjectStatus.inProgress: + return 'Đang thực hiện'; + case ProjectStatus.onHold: + return 'Tạm dừng'; + case ProjectStatus.completed: + return 'Hoàn thành'; + case ProjectStatus.cancelled: + return 'Đã hủy'; + } + } + + /// Check if project is active + bool get isActive { + return this == ProjectStatus.planning || this == ProjectStatus.inProgress; + } +} + +extension TransactionTypeExtension on TransactionType { + /// Check if transaction is earning points + bool get isEarning { + return this == TransactionType.earnedPurchase || + this == TransactionType.earnedReferral || + this == TransactionType.earnedPromotion || + this == TransactionType.earnedBonus; + } + + /// Check if transaction is spending points + bool get isSpending { + return this == TransactionType.redeemedReward || + this == TransactionType.redeemedDiscount; + } + + /// Get display name (Vietnamese) + String get displayName { + switch (this) { + case TransactionType.earnedPurchase: + return 'Mua hàng'; + case TransactionType.earnedReferral: + return 'Giới thiệu bạn bè'; + case TransactionType.earnedPromotion: + return 'Khuyến mãi'; + case TransactionType.earnedBonus: + return 'Thưởng'; + case TransactionType.redeemedReward: + return 'Đổi quà'; + case TransactionType.redeemedDiscount: + return 'Đổi giảm giá'; + case TransactionType.adjustment: + return 'Điều chỉnh'; + case TransactionType.expired: + return 'Hết hạn'; + } + } +} diff --git a/lib/core/database/models/enums.g.dart b/lib/core/database/models/enums.g.dart new file mode 100644 index 0000000..11eb54b --- /dev/null +++ b/lib/core/database/models/enums.g.dart @@ -0,0 +1,517 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enums.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MemberTierAdapter extends TypeAdapter { + @override + final typeId = 20; + + @override + MemberTier read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return MemberTier.gold; + case 1: + return MemberTier.platinum; + case 2: + return MemberTier.diamond; + default: + return MemberTier.gold; + } + } + + @override + void write(BinaryWriter writer, MemberTier obj) { + switch (obj) { + case MemberTier.gold: + writer.writeByte(0); + case MemberTier.platinum: + writer.writeByte(1); + case MemberTier.diamond: + writer.writeByte(2); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MemberTierAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class UserTypeAdapter extends TypeAdapter { + @override + final typeId = 21; + + @override + UserType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return UserType.contractor; + case 1: + return UserType.architect; + case 2: + return UserType.distributor; + case 3: + return UserType.broker; + default: + return UserType.contractor; + } + } + + @override + void write(BinaryWriter writer, UserType obj) { + switch (obj) { + case UserType.contractor: + writer.writeByte(0); + case UserType.architect: + writer.writeByte(1); + case UserType.distributor: + writer.writeByte(2); + case UserType.broker: + writer.writeByte(3); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class OrderStatusAdapter extends TypeAdapter { + @override + final typeId = 22; + + @override + OrderStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return OrderStatus.pending; + case 1: + return OrderStatus.processing; + case 2: + return OrderStatus.shipping; + case 3: + return OrderStatus.completed; + case 4: + return OrderStatus.cancelled; + case 5: + return OrderStatus.refunded; + default: + return OrderStatus.pending; + } + } + + @override + void write(BinaryWriter writer, OrderStatus obj) { + switch (obj) { + case OrderStatus.pending: + writer.writeByte(0); + case OrderStatus.processing: + writer.writeByte(1); + case OrderStatus.shipping: + writer.writeByte(2); + case OrderStatus.completed: + writer.writeByte(3); + case OrderStatus.cancelled: + writer.writeByte(4); + case OrderStatus.refunded: + writer.writeByte(5); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OrderStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ProjectStatusAdapter extends TypeAdapter { + @override + final typeId = 23; + + @override + ProjectStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ProjectStatus.planning; + case 1: + return ProjectStatus.inProgress; + case 2: + return ProjectStatus.onHold; + case 3: + return ProjectStatus.completed; + case 4: + return ProjectStatus.cancelled; + default: + return ProjectStatus.planning; + } + } + + @override + void write(BinaryWriter writer, ProjectStatus obj) { + switch (obj) { + case ProjectStatus.planning: + writer.writeByte(0); + case ProjectStatus.inProgress: + writer.writeByte(1); + case ProjectStatus.onHold: + writer.writeByte(2); + case ProjectStatus.completed: + writer.writeByte(3); + case ProjectStatus.cancelled: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProjectStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ProjectTypeAdapter extends TypeAdapter { + @override + final typeId = 24; + + @override + ProjectType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ProjectType.residential; + case 1: + return ProjectType.commercial; + case 2: + return ProjectType.industrial; + case 3: + return ProjectType.infrastructure; + case 4: + return ProjectType.renovation; + default: + return ProjectType.residential; + } + } + + @override + void write(BinaryWriter writer, ProjectType obj) { + switch (obj) { + case ProjectType.residential: + writer.writeByte(0); + case ProjectType.commercial: + writer.writeByte(1); + case ProjectType.industrial: + writer.writeByte(2); + case ProjectType.infrastructure: + writer.writeByte(3); + case ProjectType.renovation: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProjectTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class TransactionTypeAdapter extends TypeAdapter { + @override + final typeId = 25; + + @override + TransactionType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return TransactionType.earnedPurchase; + case 1: + return TransactionType.earnedReferral; + case 2: + return TransactionType.earnedPromotion; + case 3: + return TransactionType.earnedBonus; + case 4: + return TransactionType.redeemedReward; + case 5: + return TransactionType.redeemedDiscount; + case 6: + return TransactionType.adjustment; + case 7: + return TransactionType.expired; + default: + return TransactionType.earnedPurchase; + } + } + + @override + void write(BinaryWriter writer, TransactionType obj) { + switch (obj) { + case TransactionType.earnedPurchase: + writer.writeByte(0); + case TransactionType.earnedReferral: + writer.writeByte(1); + case TransactionType.earnedPromotion: + writer.writeByte(2); + case TransactionType.earnedBonus: + writer.writeByte(3); + case TransactionType.redeemedReward: + writer.writeByte(4); + case TransactionType.redeemedDiscount: + writer.writeByte(5); + case TransactionType.adjustment: + writer.writeByte(6); + case TransactionType.expired: + writer.writeByte(7); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TransactionTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class GiftStatusAdapter extends TypeAdapter { + @override + final typeId = 26; + + @override + GiftStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return GiftStatus.active; + case 1: + return GiftStatus.used; + case 2: + return GiftStatus.expired; + case 3: + return GiftStatus.reserved; + case 4: + return GiftStatus.cancelled; + default: + return GiftStatus.active; + } + } + + @override + void write(BinaryWriter writer, GiftStatus obj) { + switch (obj) { + case GiftStatus.active: + writer.writeByte(0); + case GiftStatus.used: + writer.writeByte(1); + case GiftStatus.expired: + writer.writeByte(2); + case GiftStatus.reserved: + writer.writeByte(3); + case GiftStatus.cancelled: + writer.writeByte(4); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GiftStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class PaymentStatusAdapter extends TypeAdapter { + @override + final typeId = 27; + + @override + PaymentStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return PaymentStatus.pending; + case 1: + return PaymentStatus.processing; + case 2: + return PaymentStatus.completed; + case 3: + return PaymentStatus.failed; + case 4: + return PaymentStatus.refunded; + case 5: + return PaymentStatus.cancelled; + default: + return PaymentStatus.pending; + } + } + + @override + void write(BinaryWriter writer, PaymentStatus obj) { + switch (obj) { + case PaymentStatus.pending: + writer.writeByte(0); + case PaymentStatus.processing: + writer.writeByte(1); + case PaymentStatus.completed: + writer.writeByte(2); + case PaymentStatus.failed: + writer.writeByte(3); + case PaymentStatus.refunded: + writer.writeByte(4); + case PaymentStatus.cancelled: + writer.writeByte(5); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PaymentStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class NotificationTypeAdapter extends TypeAdapter { + @override + final typeId = 28; + + @override + NotificationType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return NotificationType.order; + case 1: + return NotificationType.promotion; + case 2: + return NotificationType.system; + case 3: + return NotificationType.loyalty; + case 4: + return NotificationType.project; + case 5: + return NotificationType.payment; + case 6: + return NotificationType.message; + default: + return NotificationType.order; + } + } + + @override + void write(BinaryWriter writer, NotificationType obj) { + switch (obj) { + case NotificationType.order: + writer.writeByte(0); + case NotificationType.promotion: + writer.writeByte(1); + case NotificationType.system: + writer.writeByte(2); + case NotificationType.loyalty: + writer.writeByte(3); + case NotificationType.project: + writer.writeByte(4); + case NotificationType.payment: + writer.writeByte(5); + case NotificationType.message: + writer.writeByte(6); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NotificationTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class PaymentMethodAdapter extends TypeAdapter { + @override + final typeId = 29; + + @override + PaymentMethod read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return PaymentMethod.cashOnDelivery; + case 1: + return PaymentMethod.bankTransfer; + case 2: + return PaymentMethod.card; + case 3: + return PaymentMethod.eWallet; + case 4: + return PaymentMethod.qrCode; + case 5: + return PaymentMethod.payLater; + default: + return PaymentMethod.cashOnDelivery; + } + } + + @override + void write(BinaryWriter writer, PaymentMethod obj) { + switch (obj) { + case PaymentMethod.cashOnDelivery: + writer.writeByte(0); + case PaymentMethod.bankTransfer: + writer.writeByte(1); + case PaymentMethod.card: + writer.writeByte(2); + case PaymentMethod.eWallet: + writer.writeByte(3); + case PaymentMethod.qrCode: + writer.writeByte(4); + case PaymentMethod.payLater: + writer.writeByte(5); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PaymentMethodAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..e747b1a --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -0,0 +1,351 @@ +/// Custom exceptions for the Worker app +/// +/// This file defines all custom exception types used throughout the application +/// for better error handling and user feedback. +library; + +// ============================================================================ +// Network Exceptions +// ============================================================================ + +/// Base exception for all network-related errors +class NetworkException implements Exception { + 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)' : ''}'; +} + +/// 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.', + ); +} + +/// 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, + ); +} + +/// Exception thrown when server returns 500+ errors +class ServerException extends NetworkException { + const ServerException([ + String message = 'Lỗi máy chủ. Vui lòng thử lại sau.', + int? statusCode, + ]) : super(message, statusCode: statusCode); +} + +/// 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, + ); +} + +// ============================================================================ +// Authentication Exceptions +// ============================================================================ + +/// Base exception for authentication-related errors +class AuthException implements Exception { + const AuthException( + this.message, { + this.statusCode, + }); + + final String message; + final int? statusCode; + + @override + String toString() => 'AuthException: $message'; +} + +/// 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, + ); +} + +/// Exception thrown when user is not authenticated +class UnauthorizedException extends AuthException { + const UnauthorizedException([ + super.message = 'Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.', + ]) : super(statusCode: 401); +} + +/// 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, + ); +} + +/// 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, + ); +} + +/// 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, + ); +} + +/// 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, + ); +} + +/// 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, + ); +} + +// ============================================================================ +// Request Validation Exceptions +// ============================================================================ + +/// Exception thrown when request data is invalid +class ValidationException implements Exception { + const ValidationException( + this.message, { + this.errors, + }); + + final String message; + final Map>? errors; + + @override + String toString() { + if (errors != null && errors!.isNotEmpty) { + final errorMessages = errors!.entries + .map((e) => '${e.key}: ${e.value.join(", ")}') + .join('; '); + return 'ValidationException: $message - $errorMessages'; + } + return 'ValidationException: $message'; + } +} + +/// Exception thrown when request parameters are invalid +class BadRequestException extends ValidationException { + const BadRequestException([ + String message = 'Yêu cầu không hợp lệ.', + Map>? errors, + ]) : super(message, errors: errors); +} + +// ============================================================================ +// Resource Exceptions +// ============================================================================ + +/// Exception thrown when requested resource is not found +class NotFoundException implements Exception { + const NotFoundException([ + this.message = 'Không tìm thấy tài nguyên.', + this.resourceType, + this.resourceId, + ]); + + final String message; + final String? resourceType; + final String? resourceId; + + @override + String toString() { + if (resourceType != null && resourceId != null) { + return 'NotFoundException: $resourceType with ID $resourceId not found'; + } + return 'NotFoundException: $message'; + } +} + +/// 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.', + ]); + + final String message; + + @override + String toString() => 'ConflictException: $message'; +} + +// ============================================================================ +// Rate Limiting Exceptions +// ============================================================================ + +/// Exception thrown when API rate limit is exceeded +class RateLimitException implements Exception { + const RateLimitException([ + this.message = 'Bạn đã gửi quá nhiều yêu cầu. Vui lòng thử lại sau.', + this.retryAfter, + ]); + + final String message; + final int? retryAfter; // seconds + + @override + String toString() { + if (retryAfter != null) { + return 'RateLimitException: $message (Retry after: ${retryAfter}s)'; + } + return 'RateLimitException: $message'; + } +} + +// ============================================================================ +// Payment Exceptions +// ============================================================================ + +/// Exception thrown for payment-related errors +class PaymentException implements Exception { + const PaymentException( + this.message, { + this.transactionId, + }); + + final String message; + final String? transactionId; + + @override + String toString() => 'PaymentException: $message'; +} + +/// Exception thrown when payment fails +class PaymentFailedException extends PaymentException { + const PaymentFailedException([ + String message = 'Thanh toán thất bại. Vui lòng thử lại.', + String? transactionId, + ]) : super(message, transactionId: transactionId); +} + +/// Exception thrown when payment is cancelled +class PaymentCancelledException extends PaymentException { + const PaymentCancelledException() + : super('Thanh toán đã bị hủy.'); +} + +// ============================================================================ +// Cache Exceptions +// ============================================================================ + +/// Exception thrown for cache-related errors +class CacheException implements Exception { + const CacheException([ + this.message = 'Lỗi khi truy cập bộ nhớ đệm.', + ]); + + final String message; + + @override + String toString() => 'CacheException: $message'; +} + +/// Exception thrown when cache data is corrupted +class CacheCorruptedException extends CacheException { + const CacheCorruptedException() + : super('Dữ liệu bộ nhớ đệm bị hỏng.'); +} + +// ============================================================================ +// Storage Exceptions +// ============================================================================ + +/// 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ộ.', + ]); + + final String message; + + @override + String toString() => 'StorageException: $message'; +} + +/// 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.'); +} + +// ============================================================================ +// Parse Exceptions +// ============================================================================ + +/// Exception thrown when JSON parsing fails +class ParseException implements Exception { + const ParseException([ + this.message = 'Lỗi khi phân tích dữ liệu.', + this.source, + ]); + + final String message; + final dynamic source; + + @override + String toString() => 'ParseException: $message'; +} + +// ============================================================================ +// Unknown Exceptions +// ============================================================================ + +/// Exception thrown for unexpected errors +class UnknownException implements Exception { + const UnknownException([ + this.message = 'Đã xảy ra lỗi không xác định.', + this.originalError, + this.stackTrace, + ]); + + final String message; + final dynamic originalError; + final StackTrace? stackTrace; + + @override + String toString() { + if (originalError != null) { + return 'UnknownException: $message (Original: $originalError)'; + } + return 'UnknownException: $message'; + } +} diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 0000000..0b66268 --- /dev/null +++ b/lib/core/errors/failures.dart @@ -0,0 +1,262 @@ +/// Failure classes for error handling in the Worker app +/// +/// Failures represent domain-level errors that can be returned from use cases +/// and repositories. They wrap exceptions and provide user-friendly error messages. +library; + +/// Base failure class +sealed class Failure { + const Failure({required this.message}); + + /// Network-related failure + 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; + + /// Authentication failure + const factory Failure.authentication({ + required String message, + int? statusCode, + }) = AuthenticationFailure; + + /// Validation failure + const factory Failure.validation({ + required String message, + Map>? errors, + }) = ValidationFailure; + + /// Not found failure (404) + const factory Failure.notFound({ + required String message, + }) = NotFoundFailure; + + /// Conflict failure (409) + const factory Failure.conflict({ + required String message, + }) = ConflictFailure; + + /// Rate limit exceeded failure (429) + const factory Failure.rateLimit({ + required String message, + int? retryAfter, + }) = RateLimitFailure; + + /// Payment failure + const factory Failure.payment({ + required String message, + String? transactionId, + }) = PaymentFailure; + + /// Cache failure + const factory Failure.cache({ + required String message, + }) = CacheFailure; + + /// Storage failure + const factory Failure.storage({ + required String message, + }) = StorageFailure; + + /// Parse failure + const factory Failure.parse({ + required String message, + }) = ParseFailure; + + /// No internet connection failure + const factory Failure.noInternet() = NoInternetFailure; + + /// Timeout failure + const factory Failure.timeout() = TimeoutFailure; + + /// Unknown failure + const factory Failure.unknown({ + required String message, + }) = UnknownFailure; + + final String message; + + /// Check if this is a critical failure that requires immediate attention + bool get isCritical { + return switch (this) { + ServerFailure() => true, + AuthenticationFailure() => true, + PaymentFailure() => true, + UnknownFailure() => true, + _ => false, + }; + } + + /// Check if this failure can be retried + bool get canRetry { + return switch (this) { + NetworkFailure() => true, + ServerFailure(:final statusCode) => statusCode == 503, + AuthenticationFailure(:final statusCode) => statusCode == 401, + RateLimitFailure() => true, + CacheFailure() => true, + NoInternetFailure() => true, + TimeoutFailure() => true, + _ => false, + }; + } + + /// Get HTTP status code if available + int? get statusCode { + return switch (this) { + NetworkFailure(:final statusCode) => statusCode, + ServerFailure(:final statusCode) => statusCode, + AuthenticationFailure(:final statusCode) => statusCode, + _ => null, + }; + } + + /// 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.', + TimeoutFailure() => 'Kết nối quá lâu. Vui lòng thử lại.', + _ => message, + }; + } + + String _formatValidationMessage(String message, Map>? errors) { + if (errors != null && errors.isNotEmpty) { + final firstError = errors.values.first.first; + return '$message: $firstError'; + } + return message; + } + + String _formatRateLimitMessage(String message, int? retryAfter) { + if (retryAfter != null) { + return '$message Thử lại sau $retryAfter giây.'; + } + return message; + } +} + +/// Network-related failure +final class NetworkFailure extends Failure { + const NetworkFailure({ + required super.message, + this.statusCode, + }); + + @override + final int? statusCode; +} + +/// Server error failure (5xx errors) +final class ServerFailure extends Failure { + const ServerFailure({ + required super.message, + this.statusCode, + }); + + @override + final int? statusCode; +} + +/// Authentication failure +final class AuthenticationFailure extends Failure { + const AuthenticationFailure({ + required super.message, + this.statusCode, + }); + + @override + final int? statusCode; +} + +/// Validation failure +final class ValidationFailure extends Failure { + const ValidationFailure({ + required super.message, + this.errors, + }); + + final Map>? errors; +} + +/// Not found failure (404) +final class NotFoundFailure extends Failure { + const NotFoundFailure({ + required super.message, + }); +} + +/// Conflict failure (409) +final class ConflictFailure extends Failure { + const ConflictFailure({ + required super.message, + }); +} + +/// Rate limit exceeded failure (429) +final class RateLimitFailure extends Failure { + const RateLimitFailure({ + required super.message, + this.retryAfter, + }); + + final int? retryAfter; +} + +/// Payment failure +final class PaymentFailure extends Failure { + const PaymentFailure({ + required super.message, + this.transactionId, + }); + + final String? transactionId; +} + +/// Cache failure +final class CacheFailure extends Failure { + const CacheFailure({ + required super.message, + }); +} + +/// Storage failure +final class StorageFailure extends Failure { + const StorageFailure({ + required super.message, + }); +} + +/// Parse failure +final class ParseFailure extends Failure { + 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'); +} + +/// Timeout failure +final class TimeoutFailure extends Failure { + const TimeoutFailure() + : super(message: 'Kết nối quá lâu'); +} + +/// Unknown failure +final class UnknownFailure extends Failure { + const UnknownFailure({ + required super.message, + }); +} diff --git a/lib/core/network/README.md b/lib/core/network/README.md new file mode 100644 index 0000000..a190d77 --- /dev/null +++ b/lib/core/network/README.md @@ -0,0 +1,449 @@ +# API Integration Infrastructure - Worker App + +## Overview + +Comprehensive HTTP client infrastructure built with **Dio** and **Riverpod 3.0** for the Worker Flutter application. This setup provides robust API integration with authentication, caching, retry logic, error handling, and offline support. + +## Architecture + +``` +lib/core/network/ +├── dio_client.dart # Main HTTP client with Riverpod providers +├── api_interceptor.dart # Authentication, logging, and error interceptors +├── network_info.dart # Network connectivity monitoring +├── api_constants.dart # API endpoints and configuration +├── exceptions.dart # Custom exception definitions +└── failures.dart # Domain-level failure types +``` + +## Key Features + +### 1. Dio HTTP Client (`dio_client.dart`) + +**DioClient Class** +- Wrapper around Dio with full method support (GET, POST, PUT, PATCH, DELETE) +- File upload with multipart/form-data +- File download with progress tracking +- Cache management utilities + +**Riverpod Providers** +- `dioProvider` - Configured Dio instance with all interceptors +- `dioClientProvider` - DioClient wrapper instance +- `cacheStoreProvider` - Hive-based cache storage +- `cacheOptionsProvider` - Cache configuration + +**Configuration** +- Base URL: Configurable per environment (dev/staging/prod) +- Timeouts: 30s connection, 30s receive, 30s send +- Headers: JSON content-type, Vietnamese language by default +- Cache: 7-day max-stale, no caching on auth errors (401, 403) + +### 2. Interceptors (`api_interceptor.dart`) + +#### AuthInterceptor +- **Token Injection**: Automatically adds Bearer token to requests +- **Token Refresh**: Handles 401 errors with automatic token refresh +- **Public Endpoints**: Skips auth for login/OTP/register endpoints +- **Language Header**: Adds Vietnamese language preference +- **Storage**: Uses SharedPreferences for token persistence + +#### LoggingInterceptor +- **Request Logging**: Method, URL, headers, body, query parameters +- **Response Logging**: Status code, response data (truncated) +- **Error Logging**: Error type, status code, error data +- **Security**: Sanitizes sensitive fields (password, OTP, tokens) +- **Format**: Beautiful formatted logs with separators + +#### ErrorTransformerInterceptor +- **Dio Error Mapping**: Transforms DioException to custom exceptions +- **Status Code Handling**: + - 400 → ValidationException/BadRequestException + - 401 → UnauthorizedException/TokenExpiredException/InvalidOTPException + - 403 → ForbiddenException + - 404 → NotFoundException + - 409 → ConflictException + - 422 → ValidationException with field errors + - 429 → RateLimitException with retry-after + - 5xx → ServerException/ServiceUnavailableException +- **Connection Errors**: Timeout, NoInternet, etc. + +#### RetryInterceptor +- **Exponential Backoff**: Configurable delay multiplier +- **Max Retries**: 3 attempts by default +- **Retry Conditions**: + - Connection timeout/errors + - 5xx server errors (except 501) + - 408 Request Timeout + - 429 Too Many Requests +- **Network Check**: Verifies connectivity before retrying + +### 3. Network Monitoring (`network_info.dart`) + +**NetworkInfo Interface** +- Connection status checking +- Connection type detection (WiFi, Mobile, Ethernet, etc.) +- Real-time connectivity monitoring via Stream + +**NetworkStatus Class** +- Connection state (connected/disconnected) +- Connection type +- Timestamp +- Convenience methods (isWiFi, isMobile, isMetered) + +**Riverpod Providers** +- `networkInfoProvider` - NetworkInfo implementation +- `isConnectedProvider` - Current connection status +- `connectionTypeProvider` - Current connection type +- `networkStatusStreamProvider` - Stream of status changes +- `NetworkStatusNotifier` - Reactive network status state + +### 4. Error Handling + +**Exceptions (`exceptions.dart`)** +- NetworkException - Base network error +- NoInternetException - No connectivity +- TimeoutException - Connection timeout +- ServerException - 5xx errors +- ServiceUnavailableException - 503 errors +- AuthException - Authentication errors (401, 403) +- ValidationException - Request validation errors +- NotFoundException - 404 errors +- ConflictException - 409 errors +- RateLimitException - 429 errors +- PaymentException - Payment-related errors +- CacheException - Cache errors +- StorageException - Local storage errors +- ParseException - JSON parsing errors + +**Failures (`failures.dart`)** +- Immutable Freezed classes for domain-level errors +- User-friendly Vietnamese error messages +- Properties: + - `message` - Display message + - `isCritical` - Requires immediate attention + - `canRetry` - Can be retried + - `statusCode` - HTTP status if available + +### 5. API Constants (`api_constants.dart`) + +**Configuration** +- Base URLs (dev, staging, production) +- API version prefix (/v1) +- Timeout durations (30s) +- Retry configuration (3 attempts, exponential backoff) +- Cache durations (24h products, 1h profile, 48h categories) +- Request headers (JSON, Vietnamese language) + +**Endpoints** +- Authentication: /auth/request-otp, /auth/verify-otp, /auth/register, etc. +- Loyalty: /loyalty/points, /loyalty/rewards, /loyalty/referral, etc. +- Products: /products, /products/search, /categories, etc. +- Orders: /orders, /payments, etc. +- Projects & Quotes: /projects, /quotes, etc. +- Chat: /chat/messages, /ws/chat (WebSocket) +- Account: /profile, /addresses, etc. +- Promotions & Notifications + +## Usage Examples + +### Basic GET Request + +```dart +// Using DioClient with Riverpod +final dioClient = ref.watch(dioClientProvider); + +try { + final response = await dioClient.get( + ApiConstants.getProducts, + queryParameters: {'page': '1', 'limit': '20'}, + ); + + final products = response.data; +} on NoInternetException catch (e) { + // Handle no internet +} on ServerException catch (e) { + // Handle server error +} +``` + +### POST Request with Authentication + +```dart +final dioClient = ref.watch(dioClientProvider); + +try { + final response = await dioClient.post( + ApiConstants.createOrder, + data: { + 'items': [...], + 'deliveryAddress': {...}, + 'paymentMethod': 'COD', + }, + ); + + final order = Order.fromJson(response.data); +} on ValidationException catch (e) { + // Handle validation errors + print(e.errors); // Map> +} +``` + +### File Upload + +```dart +final dioClient = ref.watch(dioClientProvider); + +final formData = FormData.fromMap({ + 'name': 'John Doe', + 'avatar': await MultipartFile.fromFile( + filePath, + filename: 'avatar.jpg', + ), +}); + +try { + final response = await dioClient.uploadFile( + ApiConstants.uploadAvatar, + formData: formData, + onSendProgress: (sent, total) { + print('Upload progress: ${(sent / total * 100).toStringAsFixed(0)}%'); + }, + ); +} catch (e) { + // Handle error +} +``` + +### Network Status Monitoring + +```dart +// Check current connection status +final isConnected = await ref.watch(isConnectedProvider.future); + +if (!isConnected) { + // Show offline message +} + +// Listen to connection changes +ref.listen( + networkStatusStreamProvider, + (previous, next) { + next.whenData((status) { + if (status.isConnected) { + // Back online - sync data + } else { + // Offline - show message + } + }); + }, +); +``` + +### Cache Management + +```dart +final dioClient = ref.watch(dioClientProvider); + +// Clear all cache +await dioClient.clearCache(); + +// Clear specific endpoint cache +await dioClient.clearCacheByPath(ApiConstants.getProducts); + +// Force refresh from network +final response = await dioClient.get( + ApiConstants.getProducts, + options: ApiRequestOptions.forceNetwork.toDioOptions(), +); + +// Use cache-first strategy +final response = await dioClient.get( + ApiConstants.getCategories, + options: ApiRequestOptions.cached.toDioOptions(), +); +``` + +### Custom Error Handling + +```dart +try { + final response = await dioClient.post(...); +} on ValidationException catch (e) { + // Show field-specific errors + e.errors?.forEach((field, messages) { + print('$field: ${messages.join(", ")}'); + }); +} on RateLimitException catch (e) { + // Show rate limit message + if (e.retryAfter != null) { + print('Try again in ${e.retryAfter} seconds'); + } +} on TokenExpiredException catch (e) { + // Token refresh failed - redirect to login + ref.read(authProvider.notifier).logout(); +} catch (e) { + // Generic error + print('Error: $e'); +} +``` + +## Dependencies + +```yaml +dependencies: + dio: ^5.4.3+1 # HTTP client + connectivity_plus: ^6.0.3 # Network monitoring + pretty_dio_logger: ^1.3.1 # Request/response logging + dio_cache_interceptor: ^3.5.0 # Response caching + dio_cache_interceptor_hive_store: ^3.2.2 # Hive storage for cache + flutter_riverpod: ^3.0.0 # State management + riverpod_annotation: ^3.0.0 # Code generation + shared_preferences: ^2.2.3 # Token storage + path_provider: ^2.1.3 # Cache directory + freezed_annotation: ^3.0.0 # Immutable models +``` + +## Configuration + +### Environment-Specific Base URLs + +Update `ApiConstants.baseUrl` based on build flavor: + +```dart +// For dev environment +static const String baseUrl = devBaseUrl; + +// For production +static const String baseUrl = prodBaseUrl; +``` + +### Timeout Configuration + +Adjust timeouts in `ApiConstants`: + +```dart +static const Duration connectionTimeout = Duration(milliseconds: 30000); +static const Duration receiveTimeout = Duration(milliseconds: 30000); +static const Duration sendTimeout = Duration(milliseconds: 30000); +``` + +### Retry Configuration + +Customize retry behavior in `ApiConstants`: + +```dart +static const int maxRetryAttempts = 3; +static const Duration initialRetryDelay = Duration(milliseconds: 1000); +static const Duration maxRetryDelay = Duration(milliseconds: 5000); +static const double retryDelayMultiplier = 2.0; +``` + +### Cache Configuration + +Adjust cache settings in `cacheOptionsProvider`: + +```dart +CacheOptions( + store: store, + maxStale: const Duration(days: 7), + hitCacheOnErrorExcept: [401, 403], + priority: CachePriority.high, + allowPostMethod: false, +); +``` + +## Testing + +### Connection Testing + +```dart +// Test network connectivity +final networkInfo = ref.watch(networkInfoProvider); +final isConnected = await networkInfo.isConnected; +final connectionType = await networkInfo.connectionType; + +print('Connected: $isConnected'); +print('Type: ${connectionType.displayNameVi}'); +``` + +### API Endpoint Testing + +```dart +// Test authentication endpoint +try { + final response = await dioClient.post( + ApiConstants.requestOtp, + data: {'phone': '+84912345678'}, + ); + print('OTP sent successfully'); +} catch (e) { + print('Failed: $e'); +} +``` + +## Best Practices + +1. **Always use DioClient**: Don't create raw Dio instances +2. **Handle specific exceptions**: Catch specific error types for better UX +3. **Check connectivity**: Verify network status before critical requests +4. **Use cache strategically**: Cache static data (categories, products) +5. **Monitor network changes**: Listen to connectivity stream for sync +6. **Clear cache appropriately**: Clear on logout, version updates +7. **Log in debug only**: Disable logging in production +8. **Sanitize sensitive data**: Never log passwords, tokens, OTP codes +9. **Use retry wisely**: Don't retry POST/PUT/DELETE by default +10. **Validate responses**: Check response.data structure before parsing + +## Future Enhancements + +- [ ] Offline request queue implementation +- [ ] Request deduplication +- [ ] GraphQL support +- [ ] WebSocket integration for real-time chat +- [ ] Certificate pinning for security +- [ ] Request compression (gzip) +- [ ] Multi-part upload progress +- [ ] Background sync when network restored +- [ ] Advanced caching strategies (stale-while-revalidate) +- [ ] Request cancellation tokens + +## Troubleshooting + +### Issue: Token Refresh Loop + +**Solution**: Check refresh token expiry and clear auth data if expired + +### Issue: Cache Not Working + +**Solution**: Verify CacheStore initialization and directory permissions + +### Issue: Network Detection Fails + +**Solution**: Add required permissions to AndroidManifest.xml and Info.plist + +### Issue: Timeout on Large Files + +**Solution**: Increase timeout or use download with progress callback + +### Issue: Interceptor Order Matters + +**Current Order**: +1. Logging (first - logs everything) +2. Auth (adds tokens) +3. Cache (caches responses) +4. Retry (retries failures) +5. Error Transformer (last - transforms errors) + +## Support + +For issues or questions about the API integration: +- Check logs for detailed error information +- Verify network connectivity using NetworkInfo +- Review interceptor configuration +- Check API endpoint constants + +--- + +**Generated for Worker App** +Version: 1.0.0 +Last Updated: 2025-10-17 diff --git a/lib/core/network/api_interceptor.dart b/lib/core/network/api_interceptor.dart new file mode 100644 index 0000000..60cc2f5 --- /dev/null +++ b/lib/core/network/api_interceptor.dart @@ -0,0 +1,572 @@ +/// API interceptors for request/response handling +/// +/// Provides interceptors for: +/// - Authentication token injection +/// - Request/response logging +/// - Error transformation +/// - Token refresh handling +library; + +import 'dart:developer' as developer; + +import 'package:dio/dio.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'; + +part 'api_interceptor.g.dart'; + +// ============================================================================ +// Storage Keys +// ============================================================================ + +/// Keys for storing auth tokens in SharedPreferences +class AuthStorageKeys { + static const String accessToken = 'auth_access_token'; + static const String refreshToken = 'auth_refresh_token'; + static const String tokenExpiry = 'auth_token_expiry'; +} + +// ============================================================================ +// Auth Interceptor +// ============================================================================ + +/// Interceptor for adding authentication tokens to requests +class AuthInterceptor extends Interceptor { + AuthInterceptor(this._prefs, this._dio); + + final SharedPreferences _prefs; + final Dio _dio; + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + // Check if this endpoint requires authentication + if (_requiresAuth(options.path)) { + final token = await _getAccessToken(); + + if (token != null) { + // Add bearer token to headers + options.headers['Authorization'] = 'Bearer $token'; + } + } + + // Add language header + options.headers['Accept-Language'] = ApiConstants.acceptLanguageVi; + + // Add content-type and accept headers if not already set + options.headers['Content-Type'] ??= ApiConstants.contentTypeJson; + options.headers['Accept'] ??= ApiConstants.acceptJson; + + handler.next(options); + } + + @override + void onError( + DioException err, + ErrorInterceptorHandler handler, + ) async { + // Check if error is 401 Unauthorized + if (err.response?.statusCode == 401) { + // Try to refresh token + final refreshed = await _refreshAccessToken(); + + if (refreshed) { + // Retry the original request with new token + try { + final response = await _retry(err.requestOptions); + handler.resolve(response); + return; + } catch (e) { + // If retry fails, continue with error + } + } + } + + handler.next(err); + } + + /// Check if endpoint requires authentication + bool _requiresAuth(String path) { + // Public endpoints that don't require auth + final publicEndpoints = [ + ApiConstants.requestOtp, + ApiConstants.verifyOtp, + ApiConstants.register, + ]; + + return !publicEndpoints.any((endpoint) => path.contains(endpoint)); + } + + /// Get access token from storage + Future _getAccessToken() async { + return _prefs.getString(AuthStorageKeys.accessToken); + } + + /// Get refresh token from storage + Future _getRefreshToken() async { + return _prefs.getString(AuthStorageKeys.refreshToken); + } + + /// Check if token is expired + Future _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 _refreshAccessToken() async { + try { + final refreshToken = await _getRefreshToken(); + + if (refreshToken == null) { + return false; + } + + // Call refresh token endpoint + final response = await _dio.post>( + '${ApiConstants.apiBaseUrl}${ApiConstants.refreshToken}', + options: Options( + headers: { + 'Authorization': 'Bearer $refreshToken', + }, + ), + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + + // Save new tokens + await _prefs.setString( + AuthStorageKeys.accessToken, + data['accessToken'] as String, + ); + + if (data.containsKey('refreshToken')) { + await _prefs.setString( + AuthStorageKeys.refreshToken, + data['refreshToken'] as String, + ); + } + + if (data.containsKey('expiresAt')) { + await _prefs.setString( + AuthStorageKeys.tokenExpiry, + data['expiresAt'] as String, + ); + } + + return true; + } + + return false; + } catch (e) { + developer.log( + 'Failed to refresh token', + name: 'AuthInterceptor', + error: e, + ); + return false; + } + } + + /// Retry failed request with new token + Future> _retry(RequestOptions requestOptions) async { + final token = await _getAccessToken(); + + final options = Options( + method: requestOptions.method, + headers: { + ...requestOptions.headers, + 'Authorization': 'Bearer $token', + }, + ); + + return _dio.request( + requestOptions.path, + data: requestOptions.data, + queryParameters: requestOptions.queryParameters, + options: options, + ); + } +} + +// ============================================================================ +// Logging Interceptor +// ============================================================================ + +/// Interceptor for logging requests and responses in debug mode +class LoggingInterceptor extends Interceptor { + LoggingInterceptor({ + this.enableRequestLogging = true, + this.enableResponseLogging = true, + this.enableErrorLogging = true, + }); + + final bool enableRequestLogging; + final bool enableResponseLogging; + final bool enableErrorLogging; + + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) { + if (enableRequestLogging) { + developer.log( + '╔══════════════════════════════════════════════════════════════', + name: 'HTTP Request', + ); + developer.log( + '║ ${options.method} ${options.uri}', + name: 'HTTP Request', + ); + developer.log( + '║ Headers: ${_sanitizeHeaders(options.headers)}', + name: 'HTTP Request', + ); + + if (options.data != null) { + developer.log( + '║ Body: ${_sanitizeBody(options.data)}', + name: 'HTTP Request', + ); + } + + if (options.queryParameters.isNotEmpty) { + developer.log( + '║ Query Parameters: ${options.queryParameters}', + name: 'HTTP Request', + ); + } + + developer.log( + '╚══════════════════════════════════════════════════════════════', + name: 'HTTP Request', + ); + } + + handler.next(options); + } + + @override + void onResponse( + Response response, + ResponseInterceptorHandler handler, + ) { + if (enableResponseLogging) { + developer.log( + '╔══════════════════════════════════════════════════════════════', + name: 'HTTP Response', + ); + developer.log( + '║ ${response.requestOptions.method} ${response.requestOptions.uri}', + name: 'HTTP Response', + ); + developer.log( + '║ Status Code: ${response.statusCode}', + name: 'HTTP Response', + ); + developer.log( + '║ Data: ${_truncateData(response.data, 500)}', + name: 'HTTP Response', + ); + developer.log( + '╚══════════════════════════════════════════════════════════════', + name: 'HTTP Response', + ); + } + + handler.next(response); + } + + @override + void onError( + DioException err, + ErrorInterceptorHandler handler, + ) { + if (enableErrorLogging) { + developer.log( + '╔══════════════════════════════════════════════════════════════', + name: 'HTTP Error', + ); + developer.log( + '║ ${err.requestOptions.method} ${err.requestOptions.uri}', + 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', + ); + + if (err.response?.data != null) { + developer.log( + '║ Error Data: ${_truncateData(err.response?.data, 500)}', + name: 'HTTP Error', + ); + } + + developer.log( + '╚══════════════════════════════════════════════════════════════', + name: 'HTTP Error', + ); + } + + handler.next(err); + } + + /// Sanitize headers by hiding sensitive information + Map _sanitizeHeaders(Map headers) { + final sanitized = Map.from(headers); + + // Hide authorization token + if (sanitized.containsKey('Authorization')) { + sanitized['Authorization'] = '[HIDDEN]'; + } + + return sanitized; + } + + /// Sanitize request body by hiding sensitive fields + dynamic _sanitizeBody(dynamic body) { + if (body is Map) { + final sanitized = Map.from(body); + + // List of sensitive field names + final sensitiveFields = [ + 'password', + 'otp', + 'token', + 'accessToken', + 'refreshToken', + 'secret', + 'apiKey', + ]; + + for (final field in sensitiveFields) { + if (sanitized.containsKey(field)) { + sanitized[field] = '[HIDDEN]'; + } + } + + return sanitized; + } + + return body; + } + + /// Truncate data for logging to avoid huge logs + String _truncateData(dynamic data, int maxLength) { + final dataStr = data.toString(); + if (dataStr.length <= maxLength) { + return dataStr; + } + return '${dataStr.substring(0, maxLength)}... [TRUNCATED]'; + } +} + +// ============================================================================ +// Error Transformer Interceptor +// ============================================================================ + +/// Interceptor for transforming Dio errors into custom exceptions +class ErrorTransformerInterceptor extends Interceptor { + @override + void onError( + DioException err, + ErrorInterceptorHandler handler, + ) { + Exception exception; + + switch (err.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + exception = const TimeoutException(); + break; + + case DioExceptionType.connectionError: + exception = const NoInternetException(); + break; + + case DioExceptionType.badResponse: + exception = _handleBadResponse(err.response); + break; + + case DioExceptionType.cancel: + exception = NetworkException('Yêu cầu đã bị hủy'); + break; + + case DioExceptionType.unknown: + exception = NetworkException( + 'Lỗi không xác định: ${err.message}', + ); + break; + + default: + exception = NetworkException(err.message ?? 'Unknown error'); + } + + handler.reject( + DioException( + requestOptions: err.requestOptions, + response: err.response, + type: err.type, + error: exception, + message: exception.toString(), + ), + ); + } + + /// Handle bad response errors based on status code + Exception _handleBadResponse(Response? response) { + if (response == null) { + return const ServerException(); + } + + final statusCode = response.statusCode ?? 0; + final data = response.data; + + // Extract error message from response + String? message; + if (data is Map) { + message = data['message'] as String? ?? + data['error'] as String? ?? + data['msg'] as String?; + } + + switch (statusCode) { + case 400: + if (data is Map && data.containsKey('errors')) { + final errors = data['errors'] as Map?; + if (errors != null) { + final validationErrors = errors.map( + (key, value) => MapEntry( + key, + value is List + ? value.cast() + : [value.toString()], + ), + ); + return ValidationException( + message ?? 'Dữ liệu không hợp lệ', + errors: validationErrors, + ); + } + } + return BadRequestException(message ?? 'Yêu cầu không hợp lệ'); + + case 401: + if (message?.toLowerCase().contains('token') ?? false) { + return const TokenExpiredException(); + } + if (message?.toLowerCase().contains('otp') ?? false) { + return const InvalidOTPException(); + } + return UnauthorizedException(message ?? 'Phiên đăng nhập hết hạn'); + + case 403: + return const ForbiddenException(); + + case 404: + return NotFoundException(message ?? 'Không tìm thấy tài nguyên'); + + case 409: + return ConflictException(message ?? 'Tài nguyên đã tồn tại'); + + case 422: + if (data is Map && data.containsKey('errors')) { + final errors = data['errors'] as Map?; + if (errors != null) { + final validationErrors = errors.map( + (key, value) => MapEntry( + key, + value is List + ? value.cast() + : [value.toString()], + ), + ); + return ValidationException( + message ?? 'Dữ liệu không hợp lệ', + errors: validationErrors, + ); + } + } + return ValidationException(message ?? 'Dữ liệu không hợp lệ'); + + case 429: + final retryAfter = response.headers.value('retry-after'); + final retrySeconds = retryAfter != null ? int.tryParse(retryAfter) : null; + return RateLimitException(message ?? 'Quá nhiều yêu cầu', retrySeconds); + + case 500: + case 502: + case 503: + case 504: + if (statusCode == 503) { + return const ServiceUnavailableException(); + } + return ServerException(message ?? 'Lỗi máy chủ', statusCode); + + default: + return NetworkException( + message ?? 'Lỗi mạng không xác định', + statusCode: statusCode, + data: data, + ); + } + } +} + +// ============================================================================ +// Riverpod Providers +// ============================================================================ + +/// Provider for SharedPreferences instance +@riverpod +Future sharedPreferences(Ref ref) async { + return await SharedPreferences.getInstance(); +} + +/// Provider for AuthInterceptor +@riverpod +Future authInterceptor(Ref ref, Dio dio) async { + final prefs = await ref.watch(sharedPreferencesProvider.future); + return AuthInterceptor(prefs, dio); +} + +/// Provider for LoggingInterceptor +@riverpod +LoggingInterceptor loggingInterceptor(Ref ref) { + // Only enable logging in debug mode + const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter + + return LoggingInterceptor( + enableRequestLogging: isDebug, + enableResponseLogging: isDebug, + enableErrorLogging: isDebug, + ); +} + +/// Provider for ErrorTransformerInterceptor +@riverpod +ErrorTransformerInterceptor errorTransformerInterceptor(Ref ref) { + return ErrorTransformerInterceptor(); +} diff --git a/lib/core/network/api_interceptor.g.dart b/lib/core/network/api_interceptor.g.dart new file mode 100644 index 0000000..3789396 --- /dev/null +++ b/lib/core/network/api_interceptor.g.dart @@ -0,0 +1,246 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_interceptor.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider for SharedPreferences instance + +@ProviderFor(sharedPreferences) +const sharedPreferencesProvider = SharedPreferencesProvider._(); + +/// Provider for SharedPreferences instance + +final class SharedPreferencesProvider + extends + $FunctionalProvider< + AsyncValue, + SharedPreferences, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// Provider for SharedPreferences instance + const SharedPreferencesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'sharedPreferencesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$sharedPreferencesHash(); + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return sharedPreferences(ref); + } +} + +String _$sharedPreferencesHash() => r'dc403fbb1d968c7d5ab4ae1721a29ffe173701c7'; + +/// Provider for AuthInterceptor + +@ProviderFor(authInterceptor) +const authInterceptorProvider = AuthInterceptorFamily._(); + +/// Provider for AuthInterceptor + +final class AuthInterceptorProvider + extends + $FunctionalProvider< + AsyncValue, + AuthInterceptor, + FutureOr + > + with $FutureModifier, $FutureProvider { + /// Provider for AuthInterceptor + const AuthInterceptorProvider._({ + required AuthInterceptorFamily super.from, + required Dio super.argument, + }) : super( + retry: null, + name: r'authInterceptorProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$authInterceptorHash(); + + @override + String toString() { + return r'authInterceptorProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as Dio; + return authInterceptor(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is AuthInterceptorProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10'; + +/// Provider for AuthInterceptor + +final class AuthInterceptorFamily extends $Family + with $FunctionalFamilyOverride, Dio> { + const AuthInterceptorFamily._() + : super( + retry: null, + name: r'authInterceptorProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Provider for AuthInterceptor + + AuthInterceptorProvider call(Dio dio) => + AuthInterceptorProvider._(argument: dio, from: this); + + @override + String toString() => r'authInterceptorProvider'; +} + +/// Provider for LoggingInterceptor + +@ProviderFor(loggingInterceptor) +const loggingInterceptorProvider = LoggingInterceptorProvider._(); + +/// Provider for LoggingInterceptor + +final class LoggingInterceptorProvider + extends + $FunctionalProvider< + LoggingInterceptor, + LoggingInterceptor, + LoggingInterceptor + > + with $Provider { + /// Provider for LoggingInterceptor + const LoggingInterceptorProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'loggingInterceptorProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$loggingInterceptorHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + LoggingInterceptor create(Ref ref) { + return loggingInterceptor(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(LoggingInterceptor value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$loggingInterceptorHash() => + r'f3dedaeb3152d5188544232f6f270bb6908c2827'; + +/// Provider for ErrorTransformerInterceptor + +@ProviderFor(errorTransformerInterceptor) +const errorTransformerInterceptorProvider = + ErrorTransformerInterceptorProvider._(); + +/// Provider for ErrorTransformerInterceptor + +final class ErrorTransformerInterceptorProvider + extends + $FunctionalProvider< + ErrorTransformerInterceptor, + ErrorTransformerInterceptor, + ErrorTransformerInterceptor + > + with $Provider { + /// Provider for ErrorTransformerInterceptor + const ErrorTransformerInterceptorProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'errorTransformerInterceptorProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$errorTransformerInterceptorHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + ErrorTransformerInterceptor create(Ref ref) { + return errorTransformerInterceptor(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(ErrorTransformerInterceptor value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$errorTransformerInterceptorHash() => + r'15a14206b96d046054277ee0b8220838e0e9e267'; diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart new file mode 100644 index 0000000..54c5f77 --- /dev/null +++ b/lib/core/network/dio_client.dart @@ -0,0 +1,496 @@ +/// Dio HTTP client configuration for the Worker app +/// +/// Provides a configured Dio instance with interceptors for: +/// - Authentication +/// - Logging +/// - Error handling +/// - Caching +/// - Retry logic +library; + +import 'package:dio/dio.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:worker/core/constants/api_constants.dart'; +import 'package:worker/core/network/api_interceptor.dart'; +import 'package:worker/core/network/network_info.dart'; + +part 'dio_client.g.dart'; + +// ============================================================================ +// Dio Client Configuration +// ============================================================================ + +/// HTTP client wrapper around Dio with interceptors and configuration +class DioClient { + DioClient(this._dio, this._cacheStore); + + final Dio _dio; + final CacheStore? _cacheStore; + + /// Get the underlying Dio instance + Dio get dio => _dio; + + /// Get the cache store + CacheStore? get cacheStore => _cacheStore; + + // ============================================================================ + // HTTP Methods + // ============================================================================ + + /// Perform GET request + Future> get( + String path, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + try { + return await _dio.get( + path, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + } catch (e) { + rethrow; + } + } + + /// Perform POST request + Future> post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + return await _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } catch (e) { + rethrow; + } + } + + /// Perform PUT request + Future> put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + return await _dio.put( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } catch (e) { + rethrow; + } + } + + /// Perform PATCH request + Future> patch( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + return await _dio.patch( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } catch (e) { + rethrow; + } + } + + /// Perform DELETE request + Future> delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } catch (e) { + rethrow; + } + } + + /// Upload file with multipart/form-data + Future> uploadFile( + String path, { + required FormData formData, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + }) async { + try { + return await _dio.post( + path, + data: formData, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + ); + } catch (e) { + rethrow; + } + } + + /// Download file + Future> downloadFile( + String urlPath, + String savePath, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + bool deleteOnError = true, + String lengthHeader = Headers.contentLengthHeader, + Options? options, + }) async { + try { + return await _dio.download( + urlPath, + savePath, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + deleteOnError: deleteOnError, + lengthHeader: lengthHeader, + options: options, + ); + } catch (e) { + rethrow; + } + } + + // ============================================================================ + // Cache Management + // ============================================================================ + + /// Clear all cached responses + Future clearCache() async { + if (_cacheStore != null) { + await _cacheStore!.clean(); + } + } + + /// Clear specific cached response by key + Future clearCacheByKey(String key) async { + if (_cacheStore != null) { + await _cacheStore!.delete(key); + } + } + + /// Clear cache for specific path + Future clearCacheByPath(String path) async { + if (_cacheStore != null) { + final key = CacheOptions.defaultCacheKeyBuilder( + RequestOptions(path: path), + ); + await _cacheStore!.delete(key); + } + } +} + +// ============================================================================ +// Retry Interceptor +// ============================================================================ + +/// Interceptor for retrying failed requests with exponential backoff +class RetryInterceptor extends Interceptor { + RetryInterceptor( + this._networkInfo, { + this.maxRetries = ApiConstants.maxRetryAttempts, + this.initialDelay = ApiConstants.initialRetryDelay, + this.maxDelay = ApiConstants.maxRetryDelay, + this.delayMultiplier = ApiConstants.retryDelayMultiplier, + }); + + final NetworkInfo _networkInfo; + final int maxRetries; + final Duration initialDelay; + final Duration maxDelay; + final double delayMultiplier; + + @override + void onError( + DioException err, + ErrorInterceptorHandler handler, + ) async { + // Get retry count from request extra + final retries = err.requestOptions.extra['retries'] as int? ?? 0; + + // Check if we should retry + if (retries >= maxRetries || !_shouldRetry(err)) { + handler.next(err); + return; + } + + // Check network connectivity before retrying + final isConnected = await _networkInfo.isConnected; + if (!isConnected) { + handler.next(err); + return; + } + + // Calculate delay with exponential backoff + final delayMs = (initialDelay.inMilliseconds * + (delayMultiplier * (retries + 1))).toInt(); + final delay = Duration( + milliseconds: delayMs.clamp( + initialDelay.inMilliseconds, + maxDelay.inMilliseconds, + ), + ); + + // Wait before retry + await Future.delayed(delay); + + // Increment retry count + err.requestOptions.extra['retries'] = retries + 1; + + // Retry the request + try { + final dio = Dio(); + final response = await dio.fetch(err.requestOptions); + handler.resolve(response); + } on DioException catch (e) { + handler.next(e); + } + } + + /// Determine if error should trigger a retry + bool _shouldRetry(DioException error) { + // Retry on connection errors + if (error.type == DioExceptionType.connectionTimeout || + error.type == DioExceptionType.receiveTimeout || + error.type == DioExceptionType.connectionError) { + return true; + } + + // Retry on 5xx server errors (except 501) + final statusCode = error.response?.statusCode; + if (statusCode != null && statusCode >= 500 && statusCode != 501) { + return true; + } + + // Retry on 408 Request Timeout + if (statusCode == 408) { + return true; + } + + // Retry on 429 Too Many Requests (with delay from header) + if (statusCode == 429) { + return true; + } + + return false; + } +} + +// ============================================================================ +// Riverpod Providers +// ============================================================================ + +/// Provider for cache store +@riverpod +Future cacheStore(Ref ref) async { + final directory = await getTemporaryDirectory(); + return HiveCacheStore( + directory.path, + hiveBoxName: 'dio_cache', + ); +} + +/// Provider for cache options +@riverpod +Future cacheOptions(Ref ref) async { + final store = await ref.watch(cacheStoreProvider.future); + + return CacheOptions( + store: store, + maxStale: const Duration(days: 7), // Keep cache for 7 days + hitCacheOnErrorExcept: [401, 403], // Use cache on error except auth errors + priority: CachePriority.high, + cipher: null, // No encryption for now + keyBuilder: CacheOptions.defaultCacheKeyBuilder, + allowPostMethod: false, // Don't cache POST requests by default + ); +} + +/// Provider for Dio instance with all interceptors +@riverpod +Future dio(Ref ref) async { + final dio = Dio(); + + // 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 + + // 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))) + // 4. Retry interceptor + ..interceptors.add(RetryInterceptor(ref.watch(networkInfoProvider))) + // 5. Error transformer (last to transform all errors) + ..interceptors.add(ref.watch(errorTransformerInterceptorProvider)); + + return dio; +} + +/// Provider for DioClient +@riverpod +Future dioClient(Ref ref) async { + final dio = await ref.watch(dioProvider.future); + final cacheStore = await ref.watch(cacheStoreProvider.future); + + return DioClient(dio, cacheStore); +} + +// ============================================================================ +// Helper Classes +// ============================================================================ + +/// Options for API requests with custom cache policy +class ApiRequestOptions { + const ApiRequestOptions({ + this.cachePolicy, + this.cacheDuration, + this.forceRefresh = false, + }); + + final CachePolicy? cachePolicy; + final Duration? cacheDuration; + final bool forceRefresh; + + /// Options with cache enabled + static const cached = ApiRequestOptions( + cachePolicy: CachePolicy.forceCache, + ); + + /// Options with network-first strategy + static const networkFirst = ApiRequestOptions( + cachePolicy: CachePolicy.refreshForceCache, + ); + + /// Options to force refresh from network + static const forceNetwork = ApiRequestOptions( + cachePolicy: CachePolicy.refresh, + forceRefresh: true, + ); + + /// Convert to Dio Options + Options toDioOptions() { + return Options( + extra: { + if (cachePolicy != null) + CacheResponse.cacheKey: cachePolicy!.index, + if (cacheDuration != null) + 'maxStale': cacheDuration, + if (forceRefresh) + 'policy': CachePolicy.refresh.index, + }, + ); + } +} + +/// Offline request queue item +class QueuedRequest { + QueuedRequest({ + required this.method, + required this.path, + this.data, + this.queryParameters, + required this.timestamp, + }); + + factory QueuedRequest.fromJson(Map json) { + return QueuedRequest( + method: json['method'] as String, + path: json['path'] as String, + data: json['data'], + queryParameters: json['queryParameters'] as Map?, + timestamp: DateTime.parse(json['timestamp'] as String), + ); + } + + final String method; + final String path; + final dynamic data; + final Map? queryParameters; + final DateTime timestamp; + + Map toJson() => { + 'method': method, + 'path': path, + 'data': data, + 'queryParameters': queryParameters, + 'timestamp': timestamp.toIso8601String(), + }; +} diff --git a/lib/core/network/dio_client.g.dart b/lib/core/network/dio_client.g.dart new file mode 100644 index 0000000..271c30e --- /dev/null +++ b/lib/core/network/dio_client.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dio_client.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider for cache store + +@ProviderFor(cacheStore) +const cacheStoreProvider = CacheStoreProvider._(); + +/// Provider for cache store + +final class CacheStoreProvider + extends + $FunctionalProvider< + AsyncValue, + CacheStore, + FutureOr + > + with $FutureModifier, $FutureProvider { + /// Provider for cache store + const CacheStoreProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'cacheStoreProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$cacheStoreHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return cacheStore(ref); + } +} + +String _$cacheStoreHash() => r'8cbc2688ee267e03fc5aa6bf48c3ada249cb6345'; + +/// Provider for cache options + +@ProviderFor(cacheOptions) +const cacheOptionsProvider = CacheOptionsProvider._(); + +/// Provider for cache options + +final class CacheOptionsProvider + extends + $FunctionalProvider< + AsyncValue, + CacheOptions, + FutureOr + > + with $FutureModifier, $FutureProvider { + /// Provider for cache options + const CacheOptionsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'cacheOptionsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$cacheOptionsHash(); + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return cacheOptions(ref); + } +} + +String _$cacheOptionsHash() => r'6b6b951855d8c0094e36918efa79c6ba586e156d'; + +/// Provider for Dio instance with all interceptors + +@ProviderFor(dio) +const dioProvider = DioProvider._(); + +/// Provider for Dio instance with all interceptors + +final class DioProvider + extends $FunctionalProvider, Dio, FutureOr> + with $FutureModifier, $FutureProvider { + /// Provider for Dio instance with all interceptors + const DioProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'dioProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$dioHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return dio(ref); + } +} + +String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7'; + +/// Provider for DioClient + +@ProviderFor(dioClient) +const dioClientProvider = DioClientProvider._(); + +/// Provider for DioClient + +final class DioClientProvider + extends + $FunctionalProvider< + AsyncValue, + DioClient, + FutureOr + > + with $FutureModifier, $FutureProvider { + /// Provider for DioClient + const DioClientProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'dioClientProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$dioClientHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return dioClient(ref); + } +} + +String _$dioClientHash() => r'4f6754880ccc00aa99b8ae19904e9da88950a4e1'; diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart new file mode 100644 index 0000000..61273eb --- /dev/null +++ b/lib/core/network/network_info.dart @@ -0,0 +1,365 @@ +/// Network connectivity information and monitoring +/// +/// Provides real-time network status checking, connection type detection, +/// and connectivity monitoring for the Worker app. +library; + +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'network_info.g.dart'; + +// ============================================================================ +// Network Connection Types +// ============================================================================ + +/// Types of network connections +enum NetworkConnectionType { + /// WiFi connection + wifi, + + /// Mobile data connection + mobile, + + /// Ethernet connection (wired) + ethernet, + + /// Bluetooth connection + bluetooth, + + /// VPN connection + vpn, + + /// No connection + none, + + /// Unknown connection type + unknown, +} + +// ============================================================================ +// Network Status +// ============================================================================ + +/// Network connectivity status +class NetworkStatus { + const NetworkStatus({ + required this.isConnected, + required this.connectionType, + required this.timestamp, + }); + + factory NetworkStatus.connected(NetworkConnectionType type) { + return NetworkStatus( + isConnected: true, + connectionType: type, + timestamp: DateTime.now(), + ); + } + + factory NetworkStatus.disconnected() { + return NetworkStatus( + isConnected: false, + connectionType: NetworkConnectionType.none, + timestamp: DateTime.now(), + ); + } + + final bool isConnected; + final NetworkConnectionType connectionType; + final DateTime timestamp; + + /// Check if connected via WiFi + bool get isWiFi => connectionType == NetworkConnectionType.wifi; + + /// Check if connected via mobile data + bool get isMobile => connectionType == NetworkConnectionType.mobile; + + /// Check if connected via ethernet + bool get isEthernet => connectionType == NetworkConnectionType.ethernet; + + /// Check if connection is metered (mobile data) + bool get isMetered => isMobile; + + @override + String toString() { + return 'NetworkStatus(isConnected: $isConnected, type: $connectionType)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is NetworkStatus && + other.isConnected == isConnected && + other.connectionType == connectionType; + } + + @override + int get hashCode => Object.hash(isConnected, connectionType); +} + +// ============================================================================ +// Network Info Interface +// ============================================================================ + +/// Abstract interface for network information +abstract class NetworkInfo { + /// Check if device is currently connected to internet + Future get isConnected; + + /// Get current network connection type + Future get connectionType; + + /// Get current network status + Future get networkStatus; + + /// Stream of network status changes + Stream get onNetworkStatusChanged; +} + +// ============================================================================ +// Network Info Implementation +// ============================================================================ + +/// Implementation of NetworkInfo using connectivity_plus +class NetworkInfoImpl implements NetworkInfo { + NetworkInfoImpl(this._connectivity); + + final Connectivity _connectivity; + StreamController? _statusController; + StreamSubscription>? _subscription; + + @override + Future get isConnected async { + final results = await _connectivity.checkConnectivity(); + return _hasConnection(results); + } + + @override + Future get connectionType async { + final results = await _connectivity.checkConnectivity(); + return _mapConnectivityResult(results); + } + + @override + Future get networkStatus async { + final results = await _connectivity.checkConnectivity(); + final hasConnection = _hasConnection(results); + final type = _mapConnectivityResult(results); + + if (hasConnection) { + return NetworkStatus.connected(type); + } else { + return NetworkStatus.disconnected(); + } + } + + @override + Stream get onNetworkStatusChanged { + _statusController ??= StreamController.broadcast( + onListen: _startListening, + onCancel: _stopListening, + ); + return _statusController!.stream; + } + + void _startListening() { + _subscription = _connectivity.onConnectivityChanged.listen( + (results) { + final hasConnection = _hasConnection(results); + final type = _mapConnectivityResult(results); + final status = hasConnection + ? NetworkStatus.connected(type) + : NetworkStatus.disconnected(); + _statusController?.add(status); + }, + onError: (error) { + _statusController?.add(NetworkStatus.disconnected()); + }, + ); + } + + void _stopListening() { + _subscription?.cancel(); + _subscription = null; + } + + bool _hasConnection(List results) { + if (results.isEmpty) return false; + return !results.contains(ConnectivityResult.none); + } + + NetworkConnectionType _mapConnectivityResult(List results) { + if (results.isEmpty || results.contains(ConnectivityResult.none)) { + return NetworkConnectionType.none; + } + + // Priority order: WiFi > Ethernet > Mobile > Bluetooth > VPN + if (results.contains(ConnectivityResult.wifi)) { + return NetworkConnectionType.wifi; + } else if (results.contains(ConnectivityResult.ethernet)) { + return NetworkConnectionType.ethernet; + } else if (results.contains(ConnectivityResult.mobile)) { + return NetworkConnectionType.mobile; + } else if (results.contains(ConnectivityResult.bluetooth)) { + return NetworkConnectionType.bluetooth; + } else if (results.contains(ConnectivityResult.vpn)) { + return NetworkConnectionType.vpn; + } else { + return NetworkConnectionType.unknown; + } + } + + /// Dispose resources + void dispose() { + _subscription?.cancel(); + _statusController?.close(); + } +} + +// ============================================================================ +// Riverpod Providers +// ============================================================================ + +/// Provider for Connectivity instance +@riverpod +Connectivity connectivity(Ref ref) { + return Connectivity(); +} + +/// Provider for NetworkInfo instance +@riverpod +NetworkInfo networkInfo(Ref ref) { + final connectivity = ref.watch(connectivityProvider); + final networkInfo = NetworkInfoImpl(connectivity); + + // Dispose when provider is disposed + ref.onDispose(() { + networkInfo.dispose(); + }); + + return networkInfo; +} + +/// Provider for current network connection status (boolean) +@riverpod +Future isConnected(Ref ref) async { + final networkInfo = ref.watch(networkInfoProvider); + return await networkInfo.isConnected; +} + +/// Provider for current network connection type +@riverpod +Future connectionType(Ref ref) async { + final networkInfo = ref.watch(networkInfoProvider); + return await networkInfo.connectionType; +} + +/// Stream provider for network status changes +@riverpod +Stream networkStatusStream(Ref ref) { + final networkInfo = ref.watch(networkInfoProvider); + return networkInfo.onNetworkStatusChanged; +} + +/// Provider for current network status +@riverpod +class NetworkStatusNotifier extends _$NetworkStatusNotifier { + @override + Future build() async { + final networkInfo = ref.watch(networkInfoProvider); + final status = await networkInfo.networkStatus; + + // Listen to network changes + ref.listen( + networkStatusStreamProvider, + (_, next) { + next.whenData((newStatus) { + state = AsyncValue.data(newStatus); + }); + }, + ); + + return status; + } + + /// Manually refresh network status + Future refresh() async { + state = const AsyncValue.loading(); + final networkInfo = ref.read(networkInfoProvider); + state = await AsyncValue.guard(() => networkInfo.networkStatus); + } + + /// Check if connected + bool get isConnected { + return state.when( + data: (status) => status.isConnected, + loading: () => false, + error: (_, __) => false, + ); + } + + /// Get connection type + NetworkConnectionType get type { + return state.when( + data: (status) => status.connectionType, + loading: () => NetworkConnectionType.none, + error: (_, __) => NetworkConnectionType.none, + ); + } +} + +// ============================================================================ +// Utility Extensions +// ============================================================================ + +/// Extension methods for NetworkConnectionType +extension NetworkConnectionTypeX on NetworkConnectionType { + /// Get display name in Vietnamese + String get displayNameVi { + switch (this) { + case NetworkConnectionType.wifi: + return 'WiFi'; + case NetworkConnectionType.mobile: + return 'Dữ liệu di động'; + case NetworkConnectionType.ethernet: + return 'Ethernet'; + case NetworkConnectionType.bluetooth: + return 'Bluetooth'; + case NetworkConnectionType.vpn: + return 'VPN'; + case NetworkConnectionType.none: + return 'Không có kết nối'; + case NetworkConnectionType.unknown: + return 'Không xác định'; + } + } + + /// Get display name in English + String get displayNameEn { + switch (this) { + case NetworkConnectionType.wifi: + return 'WiFi'; + case NetworkConnectionType.mobile: + return 'Mobile Data'; + case NetworkConnectionType.ethernet: + return 'Ethernet'; + case NetworkConnectionType.bluetooth: + return 'Bluetooth'; + case NetworkConnectionType.vpn: + return 'VPN'; + case NetworkConnectionType.none: + return 'No Connection'; + case NetworkConnectionType.unknown: + return 'Unknown'; + } + } + + /// Check if this is a valid connection type + bool get isValid { + return this != NetworkConnectionType.none && + this != NetworkConnectionType.unknown; + } +} diff --git a/lib/core/network/network_info.g.dart b/lib/core/network/network_info.g.dart new file mode 100644 index 0000000..429f801 --- /dev/null +++ b/lib/core/network/network_info.g.dart @@ -0,0 +1,282 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'network_info.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider for Connectivity instance + +@ProviderFor(connectivity) +const connectivityProvider = ConnectivityProvider._(); + +/// Provider for Connectivity instance + +final class ConnectivityProvider + extends $FunctionalProvider + with $Provider { + /// Provider for Connectivity instance + const ConnectivityProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'connectivityProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$connectivityHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + Connectivity create(Ref ref) { + return connectivity(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Connectivity value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda'; + +/// Provider for NetworkInfo instance + +@ProviderFor(networkInfo) +const networkInfoProvider = NetworkInfoProvider._(); + +/// Provider for NetworkInfo instance + +final class NetworkInfoProvider + extends $FunctionalProvider + with $Provider { + /// Provider for NetworkInfo instance + const NetworkInfoProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'networkInfoProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$networkInfoHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + NetworkInfo create(Ref ref) { + return networkInfo(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(NetworkInfo value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$networkInfoHash() => r'aee276b1536c8c994273dbed1909a2c24a7c71d2'; + +/// Provider for current network connection status (boolean) + +@ProviderFor(isConnected) +const isConnectedProvider = IsConnectedProvider._(); + +/// Provider for current network connection status (boolean) + +final class IsConnectedProvider + extends $FunctionalProvider, bool, FutureOr> + with $FutureModifier, $FutureProvider { + /// Provider for current network connection status (boolean) + const IsConnectedProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'isConnectedProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$isConnectedHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return isConnected(ref); + } +} + +String _$isConnectedHash() => r'c9620cadbcdee8e738f865e747dd57262236782d'; + +/// Provider for current network connection type + +@ProviderFor(connectionType) +const connectionTypeProvider = ConnectionTypeProvider._(); + +/// Provider for current network connection type + +final class ConnectionTypeProvider + extends + $FunctionalProvider< + AsyncValue, + NetworkConnectionType, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// Provider for current network connection type + const ConnectionTypeProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'connectionTypeProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$connectionTypeHash(); + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return connectionType(ref); + } +} + +String _$connectionTypeHash() => r'413aead6c4ff6f2c1476e4795934fddb76b797e6'; + +/// Stream provider for network status changes + +@ProviderFor(networkStatusStream) +const networkStatusStreamProvider = NetworkStatusStreamProvider._(); + +/// Stream provider for network status changes + +final class NetworkStatusStreamProvider + extends + $FunctionalProvider< + AsyncValue, + NetworkStatus, + Stream + > + with $FutureModifier, $StreamProvider { + /// Stream provider for network status changes + const NetworkStatusStreamProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'networkStatusStreamProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$networkStatusStreamHash(); + + @$internal + @override + $StreamProviderElement $createElement( + $ProviderPointer pointer, + ) => $StreamProviderElement(pointer); + + @override + Stream create(Ref ref) { + return networkStatusStream(ref); + } +} + +String _$networkStatusStreamHash() => + r'bdff8d93b214ebc290e81ab72fb8d51d8bfb27b1'; + +/// Provider for current network status + +@ProviderFor(NetworkStatusNotifier) +const networkStatusProvider = NetworkStatusNotifierProvider._(); + +/// Provider for current network status +final class NetworkStatusNotifierProvider + extends $AsyncNotifierProvider { + /// Provider for current network status + const NetworkStatusNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'networkStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$networkStatusNotifierHash(); + + @$internal + @override + NetworkStatusNotifier create() => NetworkStatusNotifier(); +} + +String _$networkStatusNotifierHash() => + r'628e313a66129282cd06dfdd561af3f0a4517b4f'; + +/// Provider for current network status + +abstract class _$NetworkStatusNotifier extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, NetworkStatus>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, NetworkStatus>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/core/providers/QUICK_REFERENCE.md b/lib/core/providers/QUICK_REFERENCE.md new file mode 100644 index 0000000..829062d --- /dev/null +++ b/lib/core/providers/QUICK_REFERENCE.md @@ -0,0 +1,462 @@ +# Riverpod 3.0 Quick Reference Card + +## File Structure +```dart +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'my_provider.g.dart'; // REQUIRED! + +// Your providers here +``` + +## Provider Types + +### 1. Simple Value (Immutable) +```dart +@riverpod +String appName(AppNameRef ref) => 'Worker App'; + +// Usage +final name = ref.watch(appNameProvider); +``` + +### 2. Async Value (Future) +```dart +@riverpod +Future user(UserRef ref, String id) async { + return await fetchUser(id); +} + +// Usage +final userAsync = ref.watch(userProvider('123')); +userAsync.when( + data: (user) => Text(user.name), + loading: () => CircularProgressIndicator(), + error: (e, _) => Text('Error: $e'), +); +``` + +### 3. Stream +```dart +@riverpod +Stream messages(MessagesRef ref) { + return webSocket.messages; +} + +// Usage +final messages = ref.watch(messagesProvider); +``` + +### 4. Mutable State (Notifier) +```dart +@riverpod +class Counter extends _$Counter { + @override + int build() => 0; + + void increment() => state++; + void decrement() => state--; +} + +// Usage +final count = ref.watch(counterProvider); +ref.read(counterProvider.notifier).increment(); +``` + +### 5. Async Mutable State (AsyncNotifier) +```dart +@riverpod +class Profile extends _$Profile { + @override + Future build() async { + return await api.getProfile(); + } + + Future update(String name) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + return await api.updateProfile(name); + }); + } +} + +// Usage +final profile = ref.watch(profileProvider); +await ref.read(profileProvider.notifier).update('New Name'); +``` + +## Family (Parameters) + +```dart +// Single parameter +@riverpod +Future post(PostRef ref, String id) async { + return await api.getPost(id); +} + +// Multiple parameters +@riverpod +Future> posts( + PostsRef ref, { + required String userId, + int page = 1, + String? category, +}) async { + return await api.getPosts(userId, page, category); +} + +// Usage +ref.watch(postProvider('post-123')); +ref.watch(postsProvider(userId: 'user-1', page: 2)); +``` + +## AutoDispose vs KeepAlive + +```dart +// AutoDispose (default) - cleaned up when not used +@riverpod +String temp(TempRef ref) => 'Auto disposed'; + +// KeepAlive - stays alive +@Riverpod(keepAlive: true) +String config(ConfigRef ref) => 'Global config'; +``` + +## Usage in Widgets + +### ConsumerWidget +```dart +class MyWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### ConsumerStatefulWidget +```dart +class MyWidget extends ConsumerStatefulWidget { + @override + ConsumerState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends ConsumerState { + @override + Widget build(BuildContext context) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### Consumer (optimization) +```dart +Consumer( + builder: (context, ref, child) { + final count = ref.watch(counterProvider); + return Text('$count'); + }, +) +``` + +## Ref Methods + +### ref.watch() - Use in build +```dart +// Rebuilds when value changes +final value = ref.watch(myProvider); +``` + +### ref.read() - Use in callbacks +```dart +// One-time read, doesn't listen +onPressed: () { + ref.read(myProvider.notifier).update(); +} +``` + +### ref.listen() - Side effects +```dart +ref.listen(authProvider, (prev, next) { + if (next.isLoggedOut) { + Navigator.of(context).pushReplacementNamed('/login'); + } +}); +``` + +### ref.invalidate() - Force refresh +```dart +ref.invalidate(userProvider); +``` + +### ref.refresh() - Invalidate and read +```dart +final newValue = ref.refresh(userProvider); +``` + +## AsyncValue Handling + +### .when() +```dart +asyncValue.when( + data: (value) => Text(value), + loading: () => CircularProgressIndicator(), + error: (error, stack) => Text('Error: $error'), +); +``` + +### Pattern Matching (Dart 3+) +```dart +switch (asyncValue) { + case AsyncData(:final value): + return Text(value); + case AsyncError(:final error): + return Text('Error: $error'); + case AsyncLoading(): + return CircularProgressIndicator(); +} +``` + +### Direct Checks +```dart +if (asyncValue.isLoading) return Loading(); +if (asyncValue.hasError) return Error(); +final data = asyncValue.value!; +``` + +## Performance Optimization + +### Use .select() +```dart +// Bad - rebuilds on any user change +final user = ref.watch(userProvider); + +// Good - rebuilds only when name changes +final name = ref.watch( + userProvider.select((user) => user.name), +); + +// With AsyncValue +final name = ref.watch( + userProvider.select((async) => async.value?.name), +); +``` + +## Error Handling + +### AsyncValue.guard() +```dart +@riverpod +class Data extends _$Data { + @override + Future build() async => 'Initial'; + + Future update(String value) async { + state = const AsyncValue.loading(); + + // Catches errors automatically + state = await AsyncValue.guard(() async { + return await api.update(value); + }); + } +} +``` + +## Provider Composition + +```dart +@riverpod +Future dashboard(DashboardRef ref) async { + // Depend on other providers + final user = await ref.watch(userProvider.future); + final posts = await ref.watch(postsProvider.future); + + return Dashboard(user: user, posts: posts); +} +``` + +## Lifecycle Hooks + +```dart +@riverpod +String example(ExampleRef ref) { + ref.onDispose(() { + // Cleanup + print('Disposed'); + }); + + ref.onCancel(() { + // Last listener removed + }); + + ref.onResume(() { + // New listener added + }); + + return 'value'; +} +``` + +## ref.mounted Check (Riverpod 3.0) + +```dart +@riverpod +class Example extends _$Example { + @override + String build() => 'Initial'; + + Future update() async { + await Future.delayed(Duration(seconds: 2)); + + // Check if still mounted + if (!ref.mounted) return; + + state = 'Updated'; + } +} +``` + +## Code Generation Commands + +```bash +# Watch mode (recommended) +dart run build_runner watch -d + +# One-time build +dart run build_runner build --delete-conflicting-outputs + +# Clean and rebuild +dart run build_runner clean && dart run build_runner build -d +``` + +## Linting + +```bash +# Check Riverpod issues +dart run custom_lint + +# Auto-fix +dart run custom_lint --fix +``` + +## Testing + +```dart +test('counter increments', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + expect(container.read(counterProvider), 0); + container.read(counterProvider.notifier).increment(); + expect(container.read(counterProvider), 1); +}); +``` + +## Widget Testing + +```dart +testWidgets('test', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + userProvider.overrideWith((ref) => User(name: 'Test')), + ], + child: MaterialApp(home: MyScreen()), + ), + ); + + expect(find.text('Test'), findsOneWidget); +}); +``` + +## Best Practices + +✅ **DO:** +- Use `ref.watch()` in build methods +- Use `ref.read()` in event handlers +- Use `.select()` to optimize rebuilds +- Check `ref.mounted` after async operations +- Use `AsyncValue.guard()` for error handling +- Use autoDispose for temporary state +- Keep providers in dedicated directories + +❌ **DON'T:** +- Use `ref.read()` in build methods +- Forget the `part` directive +- Use deprecated `StateNotifierProvider` +- Create providers without code generation +- Forget to run build_runner after changes + +## Common Patterns + +### Loading State +```dart +Future save() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => api.save()); +} +``` + +### Pagination +```dart +@riverpod +class PostList extends _$PostList { + @override + Future> build() => _fetch(0); + + int _page = 0; + + Future loadMore() async { + final current = state.value ?? []; + _page++; + + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final newPosts = await _fetch(_page); + return [...current, ...newPosts]; + }); + } + + Future> _fetch(int page) async { + return await api.getPosts(page: page); + } +} +``` + +### Form State +```dart +@riverpod +class LoginForm extends _$LoginForm { + @override + LoginFormState build() => LoginFormState(); + + void setEmail(String email) { + state = state.copyWith(email: email); + } + + void setPassword(String password) { + state = state.copyWith(password: password); + } + + Future submit() async { + if (!state.isValid) return; + + state = state.copyWith(isLoading: true); + try { + await api.login(state.email, state.password); + state = state.copyWith(isLoading: false, success: true); + } catch (e) { + state = state.copyWith(isLoading: false, error: e.toString()); + } + } +} +``` + +## Resources + +- 📄 provider_examples.dart - All patterns with examples +- 📄 connectivity_provider.dart - Real-world implementation +- 📄 RIVERPOD_SETUP.md - Complete guide +- 🌐 https://riverpod.dev - Official documentation diff --git a/lib/core/providers/README.md b/lib/core/providers/README.md new file mode 100644 index 0000000..624f3db --- /dev/null +++ b/lib/core/providers/README.md @@ -0,0 +1,454 @@ +# Riverpod 3.0 Provider Architecture + +This directory contains core-level providers that are used across the application. + +## Directory Structure + +``` +lib/core/providers/ + ├── connectivity_provider.dart # Network connectivity monitoring + ├── provider_examples.dart # Comprehensive Riverpod 3.0 examples + └── README.md # This file +``` + +## Setup Instructions + +### 1. Install Dependencies + +Dependencies are already configured in `pubspec.yaml`: + +```yaml +dependencies: + flutter_riverpod: ^3.0.0 + riverpod_annotation: ^3.0.0 + +dev_dependencies: + build_runner: ^2.4.11 + riverpod_generator: ^3.0.0 + riverpod_lint: ^3.0.0 + custom_lint: ^0.7.0 +``` + +Run to install: +```bash +flutter pub get +``` + +### 2. Generate Code + +Run code generation whenever you create or modify providers: + +```bash +# Watch mode (auto-regenerates on changes) +dart run build_runner watch -d + +# One-time build +dart run build_runner build -d + +# Clean and rebuild +dart run build_runner build --delete-conflicting-outputs +``` + +### 3. Wrap App with ProviderScope + +In `main.dart`: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +void main() { + runApp( + const ProviderScope( + child: MyApp(), + ), + ); +} +``` + +## Riverpod 3.0 Key Concepts + +### @riverpod Annotation + +The `@riverpod` annotation is the core of code generation. It automatically generates the appropriate provider type based on your function/class signature. + +```dart +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'my_provider.g.dart'; + +// Simple value +@riverpod +String myValue(MyValueRef ref) => 'Hello'; + +// Async value +@riverpod +Future myAsync(MyAsyncRef ref) async => 'Hello'; + +// Stream +@riverpod +Stream myStream(MyStreamRef ref) => Stream.value(1); + +// Mutable state +@riverpod +class MyNotifier extends _$MyNotifier { + @override + int build() => 0; + + void increment() => state++; +} + +// Async mutable state +@riverpod +class MyAsyncNotifier extends _$MyAsyncNotifier { + @override + Future build() async => 'Initial'; + + Future update(String value) async { + state = await AsyncValue.guard(() async => value); + } +} +``` + +### Provider Types (Auto-Generated) + +1. **Simple Provider** - Immutable value + - Function returning T → Provider + +2. **FutureProvider** - Async value + - Function returning Future → FutureProvider + +3. **StreamProvider** - Stream of values + - Function returning Stream → StreamProvider + +4. **NotifierProvider** - Mutable state with methods + - Class extending Notifier → NotifierProvider + +5. **AsyncNotifierProvider** - Async mutable state + - Class extending AsyncNotifier → AsyncNotifierProvider + +6. **StreamNotifierProvider** - Stream mutable state + - Class extending StreamNotifier → StreamNotifierProvider + +### Family (Parameters) + +In Riverpod 3.0, family is just function parameters! + +```dart +// Old way (Riverpod 2.x) +final userProvider = FutureProvider.family((ref, id) async { + return fetchUser(id); +}); + +// New way (Riverpod 3.0) +@riverpod +Future user(UserRef ref, String id) async { + return fetchUser(id); +} + +// Multiple parameters (named, optional, defaults) +@riverpod +Future> posts( + PostsRef ref, { + required String userId, + int page = 1, + int limit = 20, + String? category, +}) async { + return fetchPosts(userId, page, limit, category); +} + +// Usage +ref.watch(userProvider('user123')); +ref.watch(postsProvider(userId: 'user123', page: 2, category: 'tech')); +``` + +### AutoDispose vs KeepAlive + +```dart +// AutoDispose (default) - cleaned up when not used +@riverpod +String autoExample(AutoExampleRef ref) => 'Auto disposed'; + +// KeepAlive - stays alive until app closes +@Riverpod(keepAlive: true) +String keepExample(KeepExampleRef ref) => 'Kept alive'; +``` + +### Unified Ref Type + +Riverpod 3.0 uses a single `Ref` type (no more FutureProviderRef, StreamProviderRef, etc.): + +```dart +@riverpod +Future example(ExampleRef ref) async { + // All providers use the same ref type + ref.watch(otherProvider); + ref.read(anotherProvider); + ref.listen(thirdProvider, (prev, next) {}); + ref.invalidate(fourthProvider); +} +``` + +### ref.mounted Check + +Always check `ref.mounted` after async operations in Notifiers: + +```dart +@riverpod +class Example extends _$Example { + @override + String build() => 'Initial'; + + Future updateData() async { + await Future.delayed(Duration(seconds: 2)); + + // Check if provider is still mounted + if (!ref.mounted) return; + + state = 'Updated'; + } +} +``` + +### Error Handling with AsyncValue.guard() + +```dart +@riverpod +class Data extends _$Data { + @override + Future build() async => 'Initial'; + + Future update(String value) async { + state = const AsyncValue.loading(); + + // AsyncValue.guard catches errors automatically + state = await AsyncValue.guard(() async { + await api.update(value); + return value; + }); + } +} +``` + +## Usage in Widgets + +### ConsumerWidget + +```dart +class MyWidget extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### ConsumerStatefulWidget + +```dart +class MyWidget extends ConsumerStatefulWidget { + @override + ConsumerState createState() => _MyWidgetState(); +} + +class _MyWidgetState extends ConsumerState { + @override + void initState() { + super.initState(); + // ref is available in all lifecycle methods + } + + @override + Widget build(BuildContext context) { + final value = ref.watch(myProvider); + return Text(value); + } +} +``` + +### Consumer (for optimization) + +```dart +Column( + children: [ + const Text('Static'), + Consumer( + builder: (context, ref, child) { + final count = ref.watch(counterProvider); + return Text('$count'); + }, + ), + ], +) +``` + +## Best Practices + +### 1. Use .select() for Optimization + +```dart +// Bad - rebuilds on any user change +final user = ref.watch(userProvider); + +// Good - rebuilds only when name changes +final name = ref.watch(userProvider.select((user) => user.name)); + +// Good - rebuilds only when async value has data +final userName = ref.watch( + userProvider.select((async) => async.value?.name), +); +``` + +### 2. Provider Dependencies + +```dart +@riverpod +Future dashboard(DashboardRef ref) async { + // Watch other providers + final user = await ref.watch(userProvider.future); + final posts = await ref.watch(postsProvider.future); + + return Dashboard(user: user, posts: posts); +} +``` + +### 3. Invalidation and Refresh + +```dart +// In a widget or notifier +ref.invalidate(userProvider); // Invalidate +ref.refresh(userProvider); // Invalidate and re-read +``` + +### 4. Lifecycle Hooks + +```dart +@riverpod +String example(ExampleRef ref) { + ref.onDispose(() { + // Clean up + }); + + ref.onCancel(() { + // Last listener removed + }); + + ref.onResume(() { + // New listener added after cancel + }); + + return 'value'; +} +``` + +### 5. Testing + +```dart +test('counter increments', () { + final container = ProviderContainer(); + addTearDown(container.dispose); + + expect(container.read(counterProvider), 0); + container.read(counterProvider.notifier).increment(); + expect(container.read(counterProvider), 1); +}); + +test('async provider', () async { + final container = ProviderContainer(); + addTearDown(container.dispose); + + final value = await container.read(userProvider.future); + expect(value.name, 'John'); +}); +``` + +## Migration from Riverpod 2.x + +### StateNotifierProvider → NotifierProvider + +```dart +// Old (2.x) +class Counter extends StateNotifier { + Counter() : super(0); + void increment() => state++; +} +final counterProvider = StateNotifierProvider(Counter.new); + +// New (3.0) +@riverpod +class Counter extends _$Counter { + @override + int build() => 0; + void increment() => state++; +} +``` + +### FutureProvider.family → Function with Parameters + +```dart +// Old (2.x) +final userProvider = FutureProvider.family((ref, id) async { + return fetchUser(id); +}); + +// New (3.0) +@riverpod +Future user(UserRef ref, String id) async { + return fetchUser(id); +} +``` + +### Ref Types → Single Ref + +```dart +// Old (2.x) +final provider = FutureProvider((FutureProviderRef ref) async { + return 'value'; +}); + +// New (3.0) +@riverpod +Future provider(ProviderRef ref) async { + return 'value'; +} +``` + +## Common Patterns + +See `provider_examples.dart` for comprehensive examples of: +- Simple providers +- Async providers (FutureProvider pattern) +- Stream providers +- Notifier (mutable state) +- AsyncNotifier (async mutable state) +- StreamNotifier +- Family (parameters) +- Provider composition +- Error handling +- Lifecycle hooks +- Optimization with .select() + +## Riverpod Lint Rules + +The project is configured with `riverpod_lint` for additional checks: +- `provider_dependencies` - Ensure proper dependency usage +- `scoped_providers_should_specify_dependencies` - Scoped provider safety +- `avoid_public_notifier_properties` - Encapsulation +- `avoid_ref_read_inside_build` - Performance +- `avoid_manual_providers_as_generated_provider_dependency` - Use generated providers +- `functional_ref` - Proper ref usage +- `notifier_build` - Proper Notifier implementation + +Run linting: +```bash +dart run custom_lint +``` + +## Resources + +- [Riverpod Documentation](https://riverpod.dev) +- [Code Generation Guide](https://riverpod.dev/docs/concepts/about_code_generation) +- [Migration Guide](https://riverpod.dev/docs/migration/from_state_notifier) +- [Provider Examples](./provider_examples.dart) diff --git a/lib/core/providers/connectivity_provider.dart b/lib/core/providers/connectivity_provider.dart new file mode 100644 index 0000000..bd909a4 --- /dev/null +++ b/lib/core/providers/connectivity_provider.dart @@ -0,0 +1,113 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connectivity_provider.g.dart'; + +/// Enum representing connectivity status +enum ConnectivityStatus { + /// Connected to WiFi + wifi, + + /// Connected to mobile data + mobile, + + /// No internet connection + offline, +} + +/// Provider for the Connectivity instance +/// This is a simple provider that returns a singleton instance +@riverpod +Connectivity connectivity(Ref ref) { + return Connectivity(); +} + +/// Stream provider that monitors real-time connectivity changes +/// This automatically updates whenever the device connectivity changes +/// +/// Usage: +/// ```dart +/// final connectivityState = ref.watch(connectivityStreamProvider); +/// connectivityState.when( +/// data: (status) => Text('Status: $status'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` +@riverpod +Stream connectivityStream(Ref ref) { + final connectivity = ref.watch(connectivityProvider); + + return connectivity.onConnectivityChanged.map((result) { + // Handle the List from connectivity_plus + if (result.contains(ConnectivityResult.wifi)) { + return ConnectivityStatus.wifi; + } else if (result.contains(ConnectivityResult.mobile)) { + return ConnectivityStatus.mobile; + } else { + return ConnectivityStatus.offline; + } + }); +} + +/// Provider that checks current connectivity status once +/// This is useful for one-time checks without listening to changes +/// +/// Usage: +/// ```dart +/// final status = await ref.read(currentConnectivityProvider.future); +/// if (status == ConnectivityStatus.offline) { +/// showOfflineDialog(); +/// } +/// ``` +@riverpod +Future currentConnectivity(Ref ref) async { + final connectivity = ref.watch(connectivityProvider); + final result = await connectivity.checkConnectivity(); + + // Handle the List + if (result.contains(ConnectivityResult.wifi)) { + return ConnectivityStatus.wifi; + } else if (result.contains(ConnectivityResult.mobile)) { + return ConnectivityStatus.mobile; + } else { + return ConnectivityStatus.offline; + } +} + +/// Provider that returns whether the device is currently online +/// Convenient boolean check for connectivity +/// +/// Usage: +/// ```dart +/// final isOnlineAsync = ref.watch(isOnlineProvider); +/// isOnlineAsync.when( +/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` +@riverpod +Stream isOnline(Ref ref) { + // Get the connectivity stream and map it to a boolean + final connectivity = ref.watch(connectivityProvider); + + return connectivity.onConnectivityChanged.map((result) { + // Online if connected to WiFi or mobile + return result.contains(ConnectivityResult.wifi) || + result.contains(ConnectivityResult.mobile); + }); +} + +/// Example of using .select() for optimization +/// Only rebuilds when the online status changes, not on WiFi<->Mobile switches +/// +/// Usage: +/// ```dart +/// // This only rebuilds when going online/offline +/// final isOnline = ref.watch( +/// connectivityStreamProvider.select((async) => +/// async.value != ConnectivityStatus.offline +/// ), +/// ); +/// ``` diff --git a/lib/core/providers/connectivity_provider.g.dart b/lib/core/providers/connectivity_provider.g.dart new file mode 100644 index 0000000..50ba65d --- /dev/null +++ b/lib/core/providers/connectivity_provider.g.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'connectivity_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider for the Connectivity instance +/// This is a simple provider that returns a singleton instance + +@ProviderFor(connectivity) +const connectivityProvider = ConnectivityProvider._(); + +/// Provider for the Connectivity instance +/// This is a simple provider that returns a singleton instance + +final class ConnectivityProvider + extends $FunctionalProvider + with $Provider { + /// Provider for the Connectivity instance + /// This is a simple provider that returns a singleton instance + const ConnectivityProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'connectivityProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$connectivityHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + Connectivity create(Ref ref) { + return connectivity(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Connectivity value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda'; + +/// Stream provider that monitors real-time connectivity changes +/// This automatically updates whenever the device connectivity changes +/// +/// Usage: +/// ```dart +/// final connectivityState = ref.watch(connectivityStreamProvider); +/// connectivityState.when( +/// data: (status) => Text('Status: $status'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` + +@ProviderFor(connectivityStream) +const connectivityStreamProvider = ConnectivityStreamProvider._(); + +/// Stream provider that monitors real-time connectivity changes +/// This automatically updates whenever the device connectivity changes +/// +/// Usage: +/// ```dart +/// final connectivityState = ref.watch(connectivityStreamProvider); +/// connectivityState.when( +/// data: (status) => Text('Status: $status'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` + +final class ConnectivityStreamProvider + extends + $FunctionalProvider< + AsyncValue, + ConnectivityStatus, + Stream + > + with + $FutureModifier, + $StreamProvider { + /// Stream provider that monitors real-time connectivity changes + /// This automatically updates whenever the device connectivity changes + /// + /// Usage: + /// ```dart + /// final connectivityState = ref.watch(connectivityStreamProvider); + /// connectivityState.when( + /// data: (status) => Text('Status: $status'), + /// loading: () => CircularProgressIndicator(), + /// error: (error, _) => Text('Error: $error'), + /// ); + /// ``` + const ConnectivityStreamProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'connectivityStreamProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$connectivityStreamHash(); + + @$internal + @override + $StreamProviderElement $createElement( + $ProviderPointer pointer, + ) => $StreamProviderElement(pointer); + + @override + Stream create(Ref ref) { + return connectivityStream(ref); + } +} + +String _$connectivityStreamHash() => + r'207d7c426c0182225f4d1fd2014b9bc6c667fd67'; + +/// Provider that checks current connectivity status once +/// This is useful for one-time checks without listening to changes +/// +/// Usage: +/// ```dart +/// final status = await ref.read(currentConnectivityProvider.future); +/// if (status == ConnectivityStatus.offline) { +/// showOfflineDialog(); +/// } +/// ``` + +@ProviderFor(currentConnectivity) +const currentConnectivityProvider = CurrentConnectivityProvider._(); + +/// Provider that checks current connectivity status once +/// This is useful for one-time checks without listening to changes +/// +/// Usage: +/// ```dart +/// final status = await ref.read(currentConnectivityProvider.future); +/// if (status == ConnectivityStatus.offline) { +/// showOfflineDialog(); +/// } +/// ``` + +final class CurrentConnectivityProvider + extends + $FunctionalProvider< + AsyncValue, + ConnectivityStatus, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// Provider that checks current connectivity status once + /// This is useful for one-time checks without listening to changes + /// + /// Usage: + /// ```dart + /// final status = await ref.read(currentConnectivityProvider.future); + /// if (status == ConnectivityStatus.offline) { + /// showOfflineDialog(); + /// } + /// ``` + const CurrentConnectivityProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentConnectivityProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentConnectivityHash(); + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return currentConnectivity(ref); + } +} + +String _$currentConnectivityHash() => + r'bf11d5eef553f9476a8b667e68572268bc25c9fb'; + +/// Provider that returns whether the device is currently online +/// Convenient boolean check for connectivity +/// +/// Usage: +/// ```dart +/// final isOnlineAsync = ref.watch(isOnlineProvider); +/// isOnlineAsync.when( +/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` + +@ProviderFor(isOnline) +const isOnlineProvider = IsOnlineProvider._(); + +/// Provider that returns whether the device is currently online +/// Convenient boolean check for connectivity +/// +/// Usage: +/// ```dart +/// final isOnlineAsync = ref.watch(isOnlineProvider); +/// isOnlineAsync.when( +/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), +/// loading: () => CircularProgressIndicator(), +/// error: (error, _) => Text('Error: $error'), +/// ); +/// ``` + +final class IsOnlineProvider + extends $FunctionalProvider, bool, Stream> + with $FutureModifier, $StreamProvider { + /// Provider that returns whether the device is currently online + /// Convenient boolean check for connectivity + /// + /// Usage: + /// ```dart + /// final isOnlineAsync = ref.watch(isOnlineProvider); + /// isOnlineAsync.when( + /// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), + /// loading: () => CircularProgressIndicator(), + /// error: (error, _) => Text('Error: $error'), + /// ); + /// ``` + const IsOnlineProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'isOnlineProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$isOnlineHash(); + + @$internal + @override + $StreamProviderElement $createElement($ProviderPointer pointer) => + $StreamProviderElement(pointer); + + @override + Stream create(Ref ref) { + return isOnline(ref); + } +} + +String _$isOnlineHash() => r'09f68fd322b995ffdc28fab6249d8b80108512c4'; diff --git a/lib/core/providers/provider_examples.dart b/lib/core/providers/provider_examples.dart new file mode 100644 index 0000000..5c7acdd --- /dev/null +++ b/lib/core/providers/provider_examples.dart @@ -0,0 +1,474 @@ +// ignore_for_file: unreachable_from_main + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'provider_examples.g.dart'; + +/// ============================================================================ +/// RIVERPOD 3.0 PROVIDER EXAMPLES WITH CODE GENERATION +/// ============================================================================ +/// This file contains comprehensive examples of Riverpod 3.0 patterns +/// using the @riverpod annotation and code generation. +/// +/// Key Changes in Riverpod 3.0: +/// - Unified Ref type (no more FutureProviderRef, StreamProviderRef, etc.) +/// - Simplified Notifier classes (no more separate AutoDisposeNotifier) +/// - Automatic retry for failed providers +/// - ref.mounted check after async operations +/// - Improved family parameters (just function parameters!) +/// ============================================================================ + +// ============================================================================ +// 1. SIMPLE IMMUTABLE VALUE PROVIDER +// ============================================================================ + +/// Simple provider that returns an immutable value +/// Use this for constants, configurations, or computed values +@riverpod +String appVersion(Ref ref) { + return '1.0.0'; +} + +/// Provider with computation +@riverpod +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; +} + +// ============================================================================ +// 2. ASYNC DATA FETCHING (FutureProvider pattern) +// ============================================================================ + +/// Async provider for fetching data once +/// Automatically handles loading and error states via AsyncValue +@riverpod +Future userData(Ref ref) async { + // Simulate API call + await Future.delayed(const Duration(seconds: 1)); + return 'User Data'; +} + +/// Async provider with parameters (Family pattern) +/// Parameters are just function parameters - much simpler than before! +@riverpod +Future userProfile(Ref ref, String userId) async { + // Simulate API call with userId + await Future.delayed(const Duration(seconds: 1)); + return 'Profile for user: $userId'; +} + +/// Async provider with multiple parameters +/// Named parameters, optional parameters, defaults - all supported! +@riverpod +Future> productList( + Ref ref, { + required String category, + int page = 1, + int limit = 20, + String? searchQuery, +}) async { + // Simulate API call with parameters + await Future.delayed(const Duration(milliseconds: 500)); + return ['Product 1', 'Product 2', 'Product 3']; +} + +// ============================================================================ +// 3. STREAM PROVIDER +// ============================================================================ + +/// Stream provider for real-time data +/// Use this for WebSocket connections, real-time updates, etc. +@riverpod +Stream timer(Ref ref) { + return Stream.periodic( + const Duration(seconds: 1), + (count) => count, + ); +} + +/// Stream provider with parameters +@riverpod +Stream chatMessages(Ref ref, String roomId) { + // Simulate WebSocket stream + return Stream.periodic( + const Duration(seconds: 2), + (count) => 'Message $count in room $roomId', + ); +} + +// ============================================================================ +// 4. NOTIFIER - MUTABLE STATE WITH METHODS +// ============================================================================ + +/// Notifier for mutable state with methods +/// Use this when you need to expose methods to modify state +/// +/// The @riverpod annotation generates: +/// - counterProvider: Access the state +/// - counterProvider.notifier: Access the notifier methods +@riverpod +class Counter extends _$Counter { + /// Build method returns the initial state + @override + int build() { + return 0; + } + + /// Methods to modify state + void increment() { + state++; + } + + void decrement() { + state--; + } + + void reset() { + state = 0; + } + + void add(int value) { + state += value; + } +} + +/// Notifier with parameters (Family pattern) +@riverpod +class CartQuantity extends _$CartQuantity { + /// Parameters become properties you can access + @override + int build(String productId) { + // Initialize with 0 or load from local storage + return 0; + } + + void increment() { + state++; + } + + void decrement() { + if (state > 0) { + state--; + } + } + + void setQuantity(int quantity) { + state = quantity.clamp(0, 99); + } +} + +// ============================================================================ +// 5. ASYNC NOTIFIER - MUTABLE STATE WITH ASYNC INITIALIZATION +// ============================================================================ + +/// AsyncNotifier for state that requires async initialization +/// Perfect for fetching data that can then be modified +/// +/// State type: AsyncValue +/// - AsyncValue.data(profile) when loaded +/// - AsyncValue.loading() when loading +/// - AsyncValue.error(error, stack) when error +@riverpod +class UserProfileNotifier extends _$UserProfileNotifier { + /// Build method returns Future of the initial state + @override + Future build() async { + // Fetch initial data + await Future.delayed(const Duration(seconds: 1)); + return UserProfileData(name: 'John Doe', email: 'john@example.com'); + } + + /// Method to update profile + /// Uses AsyncValue.guard() for proper error handling + Future updateName(String name) async { + // Set loading state + state = const AsyncValue.loading(); + + // Update state with error handling + state = await AsyncValue.guard(() async { + await Future.delayed(const Duration(milliseconds: 500)); + + final currentProfile = await future; // Get current data + return currentProfile.copyWith(name: name); + }); + } + + /// Refresh data + Future refresh() async { + state = const AsyncValue.loading(); + + state = await AsyncValue.guard(() async { + await Future.delayed(const Duration(seconds: 1)); + return UserProfileData(name: 'Refreshed', email: 'refresh@example.com'); + }); + } + + /// Method with ref.mounted check (Riverpod 3.0 feature) + Future updateWithMountedCheck(String name) async { + await Future.delayed(const Duration(seconds: 2)); + + // Check if provider is still mounted after async operation + if (!ref.mounted) return; + + state = AsyncValue.data( + UserProfileData(name: name, email: 'email@example.com'), + ); + } +} + +// Simple data class for example +class UserProfileData { + UserProfileData({required this.name, required this.email}); + final String name; + final String email; + + UserProfileData copyWith({String? name, String? email}) { + return UserProfileData( + name: name ?? this.name, + email: email ?? this.email, + ); + } +} + +// ============================================================================ +// 6. STREAM NOTIFIER - MUTABLE STATE FROM STREAM +// ============================================================================ + +/// StreamNotifier for state that comes from a stream but can be modified +/// Use for WebSocket connections with additional actions +@riverpod +class LiveChatNotifier extends _$LiveChatNotifier { + @override + Stream> build() { + // Return the stream + return Stream.periodic( + const Duration(seconds: 1), + (count) => ['Message $count'], + ); + } + + /// Send a new message + Future sendMessage(String message) async { + // Send via WebSocket/API + await Future.delayed(const Duration(milliseconds: 100)); + } +} + +// ============================================================================ +// 7. PROVIDER DEPENDENCIES (COMPOSITION) +// ============================================================================ + +/// Provider that depends on other providers +@riverpod +Future dashboardData(Ref ref) async { + // Watch other providers + final userData = await ref.watch(userDataProvider.future); + final version = ref.watch(appVersionProvider); + + return 'Dashboard for $userData on version $version'; +} + +/// Provider that selectively watches for optimization +/// Note: userProfileProvider is actually a Family provider (takes userId parameter) +/// In real code, you would use the generated AsyncNotifier provider +@riverpod +String userDisplayName(Ref ref) { + // Example: Watch a simple computed value based on other providers + final version = ref.watch(appVersionProvider); + + // In a real app, you would watch the UserProfileNotifier like: + // final asyncProfile = ref.watch(userProfileNotifierProvider); + // return asyncProfile.when(...) + + return 'User on version $version'; +} + +// ============================================================================ +// 8. KEEP ALIVE vs AUTO DISPOSE +// ============================================================================ + +/// By default, generated providers are autoDispose +/// They clean up when no longer used +@riverpod +String autoDisposeExample(Ref ref) { + // This provider will be disposed when no longer watched + ref.onDispose(() { + // Clean up resources + }); + + return 'Auto disposed'; +} + +/// Keep alive provider - never auto-disposes +/// Use for global state, singletons, services +@Riverpod(keepAlive: true) +String keepAliveExample(Ref ref) { + // This provider stays alive until the app closes + return 'Kept alive'; +} + +// ============================================================================ +// 9. LIFECYCLE HOOKS +// ============================================================================ + +/// Provider with lifecycle hooks +@riverpod +String lifecycleExample(Ref ref) { + // Called when provider is first created + ref.onDispose(() { + // Clean up when provider is disposed + print('Provider disposed'); + }); + + ref.onCancel(() { + // Called when last listener is removed (before dispose) + print('Last listener removed'); + }); + + ref.onResume(() { + // Called when a new listener is added after onCancel + print('New listener added'); + }); + + return 'Lifecycle example'; +} + +// ============================================================================ +// 10. INVALIDATION AND REFRESH +// ============================================================================ + +/// Provider showing how to invalidate and refresh +@riverpod +class DataManager extends _$DataManager { + @override + Future build() async { + await Future.delayed(const Duration(seconds: 1)); + return 'Initial data'; + } + + /// Refresh this provider's data + Future refresh() async { + // Method 1: Use ref.invalidateSelf() + ref.invalidateSelf(); + + // Method 2: Manually update state + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + await Future.delayed(const Duration(seconds: 1)); + return 'Refreshed data'; + }); + } + + /// Invalidate another provider + void invalidateOther() { + ref.invalidate(userDataProvider); + } +} + +// ============================================================================ +// 11. ERROR HANDLING PATTERNS +// ============================================================================ + +/// Provider with comprehensive error handling +@riverpod +class ErrorHandlingExample extends _$ErrorHandlingExample { + @override + Future build() async { + return await _fetchData(); + } + + Future _fetchData() async { + try { + // Simulate API call + await Future.delayed(const Duration(seconds: 1)); + return 'Success'; + } catch (e) { + // Errors are automatically caught by AsyncValue + rethrow; + } + } + + /// Update with error handling + Future updateData(String newData) async { + state = const AsyncValue.loading(); + + // AsyncValue.guard automatically catches errors + state = await AsyncValue.guard(() async { + await Future.delayed(const Duration(milliseconds: 500)); + return newData; + }); + + // Handle the result + state.when( + data: (data) { + // Success + print('Updated successfully: $data'); + }, + loading: () { + // Still loading + }, + error: (error, stack) { + // Handle error + print('Update failed: $error'); + }, + ); + } +} + +// ============================================================================ +// USAGE IN WIDGETS +// ============================================================================ + +/* + +// 1. Watching a simple provider +final version = ref.watch(appVersionProvider); + +// 2. Watching async provider +final userData = ref.watch(userDataProvider); +userData.when( + data: (data) => Text(data), + loading: () => CircularProgressIndicator(), + error: (error, stack) => Text('Error: $error'), +); + +// 3. Watching with family (parameters) +final profile = ref.watch(userProfileProvider('user123')); + +// 4. Watching state from Notifier +final count = ref.watch(counterProvider); + +// 5. Calling Notifier methods +ref.read(counterProvider.notifier).increment(); + +// 6. Watching AsyncNotifier state +final profileState = ref.watch(userProfileNotifierProvider); + +// 7. Calling AsyncNotifier methods +await ref.read(userProfileNotifierProvider.notifier).updateName('New Name'); + +// 8. Selective watching for optimization +final userName = ref.watch( + userProfileNotifierProvider.select((async) => async.value?.name), +); + +// 9. Invalidating a provider +ref.invalidate(userDataProvider); + +// 10. Refreshing a provider +ref.refresh(userDataProvider); + +// 11. Pattern matching (Dart 3.0+) +final profileState = ref.watch(userProfileNotifierProvider); +switch (profileState) { + case AsyncData(:final value): + return Text(value.name); + case AsyncError(:final error): + return Text('Error: $error'); + case AsyncLoading(): + return CircularProgressIndicator(); +} + +*/ diff --git a/lib/core/providers/provider_examples.g.dart b/lib/core/providers/provider_examples.g.dart new file mode 100644 index 0000000..5fe0d67 --- /dev/null +++ b/lib/core/providers/provider_examples.g.dart @@ -0,0 +1,1155 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'provider_examples.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// ============================================================================ +/// RIVERPOD 3.0 PROVIDER EXAMPLES WITH CODE GENERATION +/// ============================================================================ +/// This file contains comprehensive examples of Riverpod 3.0 patterns +/// using the @riverpod annotation and code generation. +/// +/// Key Changes in Riverpod 3.0: +/// - Unified Ref type (no more FutureProviderRef, StreamProviderRef, etc.) +/// - Simplified Notifier classes (no more separate AutoDisposeNotifier) +/// - Automatic retry for failed providers +/// - ref.mounted check after async operations +/// - Improved family parameters (just function parameters!) +/// ============================================================================ +// ============================================================================ +// 1. SIMPLE IMMUTABLE VALUE PROVIDER +// ============================================================================ +/// Simple provider that returns an immutable value +/// Use this for constants, configurations, or computed values + +@ProviderFor(appVersion) +const appVersionProvider = AppVersionProvider._(); + +/// ============================================================================ +/// RIVERPOD 3.0 PROVIDER EXAMPLES WITH CODE GENERATION +/// ============================================================================ +/// This file contains comprehensive examples of Riverpod 3.0 patterns +/// using the @riverpod annotation and code generation. +/// +/// Key Changes in Riverpod 3.0: +/// - Unified Ref type (no more FutureProviderRef, StreamProviderRef, etc.) +/// - Simplified Notifier classes (no more separate AutoDisposeNotifier) +/// - Automatic retry for failed providers +/// - ref.mounted check after async operations +/// - Improved family parameters (just function parameters!) +/// ============================================================================ +// ============================================================================ +// 1. SIMPLE IMMUTABLE VALUE PROVIDER +// ============================================================================ +/// Simple provider that returns an immutable value +/// Use this for constants, configurations, or computed values + +final class AppVersionProvider + extends $FunctionalProvider + with $Provider { + /// ============================================================================ + /// RIVERPOD 3.0 PROVIDER EXAMPLES WITH CODE GENERATION + /// ============================================================================ + /// This file contains comprehensive examples of Riverpod 3.0 patterns + /// using the @riverpod annotation and code generation. + /// + /// Key Changes in Riverpod 3.0: + /// - Unified Ref type (no more FutureProviderRef, StreamProviderRef, etc.) + /// - Simplified Notifier classes (no more separate AutoDisposeNotifier) + /// - Automatic retry for failed providers + /// - ref.mounted check after async operations + /// - Improved family parameters (just function parameters!) + /// ============================================================================ + // ============================================================================ + // 1. SIMPLE IMMUTABLE VALUE PROVIDER + // ============================================================================ + /// Simple provider that returns an immutable value + /// Use this for constants, configurations, or computed values + const AppVersionProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'appVersionProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$appVersionHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + String create(Ref ref) { + return appVersion(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$appVersionHash() => r'0c30e6c7150456c44f403b85070b580ece180d49'; + +/// Provider with computation + +@ProviderFor(pointsMultiplier) +const pointsMultiplierProvider = PointsMultiplierProvider._(); + +/// Provider with computation + +final class PointsMultiplierProvider extends $FunctionalProvider + with $Provider { + /// Provider with computation + const PointsMultiplierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'pointsMultiplierProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$pointsMultiplierHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + int create(Ref ref) { + return pointsMultiplier(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$pointsMultiplierHash() => r'18c7271aca98b3352644383f5fb67aba330382a0'; + +/// Async provider for fetching data once +/// Automatically handles loading and error states via AsyncValue + +@ProviderFor(userData) +const userDataProvider = UserDataProvider._(); + +/// Async provider for fetching data once +/// Automatically handles loading and error states via AsyncValue + +final class UserDataProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + /// Async provider for fetching data once + /// Automatically handles loading and error states via AsyncValue + const UserDataProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userDataProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userDataHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return userData(ref); + } +} + +String _$userDataHash() => r'3df905d6ea9f81ce7ca8205bd785ad4d4376b399'; + +/// Async provider with parameters (Family pattern) +/// Parameters are just function parameters - much simpler than before! + +@ProviderFor(userProfile) +const userProfileProvider = UserProfileFamily._(); + +/// Async provider with parameters (Family pattern) +/// Parameters are just function parameters - much simpler than before! + +final class UserProfileProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + /// Async provider with parameters (Family pattern) + /// Parameters are just function parameters - much simpler than before! + const UserProfileProvider._({ + required UserProfileFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'userProfileProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userProfileHash(); + + @override + String toString() { + return r'userProfileProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return userProfile(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is UserProfileProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$userProfileHash() => r'd42ed517f41ce0dfde58d74b2beb3d8415b81a22'; + +/// Async provider with parameters (Family pattern) +/// Parameters are just function parameters - much simpler than before! + +final class UserProfileFamily extends $Family + with $FunctionalFamilyOverride, String> { + const UserProfileFamily._() + : super( + retry: null, + name: r'userProfileProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Async provider with parameters (Family pattern) + /// Parameters are just function parameters - much simpler than before! + + UserProfileProvider call(String userId) => + UserProfileProvider._(argument: userId, from: this); + + @override + String toString() => r'userProfileProvider'; +} + +/// Async provider with multiple parameters +/// Named parameters, optional parameters, defaults - all supported! + +@ProviderFor(productList) +const productListProvider = ProductListFamily._(); + +/// Async provider with multiple parameters +/// Named parameters, optional parameters, defaults - all supported! + +final class ProductListProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with $FutureModifier>, $FutureProvider> { + /// Async provider with multiple parameters + /// Named parameters, optional parameters, defaults - all supported! + const ProductListProvider._({ + required ProductListFamily super.from, + required ({String category, int page, int limit, String? searchQuery}) + super.argument, + }) : super( + retry: null, + name: r'productListProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$productListHash(); + + @override + String toString() { + return r'productListProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = + this.argument + as ({String category, int page, int limit, String? searchQuery}); + return productList( + ref, + category: argument.category, + page: argument.page, + limit: argument.limit, + searchQuery: argument.searchQuery, + ); + } + + @override + bool operator ==(Object other) { + return other is ProductListProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$productListHash() => r'aacee7761543692ccd59f0ecd2f290e1a7de203a'; + +/// Async provider with multiple parameters +/// Named parameters, optional parameters, defaults - all supported! + +final class ProductListFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr>, + ({String category, int page, int limit, String? searchQuery}) + > { + const ProductListFamily._() + : super( + retry: null, + name: r'productListProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Async provider with multiple parameters + /// Named parameters, optional parameters, defaults - all supported! + + ProductListProvider call({ + required String category, + int page = 1, + int limit = 20, + String? searchQuery, + }) => ProductListProvider._( + argument: ( + category: category, + page: page, + limit: limit, + searchQuery: searchQuery, + ), + from: this, + ); + + @override + String toString() => r'productListProvider'; +} + +/// Stream provider for real-time data +/// Use this for WebSocket connections, real-time updates, etc. + +@ProviderFor(timer) +const timerProvider = TimerProvider._(); + +/// Stream provider for real-time data +/// Use this for WebSocket connections, real-time updates, etc. + +final class TimerProvider + extends $FunctionalProvider, int, Stream> + with $FutureModifier, $StreamProvider { + /// Stream provider for real-time data + /// Use this for WebSocket connections, real-time updates, etc. + const TimerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'timerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$timerHash(); + + @$internal + @override + $StreamProviderElement $createElement($ProviderPointer pointer) => + $StreamProviderElement(pointer); + + @override + Stream create(Ref ref) { + return timer(ref); + } +} + +String _$timerHash() => r'07bead4a22fc812c185b5c9cf9cd1bd3226286a3'; + +/// Stream provider with parameters + +@ProviderFor(chatMessages) +const chatMessagesProvider = ChatMessagesFamily._(); + +/// Stream provider with parameters + +final class ChatMessagesProvider + extends $FunctionalProvider, String, Stream> + with $FutureModifier, $StreamProvider { + /// Stream provider with parameters + const ChatMessagesProvider._({ + required ChatMessagesFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'chatMessagesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$chatMessagesHash(); + + @override + String toString() { + return r'chatMessagesProvider' + '' + '($argument)'; + } + + @$internal + @override + $StreamProviderElement $createElement($ProviderPointer pointer) => + $StreamProviderElement(pointer); + + @override + Stream create(Ref ref) { + final argument = this.argument as String; + return chatMessages(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is ChatMessagesProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$chatMessagesHash() => r'c4b270618336d682c83f92d1597e21390fe7826c'; + +/// Stream provider with parameters + +final class ChatMessagesFamily extends $Family + with $FunctionalFamilyOverride, String> { + const ChatMessagesFamily._() + : super( + retry: null, + name: r'chatMessagesProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Stream provider with parameters + + ChatMessagesProvider call(String roomId) => + ChatMessagesProvider._(argument: roomId, from: this); + + @override + String toString() => r'chatMessagesProvider'; +} + +/// Notifier for mutable state with methods +/// Use this when you need to expose methods to modify state +/// +/// The @riverpod annotation generates: +/// - counterProvider: Access the state +/// - counterProvider.notifier: Access the notifier methods + +@ProviderFor(Counter) +const counterProvider = CounterProvider._(); + +/// Notifier for mutable state with methods +/// Use this when you need to expose methods to modify state +/// +/// The @riverpod annotation generates: +/// - counterProvider: Access the state +/// - counterProvider.notifier: Access the notifier methods +final class CounterProvider extends $NotifierProvider { + /// Notifier for mutable state with methods + /// Use this when you need to expose methods to modify state + /// + /// The @riverpod annotation generates: + /// - counterProvider: Access the state + /// - counterProvider.notifier: Access the notifier methods + const CounterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'counterProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$counterHash(); + + @$internal + @override + Counter create() => Counter(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$counterHash() => r'a3963e2a590a1fce6dc8567a60acde4671dd8fb4'; + +/// Notifier for mutable state with methods +/// Use this when you need to expose methods to modify state +/// +/// The @riverpod annotation generates: +/// - counterProvider: Access the state +/// - counterProvider.notifier: Access the notifier methods + +abstract class _$Counter extends $Notifier { + int build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + int, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Notifier with parameters (Family pattern) + +@ProviderFor(CartQuantity) +const cartQuantityProvider = CartQuantityFamily._(); + +/// Notifier with parameters (Family pattern) +final class CartQuantityProvider extends $NotifierProvider { + /// Notifier with parameters (Family pattern) + const CartQuantityProvider._({ + required CartQuantityFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'cartQuantityProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$cartQuantityHash(); + + @override + String toString() { + return r'cartQuantityProvider' + '' + '($argument)'; + } + + @$internal + @override + CartQuantity create() => CartQuantity(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is CartQuantityProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$cartQuantityHash() => r'814d6338fe0146a9ce7c44e1ebe53b0f856983c5'; + +/// Notifier with parameters (Family pattern) + +final class CartQuantityFamily extends $Family + with $ClassFamilyOverride { + const CartQuantityFamily._() + : super( + retry: null, + name: r'cartQuantityProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Notifier with parameters (Family pattern) + + CartQuantityProvider call(String productId) => + CartQuantityProvider._(argument: productId, from: this); + + @override + String toString() => r'cartQuantityProvider'; +} + +/// Notifier with parameters (Family pattern) + +abstract class _$CartQuantity extends $Notifier { + late final _$args = ref.$arg as String; + String get productId => _$args; + + int build(String productId); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + int, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// AsyncNotifier for state that requires async initialization +/// Perfect for fetching data that can then be modified +/// +/// State type: AsyncValue +/// - AsyncValue.data(profile) when loaded +/// - AsyncValue.loading() when loading +/// - AsyncValue.error(error, stack) when error + +@ProviderFor(UserProfileNotifier) +const userProfileProvider = UserProfileNotifierProvider._(); + +/// AsyncNotifier for state that requires async initialization +/// Perfect for fetching data that can then be modified +/// +/// State type: AsyncValue +/// - AsyncValue.data(profile) when loaded +/// - AsyncValue.loading() when loading +/// - AsyncValue.error(error, stack) when error +final class UserProfileNotifierProvider + extends $AsyncNotifierProvider { + /// AsyncNotifier for state that requires async initialization + /// Perfect for fetching data that can then be modified + /// + /// State type: AsyncValue + /// - AsyncValue.data(profile) when loaded + /// - AsyncValue.loading() when loading + /// - AsyncValue.error(error, stack) when error + const UserProfileNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userProfileProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userProfileNotifierHash(); + + @$internal + @override + UserProfileNotifier create() => UserProfileNotifier(); +} + +String _$userProfileNotifierHash() => + r'87c9a9277552095a0ed0b768829e2930fa475c7f'; + +/// AsyncNotifier for state that requires async initialization +/// Perfect for fetching data that can then be modified +/// +/// State type: AsyncValue +/// - AsyncValue.data(profile) when loaded +/// - AsyncValue.loading() when loading +/// - AsyncValue.error(error, stack) when error + +abstract class _$UserProfileNotifier extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, UserProfileData>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, UserProfileData>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// StreamNotifier for state that comes from a stream but can be modified +/// Use for WebSocket connections with additional actions + +@ProviderFor(LiveChatNotifier) +const liveChatProvider = LiveChatNotifierProvider._(); + +/// StreamNotifier for state that comes from a stream but can be modified +/// Use for WebSocket connections with additional actions +final class LiveChatNotifierProvider + extends $StreamNotifierProvider> { + /// StreamNotifier for state that comes from a stream but can be modified + /// Use for WebSocket connections with additional actions + const LiveChatNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'liveChatProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$liveChatNotifierHash(); + + @$internal + @override + LiveChatNotifier create() => LiveChatNotifier(); +} + +String _$liveChatNotifierHash() => r'73a240ad5c606ea907b6bd756790f78534e05a2b'; + +/// StreamNotifier for state that comes from a stream but can be modified +/// Use for WebSocket connections with additional actions + +abstract class _$LiveChatNotifier extends $StreamNotifier> { + Stream> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Provider that depends on other providers + +@ProviderFor(dashboardData) +const dashboardDataProvider = DashboardDataProvider._(); + +/// Provider that depends on other providers + +final class DashboardDataProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + /// Provider that depends on other providers + const DashboardDataProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'dashboardDataProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$dashboardDataHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return dashboardData(ref); + } +} + +String _$dashboardDataHash() => r'978f1547d6f3d9c441e67eb3e8faefea4f2f7967'; + +/// Provider that selectively watches for optimization +/// Note: userProfileProvider is actually a Family provider (takes userId parameter) +/// In real code, you would use the generated AsyncNotifier provider + +@ProviderFor(userDisplayName) +const userDisplayNameProvider = UserDisplayNameProvider._(); + +/// Provider that selectively watches for optimization +/// Note: userProfileProvider is actually a Family provider (takes userId parameter) +/// In real code, you would use the generated AsyncNotifier provider + +final class UserDisplayNameProvider + extends $FunctionalProvider + with $Provider { + /// Provider that selectively watches for optimization + /// Note: userProfileProvider is actually a Family provider (takes userId parameter) + /// In real code, you would use the generated AsyncNotifier provider + const UserDisplayNameProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'userDisplayNameProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$userDisplayNameHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + String create(Ref ref) { + return userDisplayName(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$userDisplayNameHash() => r'758087b5550bc4f35516cd2bd44c930447a5ea06'; + +/// By default, generated providers are autoDispose +/// They clean up when no longer used + +@ProviderFor(autoDisposeExample) +const autoDisposeExampleProvider = AutoDisposeExampleProvider._(); + +/// By default, generated providers are autoDispose +/// They clean up when no longer used + +final class AutoDisposeExampleProvider + extends $FunctionalProvider + with $Provider { + /// By default, generated providers are autoDispose + /// They clean up when no longer used + const AutoDisposeExampleProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'autoDisposeExampleProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$autoDisposeExampleHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + String create(Ref ref) { + return autoDisposeExample(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$autoDisposeExampleHash() => + r'4d31694aa3e5c3be555dd66feef27e069d802039'; + +/// Keep alive provider - never auto-disposes +/// Use for global state, singletons, services + +@ProviderFor(keepAliveExample) +const keepAliveExampleProvider = KeepAliveExampleProvider._(); + +/// Keep alive provider - never auto-disposes +/// Use for global state, singletons, services + +final class KeepAliveExampleProvider + extends $FunctionalProvider + with $Provider { + /// Keep alive provider - never auto-disposes + /// Use for global state, singletons, services + const KeepAliveExampleProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'keepAliveExampleProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$keepAliveExampleHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + String create(Ref ref) { + return keepAliveExample(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$keepAliveExampleHash() => r'097410a95efd4315ce68dcf57615b86065628390'; + +/// Provider with lifecycle hooks + +@ProviderFor(lifecycleExample) +const lifecycleExampleProvider = LifecycleExampleProvider._(); + +/// Provider with lifecycle hooks + +final class LifecycleExampleProvider + extends $FunctionalProvider + with $Provider { + /// Provider with lifecycle hooks + const LifecycleExampleProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'lifecycleExampleProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$lifecycleExampleHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + String create(Ref ref) { + return lifecycleExample(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$lifecycleExampleHash() => r'b75e9f589ff8dafa137be4c2b357786148641425'; + +/// Provider showing how to invalidate and refresh + +@ProviderFor(DataManager) +const dataManagerProvider = DataManagerProvider._(); + +/// Provider showing how to invalidate and refresh +final class DataManagerProvider + extends $AsyncNotifierProvider { + /// Provider showing how to invalidate and refresh + const DataManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'dataManagerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$dataManagerHash(); + + @$internal + @override + DataManager create() => DataManager(); +} + +String _$dataManagerHash() => r'606f4d7fc0ba846c203efae10770c98824bbd295'; + +/// Provider showing how to invalidate and refresh + +abstract class _$DataManager extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, String>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, String>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Provider with comprehensive error handling + +@ProviderFor(ErrorHandlingExample) +const errorHandlingExampleProvider = ErrorHandlingExampleProvider._(); + +/// Provider with comprehensive error handling +final class ErrorHandlingExampleProvider + extends $AsyncNotifierProvider { + /// Provider with comprehensive error handling + const ErrorHandlingExampleProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'errorHandlingExampleProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$errorHandlingExampleHash(); + + @$internal + @override + ErrorHandlingExample create() => ErrorHandlingExample(); +} + +String _$errorHandlingExampleHash() => + r'5e356b031ce80eb37cf8fbb225111e1f184026d5'; + +/// Provider with comprehensive error handling + +abstract class _$ErrorHandlingExample extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, String>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, String>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..f7f3fbe --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,460 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:worker/core/theme/colors.dart'; +import 'package:worker/core/theme/typography.dart'; + +/// App theme configuration for Material 3 design system +/// Provides both light and dark theme variants +class AppTheme { + // Prevent instantiation + AppTheme._(); + + // ==================== Light Theme ==================== + + /// Light theme configuration + static ThemeData lightTheme() { + final ColorScheme colorScheme = ColorScheme.fromSeed( + seedColor: AppColors.primaryBlue, + brightness: Brightness.light, + primary: AppColors.primaryBlue, + secondary: AppColors.lightBlue, + tertiary: AppColors.accentCyan, + error: AppColors.danger, + surface: AppColors.white, + background: AppColors.grey50, + ); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + fontFamily: AppTypography.fontFamily, + + // ==================== App Bar Theme ==================== + appBarTheme: AppBarTheme( + elevation: 0, + centerTitle: true, + backgroundColor: AppColors.primaryBlue, + foregroundColor: AppColors.white, + titleTextStyle: AppTypography.titleLarge.copyWith( + color: AppColors.white, + fontWeight: FontWeight.w600, + ), + iconTheme: const IconThemeData( + color: AppColors.white, + size: 24, + ), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + + // ==================== Card Theme ==================== + cardTheme: const CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + color: AppColors.white, + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + + // ==================== Elevated Button Theme ==================== + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: AppColors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + textStyle: AppTypography.buttonText, + minimumSize: const Size(64, 48), + ), + ), + + // ==================== Text Button Theme ==================== + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppColors.primaryBlue, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + textStyle: AppTypography.buttonText, + ), + ), + + // ==================== Outlined Button Theme ==================== + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + 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), + ), + textStyle: AppTypography.buttonText, + minimumSize: const Size(64, 48), + ), + ), + + // ==================== Input Decoration Theme ==================== + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.white, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100, width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.grey100, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.danger, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + 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, + ), + ), + + // ==================== Bottom Navigation Bar Theme ==================== + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.white, + selectedItemColor: AppColors.primaryBlue, + unselectedItemColor: AppColors.grey500, + selectedIconTheme: IconThemeData( + size: 28, + color: AppColors.primaryBlue, + ), + unselectedIconTheme: IconThemeData( + size: 24, + color: AppColors.grey500, + ), + selectedLabelStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: AppTypography.fontFamily, + ), + unselectedLabelStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + fontFamily: AppTypography.fontFamily, + ), + type: BottomNavigationBarType.fixed, + elevation: 8, + ), + + // ==================== Floating Action Button Theme ==================== + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: AppColors.accentCyan, + foregroundColor: AppColors.white, + elevation: 6, + shape: CircleBorder(), + iconSize: 24, + ), + + // ==================== Chip Theme ==================== + chipTheme: ChipThemeData( + backgroundColor: AppColors.grey50, + selectedColor: AppColors.primaryBlue, + disabledColor: AppColors.grey100, + secondarySelectedColor: AppColors.lightBlue, + labelStyle: AppTypography.labelMedium, + secondaryLabelStyle: AppTypography.labelMedium.copyWith( + color: AppColors.white, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + 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, + ), + ), + + // ==================== Snackbar Theme ==================== + snackBarTheme: SnackBarThemeData( + backgroundColor: AppColors.grey900, + contentTextStyle: AppTypography.bodyMedium.copyWith( + color: AppColors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + behavior: SnackBarBehavior.floating, + elevation: 4, + ), + + // ==================== Divider Theme ==================== + dividerTheme: const DividerThemeData( + color: AppColors.grey100, + thickness: 1, + space: 1, + ), + + // ==================== Icon Theme ==================== + iconTheme: const IconThemeData( + color: AppColors.grey900, + size: 24, + ), + + // ==================== List Tile Theme ==================== + listTileTheme: ListTileThemeData( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + titleTextStyle: AppTypography.titleMedium.copyWith( + color: AppColors.grey900, + ), + subtitleTextStyle: AppTypography.bodyMedium.copyWith( + color: AppColors.grey500, + ), + iconColor: AppColors.grey500, + ), + + // ==================== Switch Theme ==================== + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColors.primaryBlue; + } + return AppColors.grey500; + }), + trackColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColors.lightBlue; + } + return AppColors.grey100; + }), + ), + + // ==================== Checkbox Theme ==================== + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColors.primaryBlue; + } + return AppColors.white; + }), + checkColor: MaterialStateProperty.all(AppColors.white), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + + // ==================== Radio Theme ==================== + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return AppColors.primaryBlue; + } + return AppColors.grey500; + }), + ), + + // ==================== Progress Indicator Theme ==================== + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: AppColors.primaryBlue, + linearTrackColor: AppColors.grey100, + circularTrackColor: AppColors.grey100, + ), + + // ==================== Badge Theme ==================== + badgeTheme: const BadgeThemeData( + backgroundColor: AppColors.danger, + textColor: AppColors.white, + smallSize: 6, + largeSize: 16, + ), + + // ==================== Tab Bar Theme ==================== + tabBarTheme: const TabBarThemeData( + labelColor: AppColors.primaryBlue, + unselectedLabelColor: AppColors.grey500, + indicatorColor: AppColors.primaryBlue, + ).copyWith( + labelStyle: AppTypography.labelLarge, + unselectedLabelStyle: AppTypography.labelLarge, + ), + ); + } + + // ==================== Dark Theme ==================== + + /// Dark theme configuration + static ThemeData darkTheme() { + final ColorScheme colorScheme = ColorScheme.fromSeed( + seedColor: AppColors.primaryBlue, + brightness: Brightness.dark, + primary: AppColors.lightBlue, + secondary: AppColors.accentCyan, + tertiary: AppColors.primaryBlue, + error: AppColors.danger, + surface: const Color(0xFF1E1E1E), + background: const Color(0xFF121212), + ); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + fontFamily: AppTypography.fontFamily, + + // ==================== App Bar Theme ==================== + appBarTheme: AppBarTheme( + elevation: 0, + centerTitle: true, + backgroundColor: const Color(0xFF1E1E1E), + foregroundColor: AppColors.white, + titleTextStyle: AppTypography.titleLarge.copyWith( + color: AppColors.white, + fontWeight: FontWeight.w600, + ), + iconTheme: const IconThemeData( + color: AppColors.white, + size: 24, + ), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + + // ==================== Card Theme ==================== + cardTheme: const CardThemeData( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + color: Color(0xFF1E1E1E), + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + + // ==================== Elevated Button Theme ==================== + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.lightBlue, + foregroundColor: AppColors.white, + elevation: 2, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + textStyle: AppTypography.buttonText, + minimumSize: const Size(64, 48), + ), + ), + + // ==================== Input Decoration Theme ==================== + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF2A2A2A), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.lightBlue, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: AppColors.danger, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + 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, + ), + ), + + // ==================== Bottom Navigation Bar Theme ==================== + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Color(0xFF1E1E1E), + selectedItemColor: AppColors.lightBlue, + unselectedItemColor: AppColors.grey500, + selectedIconTheme: IconThemeData( + size: 28, + color: AppColors.lightBlue, + ), + unselectedIconTheme: IconThemeData( + size: 24, + color: AppColors.grey500, + ), + selectedLabelStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: AppTypography.fontFamily, + ), + unselectedLabelStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + fontFamily: AppTypography.fontFamily, + ), + type: BottomNavigationBarType.fixed, + elevation: 8, + ), + + // ==================== Floating Action Button Theme ==================== + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: AppColors.accentCyan, + foregroundColor: AppColors.white, + elevation: 6, + shape: CircleBorder(), + iconSize: 24, + ), + + // ==================== Snackbar Theme ==================== + snackBarTheme: SnackBarThemeData( + backgroundColor: const Color(0xFF2A2A2A), + contentTextStyle: AppTypography.bodyMedium.copyWith( + color: AppColors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + behavior: SnackBarBehavior.floating, + elevation: 4, + ), + ); + } +} diff --git a/lib/core/theme/colors.dart b/lib/core/theme/colors.dart new file mode 100644 index 0000000..87d0484 --- /dev/null +++ b/lib/core/theme/colors.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +/// App color palette following the Worker app design system. +/// +/// Primary colors are used for main UI elements, tier colors for membership cards, +/// status colors for feedback, and neutral colors for text and backgrounds. +class AppColors { + // Primary Colors + /// Main brand color - Used for primary buttons, app bar, etc. + static const primaryBlue = Color(0xFF005B9A); + + /// Light variant of primary color - Used for highlights and accents + static const lightBlue = Color(0xFF38B6FF); + + /// Accent color for special actions - Used for FAB, links, etc. + static const accentCyan = Color(0xFF35C6F4); + + // Status Colors + /// Success state - Used for completed actions, positive feedback + static const success = Color(0xFF28a745); + + /// Warning state - Used for caution messages, pending states + static const warning = Color(0xFFffc107); + + /// Danger/Error state - Used for errors, destructive actions + static const danger = Color(0xFFdc3545); + + /// Info state - Used for informational messages + static const info = Color(0xFF17a2b8); + + // Neutral Colors + /// Lightest background shade + static const grey50 = Color(0xFFf8f9fa); + + /// Light background/border shade + static const grey100 = Color(0xFFe9ecef); + + /// Medium grey for secondary text + static const grey500 = Color(0xFF6c757d); + + /// Dark grey for primary text + static const grey900 = Color(0xFF343a40); + + /// Pure white + static const white = Color(0xFFFFFFFF); + + // Tier Gradients for Membership Cards + /// Diamond tier gradient (purple-blue) + static const diamondGradient = LinearGradient( + colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + /// Platinum tier gradient (grey-silver) + static const platinumGradient = LinearGradient( + colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + /// Gold tier gradient (yellow-orange) + static const goldGradient = LinearGradient( + colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); +} diff --git a/lib/core/theme/typography.dart b/lib/core/theme/typography.dart new file mode 100644 index 0000000..1df4ccb --- /dev/null +++ b/lib/core/theme/typography.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; + +/// App typography system following Material 3 type scale +/// Uses Roboto as the primary font family +class AppTypography { + // Prevent instantiation + AppTypography._(); + + /// Font family used throughout the app + static const String fontFamily = 'Roboto'; + + // ==================== Display Styles ==================== + + /// Display Large - 32sp, Bold + /// Used for: Large hero text, splash screens + static const TextStyle displayLarge = TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.2, + ); + + /// Display Medium - 28sp, Semi-bold + /// Used for: Page titles, section headers + static const TextStyle displayMedium = TextStyle( + fontSize: 28, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.2, + ); + + /// Display Small - 24sp, Semi-bold + /// Used for: Sub-section headers + static const TextStyle displaySmall = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.3, + ); + + // ==================== Headline Styles ==================== + + /// Headline Large - 24sp, Semi-bold + /// Used for: Main headings, dialog titles + static const TextStyle headlineLarge = TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.3, + ); + + /// Headline Medium - 20sp, Semi-bold + /// Used for: Card titles, list headers + static const TextStyle headlineMedium = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0.15, + height: 1.3, + ); + + /// Headline Small - 18sp, Medium + /// Used for: Small headers, emphasized text + static const TextStyle headlineSmall = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.15, + height: 1.4, + ); + + // ==================== Title Styles ==================== + + /// Title Large - 20sp, Medium + /// Used for: App bar titles, prominent labels + static const TextStyle titleLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.15, + height: 1.4, + ); + + /// Title Medium - 16sp, Medium + /// Used for: List item titles, card headers + static const TextStyle titleMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.15, + height: 1.5, + ); + + /// Title Small - 14sp, Medium + /// Used for: Small titles, tab labels + static const TextStyle titleSmall = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.1, + height: 1.5, + ); + + // ==================== Body Styles ==================== + + /// Body Large - 16sp, Regular + /// Used for: Main body text, descriptions + static const TextStyle bodyLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: fontFamily, + letterSpacing: 0.5, + height: 1.5, + ); + + /// Body Medium - 14sp, Regular + /// Used for: Secondary body text, captions + static const TextStyle bodyMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + fontFamily: fontFamily, + letterSpacing: 0.25, + height: 1.5, + ); + + /// Body Small - 12sp, Regular + /// Used for: Small body text, helper text + static const TextStyle bodySmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + fontFamily: fontFamily, + letterSpacing: 0.4, + height: 1.5, + ); + + // ==================== Label Styles ==================== + + /// Label Large - 14sp, Medium + /// Used for: Button text, input labels + static const TextStyle labelLarge = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.1, + height: 1.4, + ); + + /// Label Medium - 12sp, Medium + /// Used for: Small button text, chips + static const TextStyle labelMedium = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 0.5, + height: 1.4, + ); + + /// Label Small - 12sp, Regular + /// Used for: Overline text, tags, badges + static const TextStyle labelSmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + fontFamily: fontFamily, + letterSpacing: 0.5, + height: 1.4, + ); + + // ==================== Special Purpose Styles ==================== + + /// Points Display - 28sp, Bold + /// Used for: Loyalty points display on member cards + static const TextStyle pointsDisplay = TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.2, + ); + + /// Price Large - 20sp, Bold + /// Used for: Product prices, totals + static const TextStyle priceLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.3, + ); + + /// Price Medium - 16sp, Semi-bold + /// Used for: List item prices + static const TextStyle priceMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.3, + ); + + /// Price Small - 14sp, Semi-bold + /// Used for: Small price displays + static const TextStyle priceSmall = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, + letterSpacing: 0, + height: 1.3, + ); + + /// Button Text - 14sp, Medium + /// Used for: All button labels + static const TextStyle buttonText = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 1.25, + height: 1.0, + ); + + /// Overline - 10sp, Medium, Uppercase + /// Used for: Section labels, categories + static const TextStyle overline = TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + fontFamily: fontFamily, + letterSpacing: 1.5, + height: 1.6, + ); + + /// Caption - 12sp, Regular + /// Used for: Image captions, timestamps + static const TextStyle caption = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + fontFamily: fontFamily, + letterSpacing: 0.4, + height: 1.33, + ); +} diff --git a/lib/core/utils/README_L10N.md b/lib/core/utils/README_L10N.md new file mode 100644 index 0000000..d34dadb --- /dev/null +++ b/lib/core/utils/README_L10N.md @@ -0,0 +1,278 @@ +# Localization Extensions - Quick Start Guide + +## Using Localization in the Worker App + +This file demonstrates how to use the localization utilities in the Worker Flutter app. + +## Basic Usage + +### 1. Import the Extension + +```dart +import 'package:worker/core/utils/l10n_extensions.dart'; +``` + +### 2. Access Translations + +```dart +// In any widget with BuildContext +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(context.l10n.home), // "Trang chủ" or "Home" + Text(context.l10n.products), // "Sản phẩm" or "Products" + Text(context.l10n.loyalty), // "Hội viên" or "Loyalty" + ], + ); + } +} +``` + +## Helper Functions + +### Date and Time + +```dart +// Format date +final dateStr = L10nHelper.formatDate(context, DateTime.now()); +// Vietnamese: "17/10/2025" +// English: "10/17/2025" + +// Format date-time +final dateTimeStr = L10nHelper.formatDateTime(context, DateTime.now()); +// Vietnamese: "17/10/2025 lúc 14:30" +// English: "10/17/2025 at 14:30" + +// Relative time +final relativeTime = L10nHelper.formatRelativeTime( + context, + DateTime.now().subtract(Duration(minutes: 5)), +); +// Vietnamese: "5 phút trước" +// English: "5 minutes ago" +``` + +### Currency + +```dart +// Format Vietnamese Dong +final price = L10nHelper.formatCurrency(context, 1500000); +// Vietnamese: "1.500.000 ₫" +// English: "1,500,000 ₫" +``` + +### Status Helpers + +```dart +// Get localized order status +final status = L10nHelper.getOrderStatus(context, 'pending'); +// Vietnamese: "Chờ xử lý" +// English: "Pending" + +// Get localized project status +final projectStatus = L10nHelper.getProjectStatus(context, 'in_progress'); +// Vietnamese: "Đang thực hiện" +// English: "In Progress" + +// Get localized member tier +final tier = L10nHelper.getMemberTier(context, 'diamond'); +// Vietnamese: "Kim cương" +// English: "Diamond" + +// Get localized user type +final userType = L10nHelper.getUserType(context, 'contractor'); +// Vietnamese: "Thầu thợ" +// English: "Contractor" +``` + +### Counts with Pluralization + +```dart +// Format points with sign +final points = L10nHelper.formatPoints(context, 100); +// Vietnamese: "+100 điểm" +// English: "+100 points" + +// Format item count +final items = L10nHelper.formatItemCount(context, 5); +// Vietnamese: "5 sản phẩm" +// English: "5 items" + +// Format order count +final orders = L10nHelper.formatOrderCount(context, 3); +// Vietnamese: "3 đơn hàng" +// English: "3 orders" + +// Format project count +final projects = L10nHelper.formatProjectCount(context, 2); +// Vietnamese: "2 công trình" +// English: "2 projects" + +// Format days remaining +final days = L10nHelper.formatDaysRemaining(context, 7); +// Vietnamese: "Còn 7 ngày" +// English: "7 days left" +``` + +## Context Extensions + +### Language Checks + +```dart +// Get current language code +final languageCode = context.languageCode; // "vi" or "en" + +// Check if Vietnamese +if (context.isVietnamese) { + // Do something specific for Vietnamese +} + +// Check if English +if (context.isEnglish) { + // Do something specific for English +} +``` + +## Parameterized Translations + +```dart +// Simple parameter +final welcome = context.l10n.welcomeTo('Worker App'); +// Vietnamese: "Chào mừng đến với Worker App" +// English: "Welcome to Worker App" + +// Multiple parameters +final message = context.l10n.pointsToNextTier(500, 'Platinum'); +// Vietnamese: "Còn 500 điểm để lên hạng Platinum" +// English: "500 points to reach Platinum" + +// Order number +final orderNum = context.l10n.orderNumberIs('ORD-2024-001'); +// Vietnamese: "Số đơn hàng: ORD-2024-001" +// English: "Order Number: ORD-2024-001" + +// Redeem confirmation +final confirm = context.l10n.redeemConfirmMessage(500, 'Gift Voucher'); +// Vietnamese: "Bạn có chắc chắn muốn đổi 500 điểm để nhận Gift Voucher?" +// English: "Are you sure you want to redeem 500 points for Gift Voucher?" +``` + +## Complete Example + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:worker/core/utils/l10n_extensions.dart'; + +class OrderDetailPage extends ConsumerWidget { + final Order order; + + const OrderDetailPage({required this.order}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.orderDetails), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Order number + Text( + context.l10n.orderNumberIs(order.orderNumber), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + + // Order date + Text( + '${context.l10n.orderDate}: ${L10nHelper.formatDate(context, order.createdAt)}', + ), + const SizedBox(height: 8), + + // Order status + Row( + children: [ + Text(context.l10n.orderStatus + ': '), + Chip( + label: Text( + L10nHelper.getOrderStatus(context, order.status), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Items count + Text( + L10nHelper.formatItemCount(context, order.items.length), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + + // Total amount + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.total, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + L10nHelper.formatCurrency(context, order.total), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Relative time + Text( + '${context.l10n.orderPlacedAt} ${L10nHelper.formatRelativeTime(context, order.createdAt)}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ); + } +} +``` + +## Best Practices + +1. **Always use `context.l10n` instead of `AppLocalizations.of(context)!`** + - Shorter and cleaner + - Consistent throughout the codebase + +2. **Use helper functions for formatting** + - `L10nHelper.formatCurrency()` instead of manual formatting + - `L10nHelper.formatDate()` for locale-aware dates + - `L10nHelper.getOrderStatus()` for localized status strings + +3. **Check language when needed** + - Use `context.isVietnamese` and `context.isEnglish` + - Useful for conditional rendering or logic + +4. **Never hard-code strings** + - Always use translation keys + - Supports both Vietnamese and English automatically + +5. **Test both languages** + - Switch device language to test + - Verify text fits in UI for both languages + +## See Also + +- Full documentation: `/Users/ssg/project/worker/LOCALIZATION.md` +- Vietnamese translations: `/Users/ssg/project/worker/lib/l10n/app_vi.arb` +- English translations: `/Users/ssg/project/worker/lib/l10n/app_en.arb` +- Helper source code: `/Users/ssg/project/worker/lib/core/utils/l10n_extensions.dart` diff --git a/lib/core/utils/extensions.dart b/lib/core/utils/extensions.dart new file mode 100644 index 0000000..02f70b4 --- /dev/null +++ b/lib/core/utils/extensions.dart @@ -0,0 +1,471 @@ +/// Dart Extension Methods +/// +/// Provides useful extension methods for common data types +/// used throughout the app. +library; + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +// ============================================================================ +// String Extensions +// ============================================================================ + +extension StringExtensions on String { + /// Check if string is null or empty + bool get isNullOrEmpty => trim().isEmpty; + + /// Check if string is not null and not empty + bool get isNotNullOrEmpty => trim().isNotEmpty; + + /// Capitalize first letter + String get capitalize { + if (isEmpty) return this; + return '${this[0].toUpperCase()}${substring(1)}'; + } + + /// Capitalize each word + String get capitalizeWords { + if (isEmpty) return this; + return split(' ').map((word) => word.capitalize).join(' '); + } + + /// Convert to title case + String get titleCase => capitalizeWords; + + /// Remove all whitespace + String get removeWhitespace => replaceAll(RegExp(r'\s+'), ''); + + /// Check if string is a valid email + bool get isEmail { + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + return emailRegex.hasMatch(this); + } + + /// Check if string is a valid Vietnamese phone number + bool get isPhoneNumber { + final phoneRegex = RegExp(r'^(0|\+?84)(3|5|7|8|9)[0-9]{8}$'); + return phoneRegex.hasMatch(replaceAll(RegExp(r'[^\d+]'), '')); + } + + /// Check if string is numeric + bool get isNumeric { + return double.tryParse(this) != null; + } + + /// Convert string to int (returns null if invalid) + int? get toIntOrNull => int.tryParse(this); + + /// Convert string to double (returns null if invalid) + double? get toDoubleOrNull => double.tryParse(this); + + /// Truncate string with ellipsis + String truncate(int maxLength, {String ellipsis = '...'}) { + if (length <= maxLength) return this; + return '${substring(0, maxLength - ellipsis.length)}$ellipsis'; + } + + /// Remove Vietnamese diacritics + String get removeDiacritics { + const withDiacritics = + 'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ'; + const withoutDiacritics = + 'aaaaaaaaaaaaaaaaaeeeeeeeeeeeiiiiioooooooooooooooooouuuuuuuuuuuyyyyyd'; + + var result = toLowerCase(); + for (var i = 0; i < withDiacritics.length; i++) { + result = result.replaceAll(withDiacritics[i], withoutDiacritics[i]); + } + return result; + } + + /// Convert to URL-friendly slug + String get slugify { + var slug = removeDiacritics; + slug = slug.toLowerCase(); + slug = slug.replaceAll(RegExp(r'[^\w\s-]'), ''); + slug = slug.replaceAll(RegExp(r'[-\s]+'), '-'); + return slug; + } + + /// Mask email (e.g., "j***@example.com") + String get maskEmail { + if (!isEmail) return this; + final parts = split('@'); + final name = parts[0]; + final maskedName = name.length > 2 + ? '${name[0]}${'*' * (name.length - 1)}' + : name; + return '$maskedName@${parts[1]}'; + } + + /// Mask phone number (e.g., "0xxx xxx ***") + String get maskPhone { + final cleaned = replaceAll(RegExp(r'\D'), ''); + if (cleaned.length < 10) return this; + return '${cleaned.substring(0, 4)} ${cleaned.substring(4, 7)} ***'; + } +} + +// ============================================================================ +// DateTime Extensions +// ============================================================================ + +extension DateTimeExtensions on DateTime { + /// Check if date is today + bool get isToday { + final now = DateTime.now(); + return year == now.year && month == now.month && day == now.day; + } + + /// Check if date is yesterday + bool get isYesterday { + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + return year == yesterday.year && + month == yesterday.month && + day == yesterday.day; + } + + /// Check if date is tomorrow + bool get isTomorrow { + final tomorrow = DateTime.now().add(const Duration(days: 1)); + return year == tomorrow.year && + month == tomorrow.month && + day == tomorrow.day; + } + + /// Check if date is in the past + bool get isPast => isBefore(DateTime.now()); + + /// Check if date is in the future + bool get isFuture => isAfter(DateTime.now()); + + /// Get start of day (00:00:00) + DateTime get startOfDay => DateTime(year, month, day); + + /// Get end of day (23:59:59) + DateTime get endOfDay => DateTime(year, month, day, 23, 59, 59, 999); + + /// Get start of month + DateTime get startOfMonth => DateTime(year, month, 1); + + /// Get end of month + DateTime get endOfMonth => DateTime(year, month + 1, 0, 23, 59, 59, 999); + + /// Get start of year + DateTime get startOfYear => DateTime(year, 1, 1); + + /// Get end of year + DateTime get endOfYear => DateTime(year, 12, 31, 23, 59, 59, 999); + + /// Add days + DateTime addDays(int days) => add(Duration(days: days)); + + /// Subtract days + DateTime subtractDays(int days) => subtract(Duration(days: days)); + + /// Add months + DateTime addMonths(int months) => DateTime(year, month + months, day); + + /// Subtract months + DateTime subtractMonths(int months) => DateTime(year, month - months, day); + + /// Add years + DateTime addYears(int years) => DateTime(year + years, month, day); + + /// Subtract years + DateTime subtractYears(int years) => DateTime(year - years, month, day); + + /// Get age in years from this date + int get ageInYears { + final today = DateTime.now(); + var age = today.year - year; + if (today.month < month || (today.month == month && today.day < day)) { + age--; + } + return age; + } + + /// Get difference in days from now + int get daysFromNow => DateTime.now().difference(this).inDays; + + /// Get difference in hours from now + int get hoursFromNow => DateTime.now().difference(this).inHours; + + /// Get difference in minutes from now + int get minutesFromNow => DateTime.now().difference(this).inMinutes; + + /// Copy with new values + DateTime copyWith({ + int? year, + int? month, + int? day, + int? hour, + int? minute, + int? second, + int? millisecond, + int? microsecond, + }) { + return DateTime( + year ?? this.year, + month ?? this.month, + day ?? this.day, + hour ?? this.hour, + minute ?? this.minute, + second ?? this.second, + millisecond ?? this.millisecond, + microsecond ?? this.microsecond, + ); + } +} + +// ============================================================================ +// Duration Extensions +// ============================================================================ + +extension DurationExtensions on Duration { + /// Format duration as readable string (e.g., "2 giờ 30 phút") + String get formatted { + final hours = inHours; + final minutes = inMinutes.remainder(60); + final seconds = inSeconds.remainder(60); + + if (hours > 0) { + if (minutes > 0) { + return '$hours giờ $minutes phút'; + } + return '$hours giờ'; + } else if (minutes > 0) { + if (seconds > 0) { + return '$minutes phút $seconds giây'; + } + return '$minutes phút'; + } else { + return '$seconds giây'; + } + } + + /// Format as HH:MM:SS + String get hhmmss { + final hours = inHours; + final minutes = inMinutes.remainder(60); + final seconds = inSeconds.remainder(60); + + return '${hours.toString().padLeft(2, '0')}:' + '${minutes.toString().padLeft(2, '0')}:' + '${seconds.toString().padLeft(2, '0')}'; + } + + /// Format as MM:SS + String get mmss { + final minutes = inMinutes.remainder(60); + final seconds = inSeconds.remainder(60); + + return '${minutes.toString().padLeft(2, '0')}:' + '${seconds.toString().padLeft(2, '0')}'; + } +} + +// ============================================================================ +// List Extensions +// ============================================================================ + +extension ListExtensions on List { + /// Get first element or null if list is empty + T? get firstOrNull => isEmpty ? null : first; + + /// Get last element or null if list is empty + T? get lastOrNull => isEmpty ? null : last; + + /// Get element at index or null if out of bounds + T? elementAtOrNull(int index) { + if (index < 0 || index >= length) return null; + return this[index]; + } + + /// Group list by key + Map> groupBy(K Function(T) keySelector) { + final map = >{}; + for (final element in this) { + final key = keySelector(element); + if (!map.containsKey(key)) { + map[key] = []; + } + map[key]!.add(element); + } + return map; + } + + /// Get distinct elements + List get distinct => toSet().toList(); + + /// Get distinct elements by key + List distinctBy(K Function(T) keySelector) { + final seen = {}; + return where((element) => seen.add(keySelector(element))).toList(); + } + + /// Chunk list into smaller lists of specified size + List> chunk(int size) { + final chunks = >[]; + for (var i = 0; i < length; i += size) { + chunks.add(sublist(i, (i + size) > length ? length : (i + size))); + } + return chunks; + } +} + +// ============================================================================ +// Map Extensions +// ============================================================================ + +extension MapExtensions on Map { + /// Get value or default if key doesn't exist + V getOrDefault(K key, V defaultValue) { + return containsKey(key) ? this[key] as V : defaultValue; + } + + /// Get value or null if key doesn't exist + V? getOrNull(K key) { + return containsKey(key) ? this[key] : null; + } +} + +// ============================================================================ +// BuildContext Extensions +// ============================================================================ + +extension BuildContextExtensions on BuildContext { + /// Get screen size + Size get screenSize => MediaQuery.of(this).size; + + /// Get screen width + double get screenWidth => MediaQuery.of(this).size.width; + + /// Get screen height + double get screenHeight => MediaQuery.of(this).size.height; + + /// Check if screen is small (<600dp) + bool get isSmallScreen => MediaQuery.of(this).size.width < 600; + + /// Check if screen is medium (600-960dp) + bool get isMediumScreen { + final width = MediaQuery.of(this).size.width; + return width >= 600 && width < 960; + } + + /// Check if screen is large (>=960dp) + bool get isLargeScreen => MediaQuery.of(this).size.width >= 960; + + /// Get theme + ThemeData get theme => Theme.of(this); + + /// Get text theme + TextTheme get textTheme => Theme.of(this).textTheme; + + /// Get color scheme + ColorScheme get colorScheme => Theme.of(this).colorScheme; + + /// Get primary color + Color get primaryColor => Theme.of(this).primaryColor; + + /// Check if dark mode is enabled + bool get isDarkMode => Theme.of(this).brightness == Brightness.dark; + + /// Get safe area padding + EdgeInsets get safeAreaPadding => MediaQuery.of(this).padding; + + /// Get bottom safe area padding (for devices with notch) + double get bottomSafeArea => MediaQuery.of(this).padding.bottom; + + /// Get top safe area padding (for status bar) + double get topSafeArea => MediaQuery.of(this).padding.top; + + /// Show snackbar + void showSnackBar(String message, {Duration? duration}) { + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + content: Text(message), + duration: duration ?? const Duration(seconds: 3), + ), + ); + } + + /// Show error snackbar + void showErrorSnackBar(String message, {Duration? duration}) { + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: colorScheme.error, + duration: duration ?? const Duration(seconds: 4), + ), + ); + } + + /// Show success snackbar + void showSuccessSnackBar(String message, {Duration? duration}) { + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.green, + duration: duration ?? const Duration(seconds: 3), + ), + ); + } + + /// Hide keyboard + void hideKeyboard() { + FocusScope.of(this).unfocus(); + } + + /// Navigate to route + Future push(Widget page) { + return Navigator.of(this).push( + MaterialPageRoute(builder: (_) => page), + ); + } + + /// Navigate and replace current route + Future pushReplacement(Widget page) { + return Navigator.of(this).pushReplacement( + MaterialPageRoute(builder: (_) => page), + ); + } + + /// Pop current route + void pop([T? result]) { + Navigator.of(this).pop(result); + } + + /// Pop until first route + void popUntilFirst() { + Navigator.of(this).popUntil((route) => route.isFirst); + } +} + +// ============================================================================ +// Num Extensions +// ============================================================================ + +extension NumExtensions on num { + /// Check if number is positive + bool get isPositive => this > 0; + + /// Check if number is negative + bool get isNegative => this < 0; + + /// Check if number is zero + bool get isZero => this == 0; + + /// Clamp number between min and max + num clampTo(num min, num max) => clamp(min, max); + + /// Round to specified decimal places + double roundToDecimal(int places) { + final mod = math.pow(10.0, places); + return ((this * mod).round().toDouble() / mod); + } +} diff --git a/lib/core/utils/formatters.dart b/lib/core/utils/formatters.dart new file mode 100644 index 0000000..3281a63 --- /dev/null +++ b/lib/core/utils/formatters.dart @@ -0,0 +1,371 @@ +/// Data Formatters for Vietnamese Locale +/// +/// Provides formatting utilities for currency, dates, phone numbers, +/// and other data types commonly used in the app. +library; + +import 'package:intl/intl.dart'; + +/// Currency formatter for Vietnamese Dong (VND) +class CurrencyFormatter { + CurrencyFormatter._(); + + /// Format amount as Vietnamese currency (e.g., "100,000 ₫") + static String format(double amount, {bool showSymbol = true}) { + final formatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: showSymbol ? '₫' : '', + decimalDigits: 0, + ); + return formatter.format(amount); + } + + /// Format amount with custom precision + static String formatWithDecimals( + double amount, { + int decimalDigits = 2, + bool showSymbol = true, + }) { + final formatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: showSymbol ? '₫' : '', + decimalDigits: decimalDigits, + ); + return formatter.format(amount); + } + + /// Format as compact currency (e.g., "1.5M ₫") + static String formatCompact(double amount, {bool showSymbol = true}) { + final formatter = NumberFormat.compactCurrency( + locale: 'vi_VN', + symbol: showSymbol ? '₫' : '', + decimalDigits: 1, + ); + return formatter.format(amount); + } + + /// Parse currency string to double + static double? parse(String value) { + try { + // Remove currency symbol and spaces + final cleaned = value.replaceAll(RegExp(r'[₫\s,]'), ''); + return double.tryParse(cleaned); + } catch (e) { + return null; + } + } +} + +/// Date and time formatter +class DateFormatter { + DateFormatter._(); + + /// Format date as "dd/MM/yyyy" (Vietnamese format) + static String formatDate(DateTime date) { + final formatter = DateFormat('dd/MM/yyyy', 'vi_VN'); + return formatter.format(date); + } + + /// Format date as "dd-MM-yyyy" + static String formatDateDash(DateTime date) { + final formatter = DateFormat('dd-MM-yyyy', 'vi_VN'); + return formatter.format(date); + } + + /// Format time as "HH:mm" + static String formatTime(DateTime date) { + final formatter = DateFormat('HH:mm', 'vi_VN'); + return formatter.format(date); + } + + /// Format date and time as "dd/MM/yyyy HH:mm" + static String formatDateTime(DateTime date) { + final formatter = DateFormat('dd/MM/yyyy HH:mm', 'vi_VN'); + return formatter.format(date); + } + + /// Format date as "dd/MM/yyyy lúc HH:mm" + static String formatDateTimeVN(DateTime date) { + final formatter = DateFormat('dd/MM/yyyy', 'vi_VN'); + final timeFormatter = DateFormat('HH:mm', 'vi_VN'); + return '${formatter.format(date)} lúc ${timeFormatter.format(date)}'; + } + + /// Format as relative time (e.g., "2 giờ trước") + static String formatRelative(DateTime date) { + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inSeconds < 60) { + return 'Vừa xong'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes} phút trước'; + } else if (difference.inHours < 24) { + return '${difference.inHours} giờ trước'; + } else if (difference.inDays < 7) { + return '${difference.inDays} ngày trước'; + } else if (difference.inDays < 30) { + final weeks = (difference.inDays / 7).floor(); + return '$weeks tuần trước'; + } else if (difference.inDays < 365) { + final months = (difference.inDays / 30).floor(); + return '$months tháng trước'; + } else { + final years = (difference.inDays / 365).floor(); + return '$years năm trước'; + } + } + + /// Format as day of week (e.g., "Thứ Hai") + static String formatDayOfWeek(DateTime date) { + final formatter = DateFormat('EEEE', 'vi_VN'); + return formatter.format(date); + } + + /// Format as month and year (e.g., "Tháng 10 năm 2024") + static String formatMonthYear(DateTime date) { + final formatter = DateFormat('MMMM yyyy', 'vi_VN'); + return formatter.format(date); + } + + /// Format as full date with day of week (e.g., "Thứ Hai, 17/10/2024") + static String formatFullDate(DateTime date) { + final dayOfWeek = formatDayOfWeek(date); + final dateStr = formatDate(date); + return '$dayOfWeek, $dateStr'; + } + + /// Parse date string in format "dd/MM/yyyy" + static DateTime? parseDate(String dateStr) { + try { + final formatter = DateFormat('dd/MM/yyyy'); + return formatter.parse(dateStr); + } catch (e) { + return null; + } + } + + /// Parse datetime string in format "dd/MM/yyyy HH:mm" + static DateTime? parseDateTime(String dateTimeStr) { + try { + final formatter = DateFormat('dd/MM/yyyy HH:mm'); + return formatter.parse(dateTimeStr); + } catch (e) { + return null; + } + } +} + +/// Phone number formatter for Vietnamese phone numbers +class PhoneFormatter { + PhoneFormatter._(); + + /// Format phone number as "(0xxx) xxx xxx" + static String format(String phone) { + // Remove all non-digit characters + final cleaned = phone.replaceAll(RegExp(r'\D'), ''); + + if (cleaned.isEmpty) return ''; + + // Handle Vietnamese phone number formats + if (cleaned.startsWith('84')) { + // +84 format + final local = cleaned.substring(2); + if (local.length >= 9) { + return '(+84${local.substring(0, 2)}) ${local.substring(2, 5)} ${local.substring(5)}'; + } + } else if (cleaned.startsWith('0')) { + // 0xxx format + if (cleaned.length >= 10) { + return '(${cleaned.substring(0, 4)}) ${cleaned.substring(4, 7)} ${cleaned.substring(7)}'; + } + } + + return phone; // Return original if format doesn't match + } + + /// Format as international number (+84xxx xxx xxx) + static String formatInternational(String phone) { + final cleaned = phone.replaceAll(RegExp(r'\D'), ''); + + if (cleaned.isEmpty) return ''; + + if (cleaned.startsWith('0')) { + // Convert 0xxx to +84xxx + final local = cleaned.substring(1); + if (local.length >= 9) { + return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}'; + } + } else if (cleaned.startsWith('84')) { + final local = cleaned.substring(2); + if (local.length >= 9) { + return '+84${local.substring(0, 2)} ${local.substring(2, 5)} ${local.substring(5)}'; + } + } + + return phone; + } + + /// Remove formatting to get clean phone number + static String clean(String phone) { + return phone.replaceAll(RegExp(r'\D'), ''); + } + + /// Convert to E.164 format (+84xxxxxxxxx) + static String toE164(String phone) { + final cleaned = clean(phone); + + if (cleaned.startsWith('0')) { + return '+84${cleaned.substring(1)}'; + } else if (cleaned.startsWith('84')) { + return '+$cleaned'; + } else if (cleaned.startsWith('+84')) { + return cleaned; + } + + return '+84$cleaned'; + } + + /// Mask phone number (e.g., "0xxx xxx ***") + static String mask(String phone) { + final cleaned = clean(phone); + + if (cleaned.length >= 10) { + return '${cleaned.substring(0, 4)} ${cleaned.substring(4, 7)} ***'; + } + + return phone; + } +} + +/// Number formatter +class NumberFormatter { + NumberFormatter._(); + + /// Format number with thousand separators + static String format(num number, {int decimalDigits = 0}) { + final formatter = NumberFormat('#,###', 'vi_VN'); + if (decimalDigits > 0) { + return formatter.format(number); + } + return formatter.format(number.round()); + } + + /// Format as percentage + static String formatPercentage( + double value, { + int decimalDigits = 0, + bool showSymbol = true, + }) { + final formatter = NumberFormat.percentPattern('vi_VN'); + formatter.maximumFractionDigits = decimalDigits; + formatter.minimumFractionDigits = decimalDigits; + + final result = formatter.format(value / 100); + return showSymbol ? result : result.replaceAll('%', ''); + } + + /// Format as compact number (e.g., "1.5K") + static String formatCompact(num number) { + final formatter = NumberFormat.compact(locale: 'vi_VN'); + return formatter.format(number); + } + + /// Format file size + static String formatBytes(int bytes, {int decimals = 2}) { + if (bytes <= 0) return '0 B'; + + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB']; + final i = (bytes.bitLength - 1) ~/ 10; + final value = bytes / (1 << (i * 10)); + + return '${value.toStringAsFixed(decimals)} ${suffixes[i]}'; + } + + /// Format duration (e.g., "1:30:45") + static String formatDuration(Duration duration) { + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + + if (hours > 0) { + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + } +} + +/// Text formatter utilities +class TextFormatter { + TextFormatter._(); + + /// Capitalize first letter + static String capitalize(String text) { + if (text.isEmpty) return text; + return text[0].toUpperCase() + text.substring(1); + } + + /// Capitalize each word + static String capitalizeWords(String text) { + if (text.isEmpty) return text; + return text.split(' ').map((word) => capitalize(word)).join(' '); + } + + /// Truncate text with 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'; + + var result = text.toLowerCase(); + for (var i = 0; i < withDiacritics.length; i++) { + result = result.replaceAll(withDiacritics[i], withoutDiacritics[i]); + } + + return result; + } + + /// Create URL-friendly slug + static String slugify(String text) { + var slug = removeDiacritics(text); + slug = slug.toLowerCase(); + slug = slug.replaceAll(RegExp(r'[^\w\s-]'), ''); + slug = slug.replaceAll(RegExp(r'[-\s]+'), '-'); + return slug; + } +} + +/// Loyalty tier formatter +class LoyaltyFormatter { + LoyaltyFormatter._(); + + /// Format tier name in Vietnamese + static String formatTier(String tier) { + switch (tier.toLowerCase()) { + case 'diamond': + return 'Kim Cương'; + case 'platinum': + return 'Bạch Kim'; + case 'gold': + return 'Vàng'; + default: + return tier; + } + } + + /// Format points with label + static String formatPoints(int points) { + return '${NumberFormatter.format(points)} điểm'; + } + + /// Format points progress (e.g., "1,200 / 5,000 điểm") + static String formatPointsProgress(int current, int target) { + return '${NumberFormatter.format(current)} / ${NumberFormatter.format(target)} điểm'; + } +} diff --git a/lib/core/utils/l10n_extensions.dart b/lib/core/utils/l10n_extensions.dart new file mode 100644 index 0000000..866870b --- /dev/null +++ b/lib/core/utils/l10n_extensions.dart @@ -0,0 +1,274 @@ +import 'package:flutter/widgets.dart'; +import 'package:worker/generated/l10n/app_localizations.dart'; + +/// Extension for easy access to AppLocalizations +/// +/// This extension provides convenient access to localization strings +/// throughout the app without having to write `AppLocalizations.of(context)!` +/// every time. +/// +/// Usage: +/// ```dart +/// // Instead of: +/// Text(AppLocalizations.of(context)!.login) +/// +/// // You can use: +/// Text(context.l10n.login) +/// ``` +extension L10nExtension on BuildContext { + /// Get the current AppLocalizations instance + /// + /// This getter provides quick access to all localized strings. + /// It will throw an error if called before the app is initialized, + /// which helps catch localization issues during development. + AppLocalizations get l10n => AppLocalizations.of(this)!; + + /// Get the current locale language code (e.g., 'vi', 'en') + String get languageCode => Localizations.localeOf(this).languageCode; + + /// Check if the current locale is Vietnamese + bool get isVietnamese => languageCode == 'vi'; + + /// Check if the current locale is English + bool get isEnglish => languageCode == 'en'; +} + +/// Helper class for common localization patterns +/// +/// This class provides utility methods for formatting dates, times, +/// currencies, and other locale-specific data. +class L10nHelper { + const L10nHelper._(); + + /// Format a DateTime to localized date string (DD/MM/YYYY for Vietnamese, MM/DD/YYYY for English) + /// + /// Example: + /// ```dart + /// final dateStr = L10nHelper.formatDate(context, DateTime.now()); + /// // Vietnamese: "17/10/2025" + /// // English: "10/17/2025" + /// ``` + static String formatDate(BuildContext context, DateTime date) { + final day = date.day.toString().padLeft(2, '0'); + final month = date.month.toString().padLeft(2, '0'); + final year = date.year.toString(); + + return context.l10n.formatDate(day, month, year); + } + + /// Format a DateTime to localized date-time string + /// + /// Example: + /// ```dart + /// final dateTimeStr = L10nHelper.formatDateTime(context, DateTime.now()); + /// // Vietnamese: "17/10/2025 lúc 14:30" + /// // English: "10/17/2025 at 14:30" + /// ``` + static String formatDateTime(BuildContext context, DateTime dateTime) { + final day = dateTime.day.toString().padLeft(2, '0'); + final month = dateTime.month.toString().padLeft(2, '0'); + final year = dateTime.year.toString(); + final hour = dateTime.hour.toString().padLeft(2, '0'); + final minute = dateTime.minute.toString().padLeft(2, '0'); + + return context.l10n.formatDateTime(day, month, year, hour, minute); + } + + /// Format a number as Vietnamese Dong currency + /// + /// Example: + /// ```dart + /// final price = L10nHelper.formatCurrency(context, 1500000); + /// // Returns: "1.500.000 ₫" + /// ``` + static String formatCurrency(BuildContext context, double amount) { + final formatted = context.isVietnamese + ? _formatNumberVietnamese(amount) + : _formatNumberEnglish(amount); + + return context.l10n.formatCurrency(formatted); + } + + /// Format number with Vietnamese grouping (dots) + static String _formatNumberVietnamese(double number) { + final parts = number.toStringAsFixed(0).split('.'); + final intPart = parts[0]; + + // Add dots every 3 digits from right + final buffer = StringBuffer(); + for (var i = 0; i < intPart.length; i++) { + if (i > 0 && (intPart.length - i) % 3 == 0) { + buffer.write('.'); + } + buffer.write(intPart[i]); + } + + return buffer.toString(); + } + + /// Format number with English grouping (commas) + static String _formatNumberEnglish(double number) { + final parts = number.toStringAsFixed(0).split('.'); + final intPart = parts[0]; + + // Add commas every 3 digits from right + final buffer = StringBuffer(); + for (var i = 0; i < intPart.length; i++) { + if (i > 0 && (intPart.length - i) % 3 == 0) { + buffer.write(','); + } + buffer.write(intPart[i]); + } + + return buffer.toString(); + } + + /// Format a relative time (e.g., "5 minutes ago", "2 days ago") + /// + /// Example: + /// ```dart + /// final relativeTime = L10nHelper.formatRelativeTime( + /// context, + /// DateTime.now().subtract(Duration(minutes: 5)), + /// ); + /// // Returns: "5 phút trước" (Vietnamese) or "5 minutes ago" (English) + /// ``` + static String formatRelativeTime(BuildContext context, DateTime dateTime) { + final now = DateTime.now(); + final difference = now.difference(dateTime); + + if (difference.inSeconds < 60) { + return context.l10n.justNow; + } else if (difference.inMinutes < 60) { + return context.l10n.minutesAgo(difference.inMinutes); + } else if (difference.inHours < 24) { + return context.l10n.hoursAgo(difference.inHours); + } else if (difference.inDays < 7) { + return context.l10n.daysAgo(difference.inDays); + } else if (difference.inDays < 30) { + return context.l10n.weeksAgo((difference.inDays / 7).floor()); + } else if (difference.inDays < 365) { + return context.l10n.monthsAgo((difference.inDays / 30).floor()); + } else { + return context.l10n.yearsAgo((difference.inDays / 365).floor()); + } + } + + /// Get localized order status string + static String getOrderStatus(BuildContext context, String status) { + switch (status.toLowerCase()) { + case 'pending': + return context.l10n.pending; + case 'processing': + return context.l10n.processing; + case 'shipping': + return context.l10n.shipping; + case 'completed': + return context.l10n.completed; + case 'cancelled': + return context.l10n.cancelled; + default: + return status; + } + } + + /// Get localized project status string + static String getProjectStatus(BuildContext context, String status) { + switch (status.toLowerCase()) { + case 'planning': + return context.l10n.planningProjects; + case 'in_progress': + case 'inprogress': + return context.l10n.inProgressProjects; + case 'completed': + return context.l10n.completedProjects; + default: + return status; + } + } + + /// Get localized member tier string + static String getMemberTier(BuildContext context, String tier) { + switch (tier.toLowerCase()) { + case 'diamond': + return context.l10n.diamond; + case 'platinum': + return context.l10n.platinum; + case 'gold': + return context.l10n.gold; + default: + return tier; + } + } + + /// Get localized user type string + static String getUserType(BuildContext context, String userType) { + switch (userType.toLowerCase()) { + case 'contractor': + return context.l10n.contractor; + case 'architect': + return context.l10n.architect; + case 'distributor': + return context.l10n.distributor; + case 'broker': + return context.l10n.broker; + default: + return userType; + } + } + + /// Get localized password strength string + static String getPasswordStrength(BuildContext context, String strength) { + switch (strength.toLowerCase()) { + case 'weak': + return context.l10n.weak; + case 'medium': + return context.l10n.medium; + case 'strong': + return context.l10n.strong; + case 'very_strong': + case 'verystrong': + return context.l10n.veryStrong; + default: + return strength; + } + } + + /// Format points with proper pluralization + /// + /// Example: + /// ```dart + /// 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}) { + if (showSign && points > 0) { + return context.l10n.earnedPoints(points); + } else if (showSign && points < 0) { + return context.l10n.spentPoints(points.abs()); + } else { + return context.l10n.pointsBalance(points); + } + } + + /// Format item count with pluralization + static String formatItemCount(BuildContext context, int count) { + return context.l10n.itemsInCart(count); + } + + /// Format order count with pluralization + static String formatOrderCount(BuildContext context, int count) { + return context.l10n.ordersCount(count); + } + + /// Format project count with pluralization + static String formatProjectCount(BuildContext context, int count) { + return context.l10n.projectsCount(count); + } + + /// Format days remaining with pluralization + static String formatDaysRemaining(BuildContext context, int days) { + return context.l10n.daysRemaining(days); + } +} diff --git a/lib/core/utils/localization_extension.dart b/lib/core/utils/localization_extension.dart new file mode 100644 index 0000000..8d52147 --- /dev/null +++ b/lib/core/utils/localization_extension.dart @@ -0,0 +1,136 @@ +import 'package:flutter/widgets.dart'; + +import 'package:worker/generated/l10n/app_localizations.dart'; + +/// Extension on [BuildContext] for easy access to localizations +/// +/// Usage: +/// ```dart +/// Text(context.l10n.login) +/// ``` +/// +/// This provides a shorter and more convenient way to access localized strings +/// compared to the verbose `AppLocalizations.of(context)!` syntax. +extension LocalizationExtension on BuildContext { + /// Get the current app localizations + /// + /// Returns the [AppLocalizations] instance for the current context. + /// This will never be null because the app always has a default locale. + AppLocalizations get l10n => AppLocalizations.of(this); +} + +/// Extension on [AppLocalizations] for additional formatting utilities +extension LocalizationUtilities on AppLocalizations { + /// Format currency in Vietnamese Dong + /// + /// Example: 100000 -> "100.000 ₫" + String formatCurrency(double amount) { + final formatter = _getCurrencyFormatter(); + return formatter.format(amount); + } + + /// Format points display with formatted number + /// + /// Example: 1500 -> "1.500 điểm" or "1,500 points" + String formatPointsDisplay(int points) { + // Use the generated method which already handles the formatting + return pointsBalance(points); + } + + /// Format large numbers with thousand separators + /// + /// Example: 1000000 -> "1.000.000" + String formatNumber(num number) { + return _formatNumber(number); + } + + /// Get currency formatter based on locale + _CurrencyFormatter _getCurrencyFormatter() { + if (localeName.startsWith('vi')) { + return const _VietnameseCurrencyFormatter(); + } else { + return const _EnglishCurrencyFormatter(); + } + } + + /// Format number with thousand separators + String _formatNumber(num number) { + final parts = number.toString().split('.'); + final integerPart = parts[0]; + final decimalPart = parts.length > 1 ? parts[1] : ''; + + // Add thousand separators + final buffer = StringBuffer(); + final reversedInteger = integerPart.split('').reversed.join(); + + for (var i = 0; i < reversedInteger.length; i++) { + if (i > 0 && i % 3 == 0) { + buffer.write(localeName.startsWith('vi') ? '.' : ','); + } + buffer.write(reversedInteger[i]); + } + + final formattedInteger = buffer.toString().split('').reversed.join(); + + if (decimalPart.isNotEmpty) { + return '$formattedInteger.$decimalPart'; + } + + return formattedInteger; + } +} + +/// Abstract currency formatter +abstract class _CurrencyFormatter { + const _CurrencyFormatter(); + + String format(double amount); +} + +/// Vietnamese currency formatter +/// +/// Format: 100.000 ₫ +class _VietnameseCurrencyFormatter extends _CurrencyFormatter { + const _VietnameseCurrencyFormatter(); + + @override + String format(double amount) { + final rounded = amount.round(); + final parts = rounded.toString().split('').reversed.join(); + + final buffer = StringBuffer(); + for (var i = 0; i < parts.length; i++) { + if (i > 0 && i % 3 == 0) { + buffer.write('.'); + } + buffer.write(parts[i]); + } + + final formatted = buffer.toString().split('').reversed.join(); + return '$formatted ₫'; + } +} + +/// English currency formatter +/// +/// Format: ₫100,000 +class _EnglishCurrencyFormatter extends _CurrencyFormatter { + const _EnglishCurrencyFormatter(); + + @override + String format(double amount) { + final rounded = amount.round(); + final parts = rounded.toString().split('').reversed.join(); + + final buffer = StringBuffer(); + for (var i = 0; i < parts.length; i++) { + if (i > 0 && i % 3 == 0) { + buffer.write(','); + } + buffer.write(parts[i]); + } + + final formatted = buffer.toString().split('').reversed.join(); + return '₫$formatted'; + } +} diff --git a/lib/core/utils/qr_generator.dart b/lib/core/utils/qr_generator.dart new file mode 100644 index 0000000..ede4d36 --- /dev/null +++ b/lib/core/utils/qr_generator.dart @@ -0,0 +1,308 @@ +/// QR Code Generator Utility +/// +/// Provides QR code generation functionality for member cards, +/// referral codes, and other QR code use cases. +library; + +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +/// QR Code Generator +class QRGenerator { + QRGenerator._(); + + /// Generate QR code widget for member ID + /// + /// Used in member cards to display user's member ID as QR code + static Widget generateMemberQR({ + required String memberId, + double size = 80.0, + Color? foregroundColor, + Color? backgroundColor, + int version = QrVersions.auto, + int errorCorrectionLevel = QrErrorCorrectLevel.M, + }) { + return QrImageView( + data: 'MEMBER:$memberId', + version: version, + size: size, + errorCorrectionLevel: errorCorrectionLevel, + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: EdgeInsets.zero, + gapless: true, + ); + } + + /// Generate QR code widget for referral code + /// + /// Used to share referral codes via QR scanning + static Widget generateReferralQR({ + required String referralCode, + double size = 200.0, + Color? foregroundColor, + Color? backgroundColor, + }) { + return QrImageView( + data: 'REFERRAL:$referralCode', + version: QrVersions.auto, + size: size, + errorCorrectionLevel: QrErrorCorrectLevel.H, + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: const EdgeInsets.all(16), + gapless: true, + embeddedImageStyle: const QrEmbeddedImageStyle( + size: Size(48, 48), + ), + ); + } + + /// Generate QR code widget for order tracking + /// + /// Used to display order number as QR code for easy tracking + static Widget generateOrderQR({ + required String orderNumber, + double size = 150.0, + Color? foregroundColor, + Color? backgroundColor, + }) { + return QrImageView( + data: 'ORDER:$orderNumber', + version: QrVersions.auto, + size: size, + errorCorrectionLevel: QrErrorCorrectLevel.M, + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: const EdgeInsets.all(12), + gapless: true, + ); + } + + /// Generate QR code widget for product info + /// + /// Used to encode product SKU or URL for quick access + static Widget generateProductQR({ + required String productId, + double size = 120.0, + Color? foregroundColor, + Color? backgroundColor, + }) { + return QrImageView( + data: 'PRODUCT:$productId', + version: QrVersions.auto, + size: size, + errorCorrectionLevel: QrErrorCorrectLevel.M, + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: const EdgeInsets.all(10), + gapless: true, + ); + } + + /// Generate QR code widget with custom data + /// + /// Generic QR code generator for any string data + static Widget generateCustomQR({ + required String data, + double size = 200.0, + Color? foregroundColor, + Color? backgroundColor, + int errorCorrectionLevel = QrErrorCorrectLevel.M, + EdgeInsets padding = const EdgeInsets.all(16), + bool gapless = true, + }) { + return QrImageView( + data: data, + version: QrVersions.auto, + size: size, + errorCorrectionLevel: errorCorrectionLevel, + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: padding, + gapless: gapless, + ); + } + + /// Generate QR code widget with embedded logo + /// + /// Used for branded QR codes with app logo in center + static Widget generateQRWithLogo({ + required String data, + required Widget embeddedImage, + double size = 250.0, + Color? foregroundColor, + Color? backgroundColor, + Size embeddedImageSize = const Size(64, 64), + }) { + return QrImageView( + data: data, + version: QrVersions.auto, + size: size, + errorCorrectionLevel: QrErrorCorrectLevel.H, // High correction for logo + backgroundColor: backgroundColor ?? Colors.white, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: foregroundColor ?? Colors.black, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: foregroundColor ?? Colors.black, + ), + padding: const EdgeInsets.all(20), + gapless: true, + embeddedImage: embeddedImage is AssetImage + ? (embeddedImage as AssetImage).assetName as ImageProvider + : null, + embeddedImageStyle: QrEmbeddedImageStyle( + size: embeddedImageSize, + ), + ); + } + + /// Parse QR code data and extract type and value + /// + /// Returns a map with 'type' and 'value' keys + static Map? parseQRData(String data) { + try { + if (data.contains(':')) { + final parts = data.split(':'); + if (parts.length == 2) { + return { + 'type': parts[0].toUpperCase(), + 'value': parts[1], + }; + } + } + + // If no type prefix, return as generic data + return { + 'type': 'GENERIC', + 'value': data, + }; + } catch (e) { + return null; + } + } + + /// Validate QR code data format + static bool isValidQRData(String data, {String? expectedType}) { + if (data.isEmpty) return false; + + final parsed = parseQRData(data); + if (parsed == null) return false; + + if (expectedType != null) { + return parsed['type'] == expectedType.toUpperCase(); + } + + return true; + } + + /// Generate QR data string with type prefix + static String generateQRData(String type, String value) { + return '${type.toUpperCase()}:$value'; + } +} + +/// QR Code Types +class QRCodeType { + QRCodeType._(); + + static const String member = 'MEMBER'; + static const String referral = 'REFERRAL'; + static const String order = 'ORDER'; + static const String product = 'PRODUCT'; + static const String payment = 'PAYMENT'; + static const String url = 'URL'; + static const String generic = 'GENERIC'; +} + +/// QR Code Scanner Result +class QRScanResult { + final String type; + final String value; + final String rawData; + + const QRScanResult({ + required this.type, + required this.value, + required this.rawData, + }); + + /// Check if scan result is of expected type + bool isType(String expectedType) { + return type.toUpperCase() == expectedType.toUpperCase(); + } + + /// Check if result is a member QR code + bool get isMember => isType(QRCodeType.member); + + /// Check if result is a referral QR code + bool get isReferral => isType(QRCodeType.referral); + + /// Check if result is an order QR code + bool get isOrder => isType(QRCodeType.order); + + /// Check if result is a product QR code + bool get isProduct => isType(QRCodeType.product); + + /// Check if result is a URL QR code + bool get isUrl => isType(QRCodeType.url); + + factory QRScanResult.fromRawData(String rawData) { + final parsed = QRGenerator.parseQRData(rawData); + + if (parsed != null) { + return QRScanResult( + type: parsed['type']!, + value: parsed['value']!, + rawData: rawData, + ); + } + + return QRScanResult( + type: QRCodeType.generic, + value: rawData, + rawData: rawData, + ); + } + + @override + String toString() => 'QRScanResult(type: $type, value: $value)'; +} diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..16c710e --- /dev/null +++ b/lib/core/utils/validators.dart @@ -0,0 +1,540 @@ +/// Form Validators for Vietnamese Locale +/// +/// Provides validation utilities for forms with Vietnamese-specific +/// validations for phone numbers, email, passwords, etc. +library; + +/// Form field validators +class Validators { + Validators._(); + + // ======================================================================== + // Required Field Validators + // ======================================================================== + + /// Validate required field + static String? required(String? value, {String? fieldName}) { + if (value == null || value.trim().isEmpty) { + return fieldName != null + ? '$fieldName là bắt buộc' + : 'Trường này là bắt buộc'; + } + return null; + } + + // ======================================================================== + // Phone Number Validators + // ======================================================================== + + /// Validate Vietnamese phone number + /// + /// Accepts formats: + /// - 0xxx xxx xxx (10 digits starting with 0) + /// - +84xxx xxx xxx (starts with +84) + /// - 84xxx xxx xxx (starts with 84) + static String? phone(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập số điện thoại'; + } + + final cleaned = value.replaceAll(RegExp(r'\D'), ''); + + // Check if starts with valid Vietnamese mobile prefix + final vietnamesePattern = RegExp(r'^(0|\+?84)(3|5|7|8|9)[0-9]{8}$'); + + if (!vietnamesePattern.hasMatch(value.replaceAll(RegExp(r'[^\d+]'), ''))) { + return 'Số điện thoại không hợp lệ'; + } + + if (cleaned.length < 10 || cleaned.length > 11) { + return 'Số điện thoại phải có 10 chữ số'; + } + + return null; + } + + /// Validate phone number (optional) + static String? phoneOptional(String? value) { + if (value == null || value.trim().isEmpty) { + return null; // Optional, so null is valid + } + return phone(value); + } + + // ======================================================================== + // Email Validators + // ======================================================================== + + /// Validate email address + static String? email(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập email'; + } + + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (!emailRegex.hasMatch(value)) { + return 'Email không hợp lệ'; + } + + return null; + } + + /// Validate email (optional) + static String? emailOptional(String? value) { + if (value == null || value.trim().isEmpty) { + return null; + } + return email(value); + } + + // ======================================================================== + // Password Validators + // ======================================================================== + + /// Validate password strength + /// + /// Requirements: + /// - At least 8 characters + /// - At least 1 uppercase letter + /// - At least 1 lowercase letter + /// - At least 1 number + /// - At least 1 special character + static String? password(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập mật khẩu'; + } + + if (value.length < 8) { + return 'Mật khẩu phải có ít nhất 8 ký tự'; + } + + if (!RegExp(r'[A-Z]').hasMatch(value)) { + return 'Mật khẩu phải có ít nhất 1 chữ hoa'; + } + + if (!RegExp(r'[a-z]').hasMatch(value)) { + return 'Mật khẩu phải có ít nhất 1 chữ thường'; + } + + if (!RegExp(r'[0-9]').hasMatch(value)) { + return 'Mật khẩu phải có ít nhất 1 số'; + } + + if (!RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(value)) { + return 'Mật khẩu phải có ít nhất 1 ký tự đặc biệt'; + } + + return null; + } + + /// Validate password confirmation + static String? confirmPassword(String? value, String? password) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng xác nhận mật khẩu'; + } + + if (value != password) { + return 'Mật khẩu không khớp'; + } + + return null; + } + + /// Simple password validator (minimum length only) + static String? passwordSimple(String? value, {int minLength = 6}) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập mật khẩu'; + } + + if (value.length < minLength) { + return 'Mật khẩu phải có ít nhất $minLength ký tự'; + } + + return null; + } + + // ======================================================================== + // OTP Validators + // ======================================================================== + + /// Validate OTP code + static String? otp(String? value, {int length = 6}) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập mã OTP'; + } + + if (value.length != length) { + return 'Mã OTP phải có $length chữ số'; + } + + if (!RegExp(r'^[0-9]+$').hasMatch(value)) { + return 'Mã OTP chỉ được chứa số'; + } + + return null; + } + + // ======================================================================== + // Text Length Validators + // ======================================================================== + + /// Validate minimum length + static String? minLength(String? value, int min, {String? fieldName}) { + if (value == null || value.trim().isEmpty) { + return fieldName != null + ? '$fieldName là bắt buộc' + : 'Trường này là bắt buộc'; + } + + if (value.length < min) { + return fieldName != null + ? '$fieldName phải có ít nhất $min ký tự' + : 'Phải có ít nhất $min ký tự'; + } + + return null; + } + + /// Validate maximum length + static String? maxLength(String? value, int max, {String? fieldName}) { + if (value != null && value.length > max) { + return fieldName != null + ? '$fieldName không được vượt quá $max ký tự' + : 'Không được vượt quá $max ký tự'; + } + + return null; + } + + /// Validate length range + static String? lengthRange( + String? value, + int min, + int max, { + String? fieldName, + }) { + if (value == null || value.trim().isEmpty) { + return fieldName != null + ? '$fieldName là bắt buộc' + : 'Trường này là bắt buộc'; + } + + if (value.length < min || value.length > max) { + return fieldName != null + ? '$fieldName phải có từ $min đến $max ký tự' + : 'Phải có từ $min đến $max ký tự'; + } + + return null; + } + + // ======================================================================== + // Number Validators + // ======================================================================== + + /// Validate number + static String? number(String? value, {String? fieldName}) { + if (value == null || value.trim().isEmpty) { + return fieldName != null + ? '$fieldName là bắt buộc' + : 'Trường này là bắt buộc'; + } + + if (double.tryParse(value) == null) { + return fieldName != null + ? '$fieldName phải là số' + : 'Giá trị phải là số'; + } + + return null; + } + + /// Validate integer + static String? integer(String? value, {String? fieldName}) { + if (value == null || value.trim().isEmpty) { + return fieldName != null + ? '$fieldName là bắt buộc' + : 'Trường này là bắt buộc'; + } + + if (int.tryParse(value) == null) { + return fieldName != null + ? '$fieldName phải là số nguyên' + : 'Giá trị phải là số nguyên'; + } + + return null; + } + + /// Validate positive number + static String? positiveNumber(String? value, {String? fieldName}) { + final numberError = number(value, fieldName: fieldName); + if (numberError != null) return numberError; + + final num = double.parse(value!); + if (num <= 0) { + return fieldName != null + ? '$fieldName phải lớn hơn 0' + : 'Giá trị phải lớn hơn 0'; + } + + return null; + } + + /// Validate number range + static String? numberRange( + String? value, + double min, + double max, { + String? fieldName, + }) { + final numberError = number(value, fieldName: fieldName); + if (numberError != null) return numberError; + + final num = double.parse(value!); + if (num < min || num > max) { + return fieldName != null + ? '$fieldName phải từ $min đến $max' + : 'Giá trị phải từ $min đến $max'; + } + + return null; + } + + // ======================================================================== + // Date Validators + // ======================================================================== + + /// Validate date format (dd/MM/yyyy) + static String? date(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập ngày'; + } + + final dateRegex = RegExp(r'^\d{2}/\d{2}/\d{4}$'); + if (!dateRegex.hasMatch(value)) { + return 'Định dạng ngày không hợp lệ (dd/MM/yyyy)'; + } + + try { + final parts = value.split('/'); + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + + final date = DateTime(year, month, day); + + if (date.day != day || date.month != month || date.year != year) { + return 'Ngày không hợp lệ'; + } + } catch (e) { + return 'Ngày không hợp lệ'; + } + + return null; + } + + /// Validate age (must be at least 18 years old) + static String? age(String? value, {int minAge = 18}) { + final dateError = date(value); + if (dateError != null) return dateError; + + try { + final parts = value!.split('/'); + final birthDate = DateTime( + int.parse(parts[2]), + int.parse(parts[1]), + int.parse(parts[0]), + ); + + final today = DateTime.now(); + final age = today.year - + birthDate.year - + (today.month > birthDate.month || + (today.month == birthDate.month && today.day >= birthDate.day) + ? 0 + : 1); + + if (age < minAge) { + return 'Bạn phải từ $minAge tuổi trở lên'; + } + + return null; + } catch (e) { + return 'Ngày sinh không hợp lệ'; + } + } + + // ======================================================================== + // Address Validators + // ======================================================================== + + /// Validate Vietnamese address + static String? address(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập địa chỉ'; + } + + if (value.length < 10) { + return 'Địa chỉ quá ngắn'; + } + + return null; + } + + // ======================================================================== + // Tax ID Validators + // ======================================================================== + + /// Validate Vietnamese Tax ID (Mã số thuế) + /// Format: 10 or 13 digits + static String? taxId(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập mã số thuế'; + } + + final cleaned = value.replaceAll(RegExp(r'\D'), ''); + + if (cleaned.length != 10 && cleaned.length != 13) { + return 'Mã số thuế phải có 10 hoặc 13 chữ số'; + } + + return null; + } + + /// Validate tax ID (optional) + static String? taxIdOptional(String? value) { + if (value == null || value.trim().isEmpty) { + return null; + } + return taxId(value); + } + + // ======================================================================== + // URL Validators + // ======================================================================== + + /// Validate URL + static String? url(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập URL'; + } + + final urlRegex = RegExp( + r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$', + ); + + if (!urlRegex.hasMatch(value)) { + return 'URL không hợp lệ'; + } + + return null; + } + + // ======================================================================== + // Combination Validators + // ======================================================================== + + /// Combine multiple validators + static String? Function(String?) combine( + List validators, + ) { + return (String? value) { + for (final validator in validators) { + final error = validator(value); + if (error != null) return error; + } + return null; + }; + } + + // ======================================================================== + // Custom Pattern Validators + // ======================================================================== + + /// Validate against custom regex pattern + 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'; + } + + if (!pattern.hasMatch(value)) { + return errorMessage; + } + + return null; + } + + // ======================================================================== + // Match Validators + // ======================================================================== + + /// Validate that value matches another value + static String? match(String? value, String? matchValue, String fieldName) { + if (value == null || value.trim().isEmpty) { + return 'Vui lòng nhập $fieldName'; + } + + if (value != matchValue) { + return '$fieldName không khớp'; + } + + return null; + } +} + +/// Password strength enum +enum PasswordStrength { + weak, + medium, + strong, + veryStrong, +} + +/// Password strength calculator +class PasswordStrengthCalculator { + /// Calculate password strength + static PasswordStrength calculate(String password) { + if (password.isEmpty) return PasswordStrength.weak; + + var score = 0; + + // Length check + if (password.length >= 8) score++; + if (password.length >= 12) score++; + if (password.length >= 16) score++; + + // Character variety check + if (RegExp(r'[a-z]').hasMatch(password)) score++; + if (RegExp(r'[A-Z]').hasMatch(password)) score++; + if (RegExp(r'[0-9]').hasMatch(password)) score++; + if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) score++; + + // Return strength based on score + if (score <= 2) return PasswordStrength.weak; + if (score <= 4) return PasswordStrength.medium; + if (score <= 6) return PasswordStrength.strong; + return PasswordStrength.veryStrong; + } + + /// Get strength label in Vietnamese + static String getLabel(PasswordStrength strength) { + switch (strength) { + case PasswordStrength.weak: + return 'Yếu'; + case PasswordStrength.medium: + return 'Trung bình'; + case PasswordStrength.strong: + return 'Mạnh'; + case PasswordStrength.veryStrong: + return 'Rất mạnh'; + } + } +} diff --git a/lib/core/widgets/bottom_nav_bar.dart b/lib/core/widgets/bottom_nav_bar.dart new file mode 100644 index 0000000..8898c23 --- /dev/null +++ b/lib/core/widgets/bottom_nav_bar.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Custom bottom navigation bar for the Worker app. +/// +/// This widget will be fully implemented once navigation system is in place. +/// It will support 5 main tabs: Home, Products, Loyalty, Account, and More. +/// +/// Example usage: +/// ```dart +/// CustomBottomNavBar( +/// currentIndex: _currentIndex, +/// onTap: (index) => _onNavigate(index), +/// ) +/// ``` +class CustomBottomNavBar extends StatelessWidget { + /// Current selected tab index + final int currentIndex; + + /// Callback when a tab is tapped + final ValueChanged onTap; + + /// Optional badge count for notifications + final int? badgeCount; + + const CustomBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + this.badgeCount, + }); + + @override + Widget build(BuildContext context) { + // Will be implemented with navigation + // TODO: Implement full bottom navigation with: + // - Home tab (home icon) + // - Products tab (shopping_bag icon) + // - Loyalty tab (card_membership icon) + // - Account tab (person icon) + // - More tab (menu icon) with notification badge + // + // Design specs: + // - Height: 72px + // - Icon size: 24px (selected: 28px) + // - Label font size: 12px + // - Selected color: primaryBlue + // - Unselected color: grey500 + // - Badge: red circle with white text + + return BottomNavigationBar( + currentIndex: currentIndex, + onTap: onTap, + type: BottomNavigationBarType.fixed, + selectedItemColor: AppColors.primaryBlue, + unselectedItemColor: AppColors.grey500, + selectedFontSize: 12, + unselectedFontSize: 12, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.shopping_bag), + label: 'Products', + ), + BottomNavigationBarItem( + icon: Icon(Icons.card_membership), + label: 'Loyalty', + ), + BottomNavigationBarItem( + icon: Icon(Icons.person), + label: 'Account', + ), + BottomNavigationBarItem( + icon: Icon(Icons.menu), + label: 'More', + ), + ], + ); + } +} diff --git a/lib/core/widgets/custom_button.dart b/lib/core/widgets/custom_button.dart new file mode 100644 index 0000000..19baa3c --- /dev/null +++ b/lib/core/widgets/custom_button.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Button variant types for different use cases. +enum ButtonVariant { + /// Primary button with filled background color + primary, + + /// Secondary button with outlined border + secondary, +} + +/// Custom button widget following the Worker app design system. +/// +/// Supports primary and secondary variants, loading states, and disabled states. +/// +/// Example usage: +/// ```dart +/// CustomButton( +/// text: 'Login', +/// onPressed: () => _handleLogin(), +/// variant: ButtonVariant.primary, +/// isLoading: _isLoading, +/// ) +/// ``` +class CustomButton extends StatelessWidget { + /// The text to display on the button + final String text; + + /// Callback when button is pressed. If null, button is disabled. + final VoidCallback? onPressed; + + /// Visual variant of the button (primary or secondary) + final ButtonVariant variant; + + /// Whether to show loading indicator instead of text + final bool isLoading; + + /// Optional icon to display before the text + final IconData? icon; + + /// Custom width for the button. If null, uses parent constraints. + final double? width; + + /// Custom height for the button. Defaults to 48. + final double? height; + + const CustomButton({ + super.key, + required this.text, + required this.onPressed, + this.variant = ButtonVariant.primary, + this.isLoading = false, + this.icon, + this.width, + this.height, + }); + + @override + Widget build(BuildContext context) { + final isDisabled = onPressed == null || isLoading; + + if (variant == ButtonVariant.primary) { + return SizedBox( + width: width, + height: height ?? 48, + child: ElevatedButton( + onPressed: isDisabled ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + disabledBackgroundColor: AppColors.grey500, + disabledForegroundColor: Colors.white70, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _buildContent(), + ), + ); + } else { + return SizedBox( + width: width, + height: height ?? 48, + child: OutlinedButton( + onPressed: isDisabled ? null : onPressed, + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primaryBlue, + disabledForegroundColor: AppColors.grey500, + side: BorderSide( + color: isDisabled ? AppColors.grey500 : AppColors.primaryBlue, + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: _buildContent(), + ), + ); + } + } + + /// Builds the button content (text, icon, or loading indicator) + Widget _buildContent() { + if (isLoading) { + return const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + } + + if (icon != null) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 20), + const SizedBox(width: 8), + Text( + text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + return Text( + text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ); + } +} diff --git a/lib/core/widgets/empty_state.dart b/lib/core/widgets/empty_state.dart new file mode 100644 index 0000000..707160b --- /dev/null +++ b/lib/core/widgets/empty_state.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Empty state widget for displaying when lists or collections are empty. +/// +/// Shows an icon, title, subtitle, and optional action button to guide users +/// when there's no content to display. +/// +/// Example usage: +/// ```dart +/// EmptyState( +/// icon: Icons.shopping_cart_outlined, +/// title: 'Your cart is empty', +/// subtitle: 'Add some products to get started', +/// actionLabel: 'Browse Products', +/// onAction: () => Navigator.pushNamed(context, '/products'), +/// ) +/// ``` +class EmptyState extends StatelessWidget { + /// Icon to display at the top + final IconData icon; + + /// Main title text + final String title; + + /// Optional subtitle/description text + final String? subtitle; + + /// Optional action button label. If null, no button is shown. + final String? actionLabel; + + /// Optional callback for action button + final VoidCallback? onAction; + + /// Size of the icon. Defaults to 80. + final double iconSize; + + const EmptyState({ + super.key, + required this.icon, + required this.title, + this.subtitle, + this.actionLabel, + this.onAction, + this.iconSize = 80, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: iconSize, + color: AppColors.grey500, + ), + const SizedBox(height: 16), + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + textAlign: TextAlign.center, + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + textAlign: TextAlign.center, + ), + ], + if (actionLabel != null && onAction != null) ...[ + const SizedBox(height: 24), + ElevatedButton( + onPressed: onAction, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + actionLabel!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/core/widgets/error_widget.dart b/lib/core/widgets/error_widget.dart new file mode 100644 index 0000000..9c2e5a9 --- /dev/null +++ b/lib/core/widgets/error_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Custom error widget for displaying error states with retry functionality. +/// +/// Shows an error icon, message, and optional retry button. Used throughout +/// the app for error states in async operations. +/// +/// Example usage: +/// ```dart +/// CustomErrorWidget( +/// message: 'Failed to load products', +/// onRetry: () => _loadProducts(), +/// ) +/// ``` +class CustomErrorWidget extends StatelessWidget { + /// Error message to display + final String message; + + /// Optional callback for retry button. If null, no button is shown. + final VoidCallback? onRetry; + + /// Optional icon to display. Defaults to error_outline. + final IconData? icon; + + /// Size of the error icon. Defaults to 64. + final double iconSize; + + const CustomErrorWidget({ + super.key, + required this.message, + this.onRetry, + this.icon, + this.iconSize = 64, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon ?? Icons.error_outline, + size: iconSize, + color: AppColors.danger, + ), + const SizedBox(height: 16), + Text( + message, + style: const TextStyle( + fontSize: 16, + color: AppColors.grey900, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + if (onRetry != null) ...[ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/core/widgets/floating_chat_button.dart b/lib/core/widgets/floating_chat_button.dart new file mode 100644 index 0000000..2c56502 --- /dev/null +++ b/lib/core/widgets/floating_chat_button.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Floating action button for chat support access. +/// +/// Positioned at bottom-right of the screen with accent cyan color. +/// Opens chat support when tapped. +/// +/// Example usage: +/// ```dart +/// Scaffold( +/// floatingActionButton: ChatFloatingButton( +/// onPressed: () => Navigator.pushNamed(context, '/chat'), +/// ), +/// ) +/// ``` +class ChatFloatingButton extends StatelessWidget { + /// Callback when the button is pressed + final VoidCallback onPressed; + + /// Optional badge count for unread messages + final int? unreadCount; + + /// Size of the FAB. Defaults to 56. + final double size; + + const ChatFloatingButton({ + super.key, + required this.onPressed, + this.unreadCount, + this.size = 56, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: size, + height: size, + child: Stack( + children: [ + FloatingActionButton( + onPressed: onPressed, + backgroundColor: AppColors.accentCyan, + elevation: 6, + child: const Icon( + Icons.chat_bubble_outline, + color: Colors.white, + size: 24, + ), + ), + if (unreadCount != null && unreadCount! > 0) + Positioned( + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.danger, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + constraints: const BoxConstraints( + minWidth: 20, + minHeight: 20, + ), + child: Center( + child: Text( + unreadCount! > 99 ? '99+' : unreadCount.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/core/widgets/loading_indicator.dart b/lib/core/widgets/loading_indicator.dart new file mode 100644 index 0000000..6dcb282 --- /dev/null +++ b/lib/core/widgets/loading_indicator.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:worker/core/theme/colors.dart'; + +/// Custom loading indicator widget with optional message text. +/// +/// Displays a centered circular progress indicator with an optional +/// message below it. Used for loading states throughout the app. +/// +/// Example usage: +/// ```dart +/// CustomLoadingIndicator( +/// message: 'Loading products...', +/// ) +/// ``` +class CustomLoadingIndicator extends StatelessWidget { + /// Optional message to display below the loading indicator + final String? message; + + /// Size of the loading indicator. Defaults to 40. + final double size; + + /// Color of the loading indicator. Defaults to primaryBlue. + final Color? color; + + const CustomLoadingIndicator({ + super.key, + this.message, + this.size = 40, + this.color, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: size, + height: size, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + color ?? AppColors.primaryBlue, + ), + ), + ), + if (message != null) ...[ + const SizedBox(height: 16), + Text( + message!, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ); + } +} diff --git a/lib/generated/l10n/app_localizations.dart b/lib/generated/l10n/app_localizations.dart new file mode 100644 index 0000000..782d391 --- /dev/null +++ b/lib/generated/l10n/app_localizations.dart @@ -0,0 +1,3368 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_vi.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('vi'), + ]; + + /// The application title + /// + /// In en, this message translates to: + /// **'Worker App'** + String get appTitle; + + /// Home navigation item + /// + /// In en, this message translates to: + /// **'Home'** + String get home; + + /// Products navigation item + /// + /// In en, this message translates to: + /// **'Products'** + String get products; + + /// Loyalty navigation item + /// + /// In en, this message translates to: + /// **'Loyalty'** + String get loyalty; + + /// Account navigation item + /// + /// In en, this message translates to: + /// **'Account'** + String get account; + + /// More navigation item + /// + /// In en, this message translates to: + /// **'More'** + String get more; + + /// No description provided for @login. + /// + /// In en, this message translates to: + /// **'Login'** + String get login; + + /// No description provided for @phone. + /// + /// In en, this message translates to: + /// **'Phone Number'** + String get phone; + + /// No description provided for @enterPhone. + /// + /// In en, this message translates to: + /// **'Enter phone number'** + String get enterPhone; + + /// No description provided for @enterPhoneHint. + /// + /// In en, this message translates to: + /// **'Ex: 0912345678'** + String get enterPhoneHint; + + /// No description provided for @continueButton. + /// + /// In en, this message translates to: + /// **'Continue'** + String get continueButton; + + /// No description provided for @verifyOTP. + /// + /// In en, this message translates to: + /// **'Verify OTP'** + String get verifyOTP; + + /// No description provided for @enterOTP. + /// + /// In en, this message translates to: + /// **'Enter 6-digit OTP code'** + String get enterOTP; + + /// OTP sent message + /// + /// In en, this message translates to: + /// **'OTP code has been sent to {phone}'** + String otpSentTo(String phone); + + /// No description provided for @resendOTP. + /// + /// In en, this message translates to: + /// **'Resend code'** + String get resendOTP; + + /// Resend OTP countdown + /// + /// In en, this message translates to: + /// **'Resend in {seconds}s'** + String resendOTPIn(int seconds); + + /// No description provided for @register. + /// + /// In en, this message translates to: + /// **'Register'** + String get register; + + /// No description provided for @registerNewAccount. + /// + /// In en, this message translates to: + /// **'Register new account'** + String get registerNewAccount; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @logoutConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to logout?'** + String get logoutConfirm; + + /// No description provided for @save. + /// + /// In en, this message translates to: + /// **'Save'** + String get save; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + + /// No description provided for @filter. + /// + /// In en, this message translates to: + /// **'Filter'** + String get filter; + + /// No description provided for @sort. + /// + /// In en, this message translates to: + /// **'Sort'** + String get sort; + + /// No description provided for @confirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// No description provided for @close. + /// + /// In en, this message translates to: + /// **'Close'** + String get close; + + /// No description provided for @back. + /// + /// In en, this message translates to: + /// **'Back'** + String get back; + + /// No description provided for @next. + /// + /// In en, this message translates to: + /// **'Next'** + String get next; + + /// No description provided for @submit. + /// + /// In en, this message translates to: + /// **'Submit'** + String get submit; + + /// No description provided for @apply. + /// + /// In en, this message translates to: + /// **'Apply'** + String get apply; + + /// No description provided for @clear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get clear; + + /// No description provided for @clearAll. + /// + /// In en, this message translates to: + /// **'Clear All'** + String get clearAll; + + /// No description provided for @viewDetails. + /// + /// In en, this message translates to: + /// **'View Details'** + String get viewDetails; + + /// No description provided for @viewAll. + /// + /// In en, this message translates to: + /// **'View All'** + String get viewAll; + + /// No description provided for @refresh. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get refresh; + + /// No description provided for @share. + /// + /// In en, this message translates to: + /// **'Share'** + String get share; + + /// No description provided for @copy. + /// + /// In en, this message translates to: + /// **'Copy'** + String get copy; + + /// No description provided for @copied. + /// + /// In en, this message translates to: + /// **'Copied'** + String get copied; + + /// No description provided for @yes. + /// + /// In en, this message translates to: + /// **'Yes'** + String get yes; + + /// No description provided for @no. + /// + /// In en, this message translates to: + /// **'No'** + String get no; + + /// No description provided for @pending. + /// + /// In en, this message translates to: + /// **'Pending'** + String get pending; + + /// No description provided for @processing. + /// + /// In en, this message translates to: + /// **'Processing...'** + String get processing; + + /// No description provided for @shipping. + /// + /// In en, this message translates to: + /// **'Shipping'** + String get shipping; + + /// No description provided for @completed. + /// + /// In en, this message translates to: + /// **'Completed'** + String get completed; + + /// No description provided for @cancelled. + /// + /// In en, this message translates to: + /// **'Cancelled'** + String get cancelled; + + /// No description provided for @active. + /// + /// In en, this message translates to: + /// **'Active'** + String get active; + + /// No description provided for @inactive. + /// + /// In en, this message translates to: + /// **'Inactive'** + String get inactive; + + /// No description provided for @expired. + /// + /// In en, this message translates to: + /// **'Expired'** + String get expired; + + /// No description provided for @draft. + /// + /// In en, this message translates to: + /// **'Draft'** + String get draft; + + /// No description provided for @sent. + /// + /// In en, this message translates to: + /// **'Sent'** + String get sent; + + /// No description provided for @accepted. + /// + /// In en, this message translates to: + /// **'Accepted'** + String get accepted; + + /// No description provided for @rejected. + /// + /// In en, this message translates to: + /// **'Rejected'** + String get rejected; + + /// No description provided for @name. + /// + /// In en, this message translates to: + /// **'Name'** + String get name; + + /// No description provided for @fullName. + /// + /// In en, this message translates to: + /// **'Full Name'** + String get fullName; + + /// No description provided for @email. + /// + /// In en, this message translates to: + /// **'Email'** + String get email; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @currentPassword. + /// + /// In en, this message translates to: + /// **'Current Password'** + String get currentPassword; + + /// No description provided for @newPassword. + /// + /// In en, this message translates to: + /// **'New Password'** + String get newPassword; + + /// No description provided for @confirmPassword. + /// + /// In en, this message translates to: + /// **'Confirm Password'** + String get confirmPassword; + + /// No description provided for @address. + /// + /// In en, this message translates to: + /// **'Address'** + String get address; + + /// No description provided for @street. + /// + /// In en, this message translates to: + /// **'Street'** + String get street; + + /// No description provided for @city. + /// + /// In en, this message translates to: + /// **'City'** + String get city; + + /// No description provided for @district. + /// + /// In en, this message translates to: + /// **'District'** + String get district; + + /// No description provided for @ward. + /// + /// In en, this message translates to: + /// **'Ward'** + String get ward; + + /// No description provided for @postalCode. + /// + /// In en, this message translates to: + /// **'Postal Code'** + String get postalCode; + + /// No description provided for @company. + /// + /// In en, this message translates to: + /// **'Company'** + String get company; + + /// No description provided for @taxId. + /// + /// In en, this message translates to: + /// **'Tax ID'** + String get taxId; + + /// No description provided for @dateOfBirth. + /// + /// In en, this message translates to: + /// **'Date of Birth'** + String get dateOfBirth; + + /// No description provided for @gender. + /// + /// In en, this message translates to: + /// **'Gender'** + String get gender; + + /// No description provided for @male. + /// + /// In en, this message translates to: + /// **'Male'** + String get male; + + /// No description provided for @female. + /// + /// In en, this message translates to: + /// **'Female'** + String get female; + + /// No description provided for @other. + /// + /// In en, this message translates to: + /// **'Other'** + String get other; + + /// No description provided for @contractor. + /// + /// In en, this message translates to: + /// **'Contractor'** + String get contractor; + + /// No description provided for @architect. + /// + /// In en, this message translates to: + /// **'Architect'** + String get architect; + + /// No description provided for @distributor. + /// + /// In en, this message translates to: + /// **'Distributor'** + String get distributor; + + /// No description provided for @broker. + /// + /// In en, this message translates to: + /// **'Broker'** + String get broker; + + /// No description provided for @selectUserType. + /// + /// In en, this message translates to: + /// **'Select user type'** + String get selectUserType; + + /// No description provided for @points. + /// + /// In en, this message translates to: + /// **'Points'** + String get points; + + /// No description provided for @currentPoints. + /// + /// In en, this message translates to: + /// **'Current Points'** + String get currentPoints; + + /// Points balance display + /// + /// In en, this message translates to: + /// **'{points} points'** + String pointsBalance(int points); + + /// Points earned + /// + /// In en, this message translates to: + /// **'+{points} points'** + String earnedPoints(int points); + + /// Points spent + /// + /// In en, this message translates to: + /// **'-{points} points'** + String spentPoints(int points); + + /// No description provided for @memberTier. + /// + /// In en, this message translates to: + /// **'Member Tier'** + String get memberTier; + + /// No description provided for @diamond. + /// + /// In en, this message translates to: + /// **'Diamond'** + String get diamond; + + /// No description provided for @platinum. + /// + /// In en, this message translates to: + /// **'Platinum'** + String get platinum; + + /// No description provided for @gold. + /// + /// In en, this message translates to: + /// **'Gold'** + String get gold; + + /// Points needed for next tier + /// + /// In en, this message translates to: + /// **'{points} points to reach {tier}'** + String pointsToNextTier(int points, String tier); + + /// No description provided for @rewards. + /// + /// In en, this message translates to: + /// **'Rewards'** + String get rewards; + + /// No description provided for @redeemReward. + /// + /// In en, this message translates to: + /// **'Redeem Reward'** + String get redeemReward; + + /// No description provided for @pointsHistory. + /// + /// In en, this message translates to: + /// **'Points History'** + String get pointsHistory; + + /// No description provided for @myGifts. + /// + /// In en, this message translates to: + /// **'My Gifts'** + String get myGifts; + + /// No description provided for @referral. + /// + /// In en, this message translates to: + /// **'Refer Friends'** + String get referral; + + /// No description provided for @referralCode. + /// + /// In en, this message translates to: + /// **'Referral Code'** + String get referralCode; + + /// No description provided for @referralLink. + /// + /// In en, this message translates to: + /// **'Referral Link'** + String get referralLink; + + /// No description provided for @totalReferrals. + /// + /// In en, this message translates to: + /// **'Total Referrals'** + String get totalReferrals; + + /// No description provided for @shareReferralCode. + /// + /// In en, this message translates to: + /// **'Share Referral Code'** + String get shareReferralCode; + + /// No description provided for @copyReferralCode. + /// + /// In en, this message translates to: + /// **'Copy Code'** + String get copyReferralCode; + + /// No description provided for @copyReferralLink. + /// + /// In en, this message translates to: + /// **'Copy Link'** + String get copyReferralLink; + + /// No description provided for @product. + /// + /// In en, this message translates to: + /// **'Product'** + String get product; + + /// No description provided for @productName. + /// + /// In en, this message translates to: + /// **'Product Name'** + String get productName; + + /// No description provided for @productCode. + /// + /// In en, this message translates to: + /// **'Product Code'** + String get productCode; + + /// No description provided for @price. + /// + /// In en, this message translates to: + /// **'Price'** + String get price; + + /// No description provided for @salePrice. + /// + /// In en, this message translates to: + /// **'Sale Price'** + String get salePrice; + + /// No description provided for @quantity. + /// + /// In en, this message translates to: + /// **'Quantity'** + String get quantity; + + /// No description provided for @stock. + /// + /// In en, this message translates to: + /// **'Stock'** + String get stock; + + /// No description provided for @inStock. + /// + /// In en, this message translates to: + /// **'In Stock'** + String get inStock; + + /// No description provided for @outOfStock. + /// + /// In en, this message translates to: + /// **'Out of Stock'** + String get outOfStock; + + /// No description provided for @category. + /// + /// In en, this message translates to: + /// **'Category'** + String get category; + + /// No description provided for @allCategories. + /// + /// In en, this message translates to: + /// **'All Categories'** + String get allCategories; + + /// No description provided for @addToCart. + /// + /// In en, this message translates to: + /// **'Add to Cart'** + String get addToCart; + + /// No description provided for @cart. + /// + /// In en, this message translates to: + /// **'Cart'** + String get cart; + + /// No description provided for @cartEmpty. + /// + /// In en, this message translates to: + /// **'Cart is empty'** + String get cartEmpty; + + /// Number of items in cart + /// + /// In en, this message translates to: + /// **'{count} items'** + String cartItemsCount(int count); + + /// No description provided for @removeFromCart. + /// + /// In en, this message translates to: + /// **'Remove from Cart'** + String get removeFromCart; + + /// No description provided for @clearCart. + /// + /// In en, this message translates to: + /// **'Clear Cart'** + String get clearCart; + + /// No description provided for @clearCartConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all items from the cart?'** + String get clearCartConfirm; + + /// No description provided for @checkout. + /// + /// In en, this message translates to: + /// **'Checkout'** + String get checkout; + + /// No description provided for @subtotal. + /// + /// In en, this message translates to: + /// **'Subtotal'** + String get subtotal; + + /// No description provided for @discount. + /// + /// In en, this message translates to: + /// **'Discount'** + String get discount; + + /// No description provided for @total. + /// + /// In en, this message translates to: + /// **'Total'** + String get total; + + /// No description provided for @placeOrder. + /// + /// In en, this message translates to: + /// **'Place Order'** + String get placeOrder; + + /// No description provided for @orderPlaced. + /// + /// In en, this message translates to: + /// **'Order Placed'** + String get orderPlaced; + + /// No description provided for @orderSuccess. + /// + /// In en, this message translates to: + /// **'Order Successful'** + String get orderSuccess; + + /// No description provided for @orders. + /// + /// In en, this message translates to: + /// **'Orders'** + String get orders; + + /// No description provided for @myOrders. + /// + /// In en, this message translates to: + /// **'My Orders'** + String get myOrders; + + /// No description provided for @orderNumber. + /// + /// In en, this message translates to: + /// **'Order Number'** + String get orderNumber; + + /// No description provided for @orderDate. + /// + /// In en, this message translates to: + /// **'Order Date'** + String get orderDate; + + /// No description provided for @orderStatus. + /// + /// In en, this message translates to: + /// **'Order Status'** + String get orderStatus; + + /// No description provided for @orderDetails. + /// + /// In en, this message translates to: + /// **'Order Details'** + String get orderDetails; + + /// No description provided for @trackOrder. + /// + /// In en, this message translates to: + /// **'Track Order'** + String get trackOrder; + + /// No description provided for @reorder. + /// + /// In en, this message translates to: + /// **'Reorder'** + String get reorder; + + /// No description provided for @paymentMethod. + /// + /// In en, this message translates to: + /// **'Payment Method'** + String get paymentMethod; + + /// No description provided for @cashOnDelivery. + /// + /// In en, this message translates to: + /// **'Cash on Delivery'** + String get cashOnDelivery; + + /// No description provided for @bankTransfer. + /// + /// In en, this message translates to: + /// **'Bank Transfer'** + String get bankTransfer; + + /// No description provided for @creditCard. + /// + /// In en, this message translates to: + /// **'Credit Card'** + String get creditCard; + + /// No description provided for @eWallet. + /// + /// In en, this message translates to: + /// **'E-Wallet'** + String get eWallet; + + /// No description provided for @deliveryAddress. + /// + /// In en, this message translates to: + /// **'Delivery Address'** + String get deliveryAddress; + + /// No description provided for @estimatedDelivery. + /// + /// In en, this message translates to: + /// **'Estimated Delivery'** + String get estimatedDelivery; + + /// No description provided for @payments. + /// + /// In en, this message translates to: + /// **'Payments'** + String get payments; + + /// No description provided for @paymentId. + /// + /// In en, this message translates to: + /// **'Payment ID'** + String get paymentId; + + /// No description provided for @paymentStatus. + /// + /// In en, this message translates to: + /// **'Payment Status'** + String get paymentStatus; + + /// No description provided for @projects. + /// + /// In en, this message translates to: + /// **'Projects'** + String get projects; + + /// No description provided for @myProjects. + /// + /// In en, this message translates to: + /// **'My Projects'** + String get myProjects; + + /// No description provided for @createProject. + /// + /// In en, this message translates to: + /// **'Create Project'** + String get createProject; + + /// No description provided for @projectName. + /// + /// In en, this message translates to: + /// **'Project Name'** + String get projectName; + + /// No description provided for @projectCode. + /// + /// In en, this message translates to: + /// **'Project Code'** + String get projectCode; + + /// No description provided for @projectType. + /// + /// In en, this message translates to: + /// **'Project Type'** + String get projectType; + + /// No description provided for @residential. + /// + /// In en, this message translates to: + /// **'Residential'** + String get residential; + + /// No description provided for @commercial. + /// + /// In en, this message translates to: + /// **'Commercial'** + String get commercial; + + /// No description provided for @industrial. + /// + /// In en, this message translates to: + /// **'Industrial'** + String get industrial; + + /// No description provided for @client. + /// + /// In en, this message translates to: + /// **'Client'** + String get client; + + /// No description provided for @clientName. + /// + /// In en, this message translates to: + /// **'Client Name'** + String get clientName; + + /// No description provided for @clientPhone. + /// + /// In en, this message translates to: + /// **'Client Phone'** + String get clientPhone; + + /// No description provided for @location. + /// + /// In en, this message translates to: + /// **'Location'** + String get location; + + /// No description provided for @startDate. + /// + /// In en, this message translates to: + /// **'Start Date'** + String get startDate; + + /// No description provided for @endDate. + /// + /// In en, this message translates to: + /// **'End Date'** + String get endDate; + + /// No description provided for @progress. + /// + /// In en, this message translates to: + /// **'Progress'** + String get progress; + + /// No description provided for @budget. + /// + /// In en, this message translates to: + /// **'Budget'** + String get budget; + + /// No description provided for @description. + /// + /// In en, this message translates to: + /// **'Description'** + String get description; + + /// No description provided for @notes. + /// + /// In en, this message translates to: + /// **'Notes'** + String get notes; + + /// No description provided for @quotes. + /// + /// In en, this message translates to: + /// **'Quotes'** + String get quotes; + + /// No description provided for @createQuote. + /// + /// In en, this message translates to: + /// **'Create Quote'** + String get createQuote; + + /// No description provided for @quoteNumber. + /// + /// In en, this message translates to: + /// **'Quote Number'** + String get quoteNumber; + + /// No description provided for @quoteDate. + /// + /// In en, this message translates to: + /// **'Quote Date'** + String get quoteDate; + + /// No description provided for @validity. + /// + /// In en, this message translates to: + /// **'Validity'** + String get validity; + + /// No description provided for @convertToOrder. + /// + /// In en, this message translates to: + /// **'Convert to Order'** + String get convertToOrder; + + /// No description provided for @duplicate. + /// + /// In en, this message translates to: + /// **'Duplicate'** + String get duplicate; + + /// No description provided for @profile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get profile; + + /// No description provided for @editProfile. + /// + /// In en, this message translates to: + /// **'Edit Profile'** + String get editProfile; + + /// No description provided for @avatar. + /// + /// In en, this message translates to: + /// **'Avatar'** + String get avatar; + + /// No description provided for @uploadAvatar. + /// + /// In en, this message translates to: + /// **'Upload Avatar'** + String get uploadAvatar; + + /// No description provided for @changePassword. + /// + /// In en, this message translates to: + /// **'Change Password'** + String get changePassword; + + /// No description provided for @passwordChanged. + /// + /// In en, this message translates to: + /// **'Password changed successfully'** + String get passwordChanged; + + /// No description provided for @addresses. + /// + /// In en, this message translates to: + /// **'Addresses'** + String get addresses; + + /// No description provided for @myAddresses. + /// + /// In en, this message translates to: + /// **'My Addresses'** + String get myAddresses; + + /// No description provided for @addAddress. + /// + /// In en, this message translates to: + /// **'Add Address'** + String get addAddress; + + /// No description provided for @editAddress. + /// + /// In en, this message translates to: + /// **'Edit Address'** + String get editAddress; + + /// No description provided for @deleteAddress. + /// + /// In en, this message translates to: + /// **'Delete Address'** + String get deleteAddress; + + /// No description provided for @deleteAddressConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this address?'** + String get deleteAddressConfirm; + + /// No description provided for @setAsDefault. + /// + /// In en, this message translates to: + /// **'Set as Default'** + String get setAsDefault; + + /// No description provided for @defaultAddress. + /// + /// In en, this message translates to: + /// **'Default Address'** + String get defaultAddress; + + /// No description provided for @homeAddress. + /// + /// In en, this message translates to: + /// **'Home'** + String get homeAddress; + + /// No description provided for @officeAddress. + /// + /// In en, this message translates to: + /// **'Office'** + String get officeAddress; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @notifications. + /// + /// In en, this message translates to: + /// **'Notifications'** + String get notifications; + + /// No description provided for @notificationSettings. + /// + /// In en, this message translates to: + /// **'Notification Settings'** + String get notificationSettings; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @theme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get theme; + + /// No description provided for @lightMode. + /// + /// In en, this message translates to: + /// **'Light'** + String get lightMode; + + /// No description provided for @darkMode. + /// + /// In en, this message translates to: + /// **'Dark'** + String get darkMode; + + /// No description provided for @systemMode. + /// + /// In en, this message translates to: + /// **'System'** + String get systemMode; + + /// No description provided for @promotions. + /// + /// In en, this message translates to: + /// **'Promotions'** + String get promotions; + + /// No description provided for @promotion. + /// + /// In en, this message translates to: + /// **'Promotion'** + String get promotion; + + /// No description provided for @activePromotions. + /// + /// In en, this message translates to: + /// **'Active Promotions'** + String get activePromotions; + + /// No description provided for @upcomingPromotions. + /// + /// In en, this message translates to: + /// **'Upcoming Promotions'** + String get upcomingPromotions; + + /// No description provided for @expiredPromotions. + /// + /// In en, this message translates to: + /// **'Expired Promotions'** + String get expiredPromotions; + + /// No description provided for @claimPromotion. + /// + /// In en, this message translates to: + /// **'Claim Promotion'** + String get claimPromotion; + + /// No description provided for @termsAndConditions. + /// + /// In en, this message translates to: + /// **'Terms & Conditions'** + String get termsAndConditions; + + /// No description provided for @chat. + /// + /// In en, this message translates to: + /// **'Chat'** + String get chat; + + /// No description provided for @chatSupport. + /// + /// In en, this message translates to: + /// **'Chat Support'** + String get chatSupport; + + /// No description provided for @sendMessage. + /// + /// In en, this message translates to: + /// **'Send Message'** + String get sendMessage; + + /// No description provided for @typeMessage. + /// + /// In en, this message translates to: + /// **'Type a message...'** + String get typeMessage; + + /// No description provided for @typingIndicator. + /// + /// In en, this message translates to: + /// **'typing...'** + String get typingIndicator; + + /// No description provided for @attachFile. + /// + /// In en, this message translates to: + /// **'Attach File'** + String get attachFile; + + /// No description provided for @supportAgent. + /// + /// In en, this message translates to: + /// **'Support Agent'** + String get supportAgent; + + /// No description provided for @fieldRequired. + /// + /// In en, this message translates to: + /// **'This field is required'** + String get fieldRequired; + + /// No description provided for @invalidPhone. + /// + /// In en, this message translates to: + /// **'Invalid phone number'** + String get invalidPhone; + + /// No description provided for @invalidEmail. + /// + /// In en, this message translates to: + /// **'Invalid email'** + String get invalidEmail; + + /// No description provided for @invalidOTP. + /// + /// In en, this message translates to: + /// **'Invalid OTP code'** + String get invalidOTP; + + /// No description provided for @passwordTooShort. + /// + /// In en, this message translates to: + /// **'Password must be at least 8 characters'** + String get passwordTooShort; + + /// No description provided for @passwordsNotMatch. + /// + /// In en, this message translates to: + /// **'Passwords do not match'** + String get passwordsNotMatch; + + /// No description provided for @passwordRequirements. + /// + /// In en, this message translates to: + /// **'Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters'** + String get passwordRequirements; + + /// No description provided for @invalidAmount. + /// + /// In en, this message translates to: + /// **'Invalid amount'** + String get invalidAmount; + + /// No description provided for @insufficientPoints. + /// + /// In en, this message translates to: + /// **'Insufficient points to redeem'** + String get insufficientPoints; + + /// No description provided for @error. + /// + /// In en, this message translates to: + /// **'Error'** + String get error; + + /// No description provided for @errorOccurred. + /// + /// In en, this message translates to: + /// **'An error occurred'** + String get errorOccurred; + + /// No description provided for @networkError. + /// + /// In en, this message translates to: + /// **'Network error. Please check your internet connection.'** + String get networkError; + + /// No description provided for @serverError. + /// + /// In en, this message translates to: + /// **'Server error. Please try again later.'** + String get serverError; + + /// No description provided for @sessionExpired. + /// + /// In en, this message translates to: + /// **'Session expired. Please login again.'** + String get sessionExpired; + + /// No description provided for @notFound. + /// + /// In en, this message translates to: + /// **'Not found'** + String get notFound; + + /// No description provided for @unauthorized. + /// + /// In en, this message translates to: + /// **'Unauthorized access'** + String get unauthorized; + + /// No description provided for @tryAgain. + /// + /// In en, this message translates to: + /// **'Try Again'** + String get tryAgain; + + /// No description provided for @contactSupport. + /// + /// In en, this message translates to: + /// **'Contact Support'** + String get contactSupport; + + /// No description provided for @success. + /// + /// In en, this message translates to: + /// **'Success'** + String get success; + + /// No description provided for @savedSuccessfully. + /// + /// In en, this message translates to: + /// **'Saved successfully'** + String get savedSuccessfully; + + /// No description provided for @updatedSuccessfully. + /// + /// In en, this message translates to: + /// **'Updated successfully'** + String get updatedSuccessfully; + + /// No description provided for @deletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Deleted successfully'** + String get deletedSuccessfully; + + /// No description provided for @sentSuccessfully. + /// + /// In en, this message translates to: + /// **'Sent successfully'** + String get sentSuccessfully; + + /// No description provided for @redeemSuccessful. + /// + /// In en, this message translates to: + /// **'Reward redeemed successfully'** + String get redeemSuccessful; + + /// No description provided for @giftCode. + /// + /// In en, this message translates to: + /// **'Gift Code'** + String get giftCode; + + /// No description provided for @loading. + /// + /// In en, this message translates to: + /// **'Loading...'** + String get loading; + + /// No description provided for @loadingData. + /// + /// In en, this message translates to: + /// **'Loading data...'** + String get loadingData; + + /// No description provided for @pleaseWait. + /// + /// In en, this message translates to: + /// **'Please wait...'** + String get pleaseWait; + + /// No description provided for @noData. + /// + /// In en, this message translates to: + /// **'No data'** + String get noData; + + /// No description provided for @noResults. + /// + /// In en, this message translates to: + /// **'No results'** + String get noResults; + + /// No description provided for @noProductsFound. + /// + /// In en, this message translates to: + /// **'No products found'** + String get noProductsFound; + + /// No description provided for @noOrdersYet. + /// + /// In en, this message translates to: + /// **'No orders yet'** + String get noOrdersYet; + + /// No description provided for @noProjectsYet. + /// + /// In en, this message translates to: + /// **'No projects yet'** + String get noProjectsYet; + + /// No description provided for @noNotifications. + /// + /// In en, this message translates to: + /// **'No notifications'** + String get noNotifications; + + /// No description provided for @noGiftsYet. + /// + /// In en, this message translates to: + /// **'No gifts yet'** + String get noGiftsYet; + + /// No description provided for @startShopping. + /// + /// In en, this message translates to: + /// **'Start Shopping'** + String get startShopping; + + /// No description provided for @createFirstProject. + /// + /// In en, this message translates to: + /// **'Create Your First Project'** + String get createFirstProject; + + /// No description provided for @today. + /// + /// In en, this message translates to: + /// **'Today'** + String get today; + + /// No description provided for @yesterday. + /// + /// In en, this message translates to: + /// **'Yesterday'** + String get yesterday; + + /// No description provided for @thisWeek. + /// + /// In en, this message translates to: + /// **'This Week'** + String get thisWeek; + + /// No description provided for @thisMonth. + /// + /// In en, this message translates to: + /// **'This Month'** + String get thisMonth; + + /// No description provided for @all. + /// + /// In en, this message translates to: + /// **'All'** + String get all; + + /// No description provided for @dateRange. + /// + /// In en, this message translates to: + /// **'Date Range'** + String get dateRange; + + /// No description provided for @from. + /// + /// In en, this message translates to: + /// **'From'** + String get from; + + /// No description provided for @to. + /// + /// In en, this message translates to: + /// **'To'** + String get to; + + /// No description provided for @date. + /// + /// In en, this message translates to: + /// **'Date'** + String get date; + + /// No description provided for @time. + /// + /// In en, this message translates to: + /// **'Time'** + String get time; + + /// No description provided for @version. + /// + /// In en, this message translates to: + /// **'Version'** + String get version; + + /// No description provided for @appVersion. + /// + /// In en, this message translates to: + /// **'App Version'** + String get appVersion; + + /// No description provided for @help. + /// + /// In en, this message translates to: + /// **'Help'** + String get help; + + /// No description provided for @helpCenter. + /// + /// In en, this message translates to: + /// **'Help Center'** + String get helpCenter; + + /// No description provided for @aboutUs. + /// + /// In en, this message translates to: + /// **'About Us'** + String get aboutUs; + + /// No description provided for @privacyPolicy. + /// + /// In en, this message translates to: + /// **'Privacy Policy'** + String get privacyPolicy; + + /// No description provided for @termsOfService. + /// + /// In en, this message translates to: + /// **'Terms of Service'** + String get termsOfService; + + /// No description provided for @rateApp. + /// + /// In en, this message translates to: + /// **'Rate App'** + String get rateApp; + + /// No description provided for @feedback. + /// + /// In en, this message translates to: + /// **'Feedback'** + String get feedback; + + /// No description provided for @sendFeedback. + /// + /// In en, this message translates to: + /// **'Send Feedback'** + String get sendFeedback; + + /// No description provided for @unsavedChanges. + /// + /// In en, this message translates to: + /// **'Unsaved Changes'** + String get unsavedChanges; + + /// No description provided for @unsavedChangesMessage. + /// + /// In en, this message translates to: + /// **'Do you want to save changes before leaving?'** + String get unsavedChangesMessage; + + /// No description provided for @welcome. + /// + /// In en, this message translates to: + /// **'Welcome'** + String get welcome; + + /// No description provided for @welcomeBack. + /// + /// In en, this message translates to: + /// **'Welcome Back'** + String get welcomeBack; + + /// Welcome message with app name + /// + /// In en, this message translates to: + /// **'Welcome to {appName}'** + String welcomeTo(String appName); + + /// Number of items in cart with pluralization + /// + /// In en, this message translates to: + /// **'{count, plural, =0{No items} =1{1 item} other{{count} items}}'** + String itemsInCart(int count); + + /// Number of orders with pluralization + /// + /// In en, this message translates to: + /// **'{count, plural, =0{No orders} =1{1 order} other{{count} orders}}'** + String ordersCount(int count); + + /// Number of projects with pluralization + /// + /// In en, this message translates to: + /// **'{count, plural, =0{No projects} =1{1 project} other{{count} projects}}'** + String projectsCount(int count); + + /// Days remaining with pluralization + /// + /// In en, this message translates to: + /// **'{count, plural, =0{Today} =1{1 day left} other{{count} days left}}'** + String daysRemaining(int count); + + /// Format currency in Vietnamese Dong + /// + /// In en, this message translates to: + /// **'{amount} ₫'** + String formatCurrency(String amount); + + /// Date format MM/DD/YYYY + /// + /// In en, this message translates to: + /// **'{month}/{day}/{year}'** + String formatDate(String day, String month, String year); + + /// DateTime format MM/DD/YYYY at HH:mm + /// + /// In en, this message translates to: + /// **'{month}/{day}/{year} at {hour}:{minute}'** + String formatDateTime( + String day, + String month, + String year, + String hour, + String minute, + ); + + /// Member since date + /// + /// In en, this message translates to: + /// **'Member since {date}'** + String memberSince(String date); + + /// Valid until date + /// + /// In en, this message translates to: + /// **'Valid until {date}'** + String validUntil(String date); + + /// No description provided for @used. + /// + /// In en, this message translates to: + /// **'Used'** + String get used; + + /// No description provided for @unused. + /// + /// In en, this message translates to: + /// **'Unused'** + String get unused; + + /// No description provided for @available. + /// + /// In en, this message translates to: + /// **'Available'** + String get available; + + /// No description provided for @unavailable. + /// + /// In en, this message translates to: + /// **'Unavailable'** + String get unavailable; + + /// No description provided for @validFrom. + /// + /// In en, this message translates to: + /// **'Valid from'** + String get validFrom; + + /// No description provided for @validTo. + /// + /// In en, this message translates to: + /// **'Valid to'** + String get validTo; + + /// No description provided for @usageInstructions. + /// + /// In en, this message translates to: + /// **'Usage Instructions'** + String get usageInstructions; + + /// No description provided for @useNow. + /// + /// In en, this message translates to: + /// **'Use Now'** + String get useNow; + + /// No description provided for @scanQRCode. + /// + /// In en, this message translates to: + /// **'Scan QR Code'** + String get scanQRCode; + + /// No description provided for @scanBarcode. + /// + /// In en, this message translates to: + /// **'Scan Barcode'** + String get scanBarcode; + + /// No description provided for @qrCodeScanner. + /// + /// In en, this message translates to: + /// **'QR Code Scanner'** + String get qrCodeScanner; + + /// No description provided for @memberId. + /// + /// In en, this message translates to: + /// **'Member ID'** + String get memberId; + + /// No description provided for @showQRCode. + /// + /// In en, this message translates to: + /// **'Show QR Code'** + String get showQRCode; + + /// No description provided for @tier. + /// + /// In en, this message translates to: + /// **'Tier'** + String get tier; + + /// No description provided for @tierBenefits. + /// + /// In en, this message translates to: + /// **'Tier Benefits'** + String get tierBenefits; + + /// No description provided for @pointsMultiplier. + /// + /// In en, this message translates to: + /// **'Points Multiplier'** + String get pointsMultiplier; + + /// Points multiplier display + /// + /// In en, this message translates to: + /// **'x{multiplier}'** + String multiplierX(String multiplier); + + /// No description provided for @specialOffers. + /// + /// In en, this message translates to: + /// **'Special Offers'** + String get specialOffers; + + /// No description provided for @exclusiveDiscounts. + /// + /// In en, this message translates to: + /// **'Exclusive Discounts'** + String get exclusiveDiscounts; + + /// No description provided for @prioritySupport. + /// + /// In en, this message translates to: + /// **'Priority Support'** + String get prioritySupport; + + /// No description provided for @earlyAccess. + /// + /// In en, this message translates to: + /// **'Early Access'** + String get earlyAccess; + + /// No description provided for @birthdayGift. + /// + /// In en, this message translates to: + /// **'Birthday Gift'** + String get birthdayGift; + + /// No description provided for @transactionType. + /// + /// In en, this message translates to: + /// **'Transaction Type'** + String get transactionType; + + /// No description provided for @earnPoints. + /// + /// In en, this message translates to: + /// **'Earn Points'** + String get earnPoints; + + /// No description provided for @redeemPoints. + /// + /// In en, this message translates to: + /// **'Redeem Points'** + String get redeemPoints; + + /// No description provided for @bonusPoints. + /// + /// In en, this message translates to: + /// **'Bonus Points'** + String get bonusPoints; + + /// No description provided for @refundPoints. + /// + /// In en, this message translates to: + /// **'Refund Points'** + String get refundPoints; + + /// No description provided for @expiredPoints. + /// + /// In en, this message translates to: + /// **'Expired Points'** + String get expiredPoints; + + /// No description provided for @transferPoints. + /// + /// In en, this message translates to: + /// **'Transfer Points'** + String get transferPoints; + + /// No description provided for @pointsExpiry. + /// + /// In en, this message translates to: + /// **'Points Expiry'** + String get pointsExpiry; + + /// Points expiration date + /// + /// In en, this message translates to: + /// **'Points will expire on {date}'** + String pointsWillExpireOn(String date); + + /// Points expiring soon warning + /// + /// In en, this message translates to: + /// **'{points} points expiring soon'** + String pointsExpiringSoon(int points); + + /// No description provided for @newBalance. + /// + /// In en, this message translates to: + /// **'New Balance'** + String get newBalance; + + /// No description provided for @previousBalance. + /// + /// In en, this message translates to: + /// **'Previous Balance'** + String get previousBalance; + + /// No description provided for @balanceAfter. + /// + /// In en, this message translates to: + /// **'Balance After Transaction'** + String get balanceAfter; + + /// No description provided for @disputeTransaction. + /// + /// In en, this message translates to: + /// **'Dispute Transaction'** + String get disputeTransaction; + + /// No description provided for @disputeReason. + /// + /// In en, this message translates to: + /// **'Dispute Reason'** + String get disputeReason; + + /// No description provided for @disputeSubmitted. + /// + /// In en, this message translates to: + /// **'Dispute Submitted'** + String get disputeSubmitted; + + /// No description provided for @rewardCategory. + /// + /// In en, this message translates to: + /// **'Reward Category'** + String get rewardCategory; + + /// No description provided for @vouchers. + /// + /// In en, this message translates to: + /// **'Vouchers'** + String get vouchers; + + /// No description provided for @productRewards. + /// + /// In en, this message translates to: + /// **'Product Rewards'** + String get productRewards; + + /// No description provided for @services. + /// + /// In en, this message translates to: + /// **'Services'** + String get services; + + /// No description provided for @experiences. + /// + /// In en, this message translates to: + /// **'Experiences'** + String get experiences; + + /// No description provided for @pointsCost. + /// + /// In en, this message translates to: + /// **'Points Cost'** + String get pointsCost; + + /// Points required for reward + /// + /// In en, this message translates to: + /// **'Requires {points} points'** + String pointsRequired(int points); + + /// No description provided for @expiryDate. + /// + /// In en, this message translates to: + /// **'Expiry Date'** + String get expiryDate; + + /// Expiration date + /// + /// In en, this message translates to: + /// **'Expires on {date}'** + String expiresOn(String date); + + /// No description provided for @redeemConfirm. + /// + /// In en, this message translates to: + /// **'Confirm Redemption'** + String get redeemConfirm; + + /// Redeem confirmation message + /// + /// In en, this message translates to: + /// **'Are you sure you want to redeem {points} points for {reward}?'** + String redeemConfirmMessage(int points, String reward); + + /// No description provided for @giftStatus. + /// + /// In en, this message translates to: + /// **'Gift Status'** + String get giftStatus; + + /// No description provided for @activeGifts. + /// + /// In en, this message translates to: + /// **'Active Gifts'** + String get activeGifts; + + /// No description provided for @usedGifts. + /// + /// In en, this message translates to: + /// **'Used Gifts'** + String get usedGifts; + + /// No description provided for @expiredGifts. + /// + /// In en, this message translates to: + /// **'Expired Gifts'** + String get expiredGifts; + + /// No description provided for @giftDetails. + /// + /// In en, this message translates to: + /// **'Gift Details'** + String get giftDetails; + + /// No description provided for @howToUse. + /// + /// In en, this message translates to: + /// **'How to Use'** + String get howToUse; + + /// No description provided for @referralInvite. + /// + /// In en, this message translates to: + /// **'Invite Friends'** + String get referralInvite; + + /// No description provided for @referralReward. + /// + /// In en, this message translates to: + /// **'Referral Reward'** + String get referralReward; + + /// No description provided for @referralSuccess. + /// + /// In en, this message translates to: + /// **'Referral Successful'** + String get referralSuccess; + + /// No description provided for @friendsReferred. + /// + /// In en, this message translates to: + /// **'Friends Referred'** + String get friendsReferred; + + /// No description provided for @pointsEarned. + /// + /// In en, this message translates to: + /// **'Points Earned'** + String get pointsEarned; + + /// No description provided for @referralSteps. + /// + /// In en, this message translates to: + /// **'How It Works'** + String get referralSteps; + + /// No description provided for @step1. + /// + /// In en, this message translates to: + /// **'Step 1'** + String get step1; + + /// No description provided for @step2. + /// + /// In en, this message translates to: + /// **'Step 2'** + String get step2; + + /// No description provided for @step3. + /// + /// In en, this message translates to: + /// **'Step 3'** + String get step3; + + /// No description provided for @shareYourCode. + /// + /// In en, this message translates to: + /// **'Share Your Code'** + String get shareYourCode; + + /// No description provided for @friendRegisters. + /// + /// In en, this message translates to: + /// **'Friend Registers'** + String get friendRegisters; + + /// No description provided for @bothGetRewards. + /// + /// In en, this message translates to: + /// **'Both Get Rewards'** + String get bothGetRewards; + + /// No description provided for @inviteFriends. + /// + /// In en, this message translates to: + /// **'Invite Friends'** + String get inviteFriends; + + /// No description provided for @sku. + /// + /// In en, this message translates to: + /// **'SKU'** + String get sku; + + /// No description provided for @brand. + /// + /// In en, this message translates to: + /// **'Brand'** + String get brand; + + /// No description provided for @model. + /// + /// In en, this message translates to: + /// **'Model'** + String get model; + + /// No description provided for @specification. + /// + /// In en, this message translates to: + /// **'Specification'** + String get specification; + + /// No description provided for @specifications. + /// + /// In en, this message translates to: + /// **'Specifications'** + String get specifications; + + /// No description provided for @material. + /// + /// In en, this message translates to: + /// **'Material'** + String get material; + + /// No description provided for @size. + /// + /// In en, this message translates to: + /// **'Size'** + String get size; + + /// No description provided for @color. + /// + /// In en, this message translates to: + /// **'Color'** + String get color; + + /// No description provided for @weight. + /// + /// In en, this message translates to: + /// **'Weight'** + String get weight; + + /// No description provided for @dimensions. + /// + /// In en, this message translates to: + /// **'Dimensions'** + String get dimensions; + + /// No description provided for @availability. + /// + /// In en, this message translates to: + /// **'Availability'** + String get availability; + + /// No description provided for @addedToCart. + /// + /// In en, this message translates to: + /// **'Added to Cart'** + String get addedToCart; + + /// No description provided for @productDetails. + /// + /// In en, this message translates to: + /// **'Product Details'** + String get productDetails; + + /// No description provided for @relatedProducts. + /// + /// In en, this message translates to: + /// **'Related Products'** + String get relatedProducts; + + /// No description provided for @recommended. + /// + /// In en, this message translates to: + /// **'Recommended'** + String get recommended; + + /// No description provided for @newArrival. + /// + /// In en, this message translates to: + /// **'New Arrival'** + String get newArrival; + + /// No description provided for @bestSeller. + /// + /// In en, this message translates to: + /// **'Best Seller'** + String get bestSeller; + + /// No description provided for @onSale. + /// + /// In en, this message translates to: + /// **'On Sale'** + String get onSale; + + /// No description provided for @limitedStock. + /// + /// In en, this message translates to: + /// **'Limited Stock'** + String get limitedStock; + + /// No description provided for @lowStock. + /// + /// In en, this message translates to: + /// **'Low Stock'** + String get lowStock; + + /// No description provided for @updateQuantity. + /// + /// In en, this message translates to: + /// **'Update Quantity'** + String get updateQuantity; + + /// No description provided for @itemRemoved. + /// + /// In en, this message translates to: + /// **'Item Removed'** + String get itemRemoved; + + /// No description provided for @cartUpdated. + /// + /// In en, this message translates to: + /// **'Cart Updated'** + String get cartUpdated; + + /// No description provided for @proceedToCheckout. + /// + /// In en, this message translates to: + /// **'Proceed to Checkout'** + String get proceedToCheckout; + + /// No description provided for @continueShopping. + /// + /// In en, this message translates to: + /// **'Continue Shopping'** + String get continueShopping; + + /// No description provided for @emptyCart. + /// + /// In en, this message translates to: + /// **'Empty Cart'** + String get emptyCart; + + /// No description provided for @emptyCartMessage. + /// + /// In en, this message translates to: + /// **'You don\'t have any items in your cart'** + String get emptyCartMessage; + + /// No description provided for @selectAddress. + /// + /// In en, this message translates to: + /// **'Select Address'** + String get selectAddress; + + /// No description provided for @selectPaymentMethod. + /// + /// In en, this message translates to: + /// **'Select Payment Method'** + String get selectPaymentMethod; + + /// No description provided for @orderSummary. + /// + /// In en, this message translates to: + /// **'Order Summary'** + String get orderSummary; + + /// No description provided for @orderConfirmation. + /// + /// In en, this message translates to: + /// **'Order Confirmation'** + String get orderConfirmation; + + /// No description provided for @orderSuccessMessage. + /// + /// In en, this message translates to: + /// **'Your order has been placed successfully!'** + String get orderSuccessMessage; + + /// Order number display + /// + /// In en, this message translates to: + /// **'Order Number: {orderNumber}'** + String orderNumberIs(String orderNumber); + + /// Estimated delivery date + /// + /// In en, this message translates to: + /// **'Estimated Delivery: {date}'** + String estimatedDeliveryDate(String date); + + /// No description provided for @viewOrder. + /// + /// In en, this message translates to: + /// **'View Order'** + String get viewOrder; + + /// No description provided for @backToHome. + /// + /// In en, this message translates to: + /// **'Back to Home'** + String get backToHome; + + /// No description provided for @allOrders. + /// + /// In en, this message translates to: + /// **'All Orders'** + String get allOrders; + + /// No description provided for @pendingOrders. + /// + /// In en, this message translates to: + /// **'Pending'** + String get pendingOrders; + + /// No description provided for @processingOrders. + /// + /// In en, this message translates to: + /// **'Processing'** + String get processingOrders; + + /// No description provided for @shippingOrders. + /// + /// In en, this message translates to: + /// **'Shipping'** + String get shippingOrders; + + /// No description provided for @completedOrders. + /// + /// In en, this message translates to: + /// **'Completed'** + String get completedOrders; + + /// No description provided for @cancelledOrders. + /// + /// In en, this message translates to: + /// **'Cancelled'** + String get cancelledOrders; + + /// No description provided for @cancelOrder. + /// + /// In en, this message translates to: + /// **'Cancel Order'** + String get cancelOrder; + + /// No description provided for @cancelOrderConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to cancel this order?'** + String get cancelOrderConfirm; + + /// No description provided for @cancelReason. + /// + /// In en, this message translates to: + /// **'Cancellation Reason'** + String get cancelReason; + + /// No description provided for @orderCancelled. + /// + /// In en, this message translates to: + /// **'Order Cancelled'** + String get orderCancelled; + + /// No description provided for @orderTimeline. + /// + /// In en, this message translates to: + /// **'Order Timeline'** + String get orderTimeline; + + /// No description provided for @orderPlacedAt. + /// + /// In en, this message translates to: + /// **'Order placed at'** + String get orderPlacedAt; + + /// No description provided for @orderProcessedAt. + /// + /// In en, this message translates to: + /// **'Order processed at'** + String get orderProcessedAt; + + /// No description provided for @orderShippedAt. + /// + /// In en, this message translates to: + /// **'Order shipped at'** + String get orderShippedAt; + + /// No description provided for @orderDeliveredAt. + /// + /// In en, this message translates to: + /// **'Order delivered at'** + String get orderDeliveredAt; + + /// No description provided for @trackingNumber. + /// + /// In en, this message translates to: + /// **'Tracking Number'** + String get trackingNumber; + + /// No description provided for @shippingCarrier. + /// + /// In en, this message translates to: + /// **'Shipping Carrier'** + String get shippingCarrier; + + /// No description provided for @allProjects. + /// + /// In en, this message translates to: + /// **'All Projects'** + String get allProjects; + + /// No description provided for @planningProjects. + /// + /// In en, this message translates to: + /// **'Planning'** + String get planningProjects; + + /// No description provided for @inProgressProjects. + /// + /// In en, this message translates to: + /// **'In Progress'** + String get inProgressProjects; + + /// No description provided for @completedProjects. + /// + /// In en, this message translates to: + /// **'Completed'** + String get completedProjects; + + /// No description provided for @projectDetails. + /// + /// In en, this message translates to: + /// **'Project Details'** + String get projectDetails; + + /// No description provided for @projectStatus. + /// + /// In en, this message translates to: + /// **'Project Status'** + String get projectStatus; + + /// No description provided for @updateProgress. + /// + /// In en, this message translates to: + /// **'Update Progress'** + String get updateProgress; + + /// No description provided for @progressUpdated. + /// + /// In en, this message translates to: + /// **'Progress Updated'** + String get progressUpdated; + + /// No description provided for @projectCompleted. + /// + /// In en, this message translates to: + /// **'Project Completed'** + String get projectCompleted; + + /// No description provided for @completeProject. + /// + /// In en, this message translates to: + /// **'Complete Project'** + String get completeProject; + + /// No description provided for @completeProjectConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to mark this project as completed?'** + String get completeProjectConfirm; + + /// No description provided for @deleteProject. + /// + /// In en, this message translates to: + /// **'Delete Project'** + String get deleteProject; + + /// No description provided for @deleteProjectConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this project?'** + String get deleteProjectConfirm; + + /// No description provided for @projectPhotos. + /// + /// In en, this message translates to: + /// **'Project Photos'** + String get projectPhotos; + + /// No description provided for @addPhotos. + /// + /// In en, this message translates to: + /// **'Add Photos'** + String get addPhotos; + + /// No description provided for @projectDocuments. + /// + /// In en, this message translates to: + /// **'Project Documents'** + String get projectDocuments; + + /// No description provided for @uploadDocument. + /// + /// In en, this message translates to: + /// **'Upload Document'** + String get uploadDocument; + + /// No description provided for @allQuotes. + /// + /// In en, this message translates to: + /// **'All Quotes'** + String get allQuotes; + + /// No description provided for @draftQuotes. + /// + /// In en, this message translates to: + /// **'Drafts'** + String get draftQuotes; + + /// No description provided for @sentQuotes. + /// + /// In en, this message translates to: + /// **'Sent'** + String get sentQuotes; + + /// No description provided for @acceptedQuotes. + /// + /// In en, this message translates to: + /// **'Accepted'** + String get acceptedQuotes; + + /// No description provided for @rejectedQuotes. + /// + /// In en, this message translates to: + /// **'Rejected'** + String get rejectedQuotes; + + /// No description provided for @expiredQuotes. + /// + /// In en, this message translates to: + /// **'Expired'** + String get expiredQuotes; + + /// No description provided for @quoteDetails. + /// + /// In en, this message translates to: + /// **'Quote Details'** + String get quoteDetails; + + /// No description provided for @sendQuote. + /// + /// In en, this message translates to: + /// **'Send Quote'** + String get sendQuote; + + /// No description provided for @sendQuoteConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to send this quote to the client?'** + String get sendQuoteConfirm; + + /// No description provided for @quoteSent. + /// + /// In en, this message translates to: + /// **'Quote Sent'** + String get quoteSent; + + /// No description provided for @acceptQuote. + /// + /// In en, this message translates to: + /// **'Accept Quote'** + String get acceptQuote; + + /// No description provided for @rejectQuote. + /// + /// In en, this message translates to: + /// **'Reject Quote'** + String get rejectQuote; + + /// No description provided for @deleteQuote. + /// + /// In en, this message translates to: + /// **'Delete Quote'** + String get deleteQuote; + + /// No description provided for @deleteQuoteConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this quote?'** + String get deleteQuoteConfirm; + + /// No description provided for @quoteItems. + /// + /// In en, this message translates to: + /// **'Quote Items'** + String get quoteItems; + + /// No description provided for @addItem. + /// + /// In en, this message translates to: + /// **'Add Item'** + String get addItem; + + /// No description provided for @editItem. + /// + /// In en, this message translates to: + /// **'Edit Item'** + String get editItem; + + /// No description provided for @removeItem. + /// + /// In en, this message translates to: + /// **'Remove Item'** + String get removeItem; + + /// No description provided for @recipient. + /// + /// In en, this message translates to: + /// **'Recipient'** + String get recipient; + + /// No description provided for @recipientName. + /// + /// In en, this message translates to: + /// **'Recipient Name'** + String get recipientName; + + /// No description provided for @recipientPhone. + /// + /// In en, this message translates to: + /// **'Recipient Phone'** + String get recipientPhone; + + /// No description provided for @addressType. + /// + /// In en, this message translates to: + /// **'Address Type'** + String get addressType; + + /// No description provided for @addressLabel. + /// + /// In en, this message translates to: + /// **'Address Label'** + String get addressLabel; + + /// No description provided for @setDefault. + /// + /// In en, this message translates to: + /// **'Set as Default'** + String get setDefault; + + /// No description provided for @defaultLabel. + /// + /// In en, this message translates to: + /// **'Default'** + String get defaultLabel; + + /// No description provided for @addressSaved. + /// + /// In en, this message translates to: + /// **'Address Saved'** + String get addressSaved; + + /// No description provided for @currentPasswordRequired. + /// + /// In en, this message translates to: + /// **'Please enter current password'** + String get currentPasswordRequired; + + /// No description provided for @newPasswordRequired. + /// + /// In en, this message translates to: + /// **'Please enter new password'** + String get newPasswordRequired; + + /// No description provided for @confirmPasswordRequired. + /// + /// In en, this message translates to: + /// **'Please confirm new password'** + String get confirmPasswordRequired; + + /// No description provided for @incorrectPassword. + /// + /// In en, this message translates to: + /// **'Incorrect password'** + String get incorrectPassword; + + /// No description provided for @passwordStrength. + /// + /// In en, this message translates to: + /// **'Password Strength'** + String get passwordStrength; + + /// No description provided for @weak. + /// + /// In en, this message translates to: + /// **'Weak'** + String get weak; + + /// No description provided for @medium. + /// + /// In en, this message translates to: + /// **'Medium'** + String get medium; + + /// No description provided for @strong. + /// + /// In en, this message translates to: + /// **'Strong'** + String get strong; + + /// No description provided for @veryStrong. + /// + /// In en, this message translates to: + /// **'Very Strong'** + String get veryStrong; + + /// No description provided for @passwordRequirement1. + /// + /// In en, this message translates to: + /// **'At least 8 characters'** + String get passwordRequirement1; + + /// No description provided for @passwordRequirement2. + /// + /// In en, this message translates to: + /// **'Include uppercase letter'** + String get passwordRequirement2; + + /// No description provided for @passwordRequirement3. + /// + /// In en, this message translates to: + /// **'Include lowercase letter'** + String get passwordRequirement3; + + /// No description provided for @passwordRequirement4. + /// + /// In en, this message translates to: + /// **'Include number'** + String get passwordRequirement4; + + /// No description provided for @passwordRequirement5. + /// + /// In en, this message translates to: + /// **'Include special character'** + String get passwordRequirement5; + + /// No description provided for @uploadPhoto. + /// + /// In en, this message translates to: + /// **'Upload Photo'** + String get uploadPhoto; + + /// No description provided for @takePhoto. + /// + /// In en, this message translates to: + /// **'Take Photo'** + String get takePhoto; + + /// No description provided for @chooseFromGallery. + /// + /// In en, this message translates to: + /// **'Choose from Gallery'** + String get chooseFromGallery; + + /// No description provided for @removePhoto. + /// + /// In en, this message translates to: + /// **'Remove Photo'** + String get removePhoto; + + /// No description provided for @cropPhoto. + /// + /// In en, this message translates to: + /// **'Crop Photo'** + String get cropPhoto; + + /// No description provided for @photoUploaded. + /// + /// In en, this message translates to: + /// **'Photo Uploaded'** + String get photoUploaded; + + /// No description provided for @enableNotifications. + /// + /// In en, this message translates to: + /// **'Enable Notifications'** + String get enableNotifications; + + /// No description provided for @disableNotifications. + /// + /// In en, this message translates to: + /// **'Disable Notifications'** + String get disableNotifications; + + /// No description provided for @orderNotifications. + /// + /// In en, this message translates to: + /// **'Order Notifications'** + String get orderNotifications; + + /// No description provided for @promotionNotifications. + /// + /// In en, this message translates to: + /// **'Promotion Notifications'** + String get promotionNotifications; + + /// No description provided for @systemNotifications. + /// + /// In en, this message translates to: + /// **'System Notifications'** + String get systemNotifications; + + /// No description provided for @chatNotifications. + /// + /// In en, this message translates to: + /// **'Chat Notifications'** + String get chatNotifications; + + /// No description provided for @pushNotifications. + /// + /// In en, this message translates to: + /// **'Push Notifications'** + String get pushNotifications; + + /// No description provided for @emailNotifications. + /// + /// In en, this message translates to: + /// **'Email Notifications'** + String get emailNotifications; + + /// No description provided for @smsNotifications. + /// + /// In en, this message translates to: + /// **'SMS Notifications'** + String get smsNotifications; + + /// No description provided for @vietnamese. + /// + /// In en, this message translates to: + /// **'Vietnamese'** + String get vietnamese; + + /// No description provided for @english. + /// + /// In en, this message translates to: + /// **'English'** + String get english; + + /// No description provided for @selectLanguage. + /// + /// In en, this message translates to: + /// **'Select Language'** + String get selectLanguage; + + /// No description provided for @languageChanged. + /// + /// In en, this message translates to: + /// **'Language Changed'** + String get languageChanged; + + /// No description provided for @selectTheme. + /// + /// In en, this message translates to: + /// **'Select Theme'** + String get selectTheme; + + /// No description provided for @themeChanged. + /// + /// In en, this message translates to: + /// **'Theme Changed'** + String get themeChanged; + + /// No description provided for @autoTheme. + /// + /// In en, this message translates to: + /// **'Auto'** + String get autoTheme; + + /// No description provided for @allNotifications. + /// + /// In en, this message translates to: + /// **'All'** + String get allNotifications; + + /// No description provided for @orderNotification. + /// + /// In en, this message translates to: + /// **'Orders'** + String get orderNotification; + + /// No description provided for @systemNotification. + /// + /// In en, this message translates to: + /// **'System'** + String get systemNotification; + + /// No description provided for @promotionNotification. + /// + /// In en, this message translates to: + /// **'Promotions'** + String get promotionNotification; + + /// No description provided for @markAsRead. + /// + /// In en, this message translates to: + /// **'Mark as Read'** + String get markAsRead; + + /// No description provided for @markAllAsRead. + /// + /// In en, this message translates to: + /// **'Mark All as Read'** + String get markAllAsRead; + + /// No description provided for @deleteNotification. + /// + /// In en, this message translates to: + /// **'Delete Notification'** + String get deleteNotification; + + /// No description provided for @clearNotifications. + /// + /// In en, this message translates to: + /// **'Clear All Notifications'** + String get clearNotifications; + + /// No description provided for @clearNotificationsConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to clear all notifications?'** + String get clearNotificationsConfirm; + + /// No description provided for @notificationCleared. + /// + /// In en, this message translates to: + /// **'Notification Cleared'** + String get notificationCleared; + + /// Unread notifications count + /// + /// In en, this message translates to: + /// **'{count} unread notifications'** + String unreadNotifications(int count); + + /// No description provided for @online. + /// + /// In en, this message translates to: + /// **'Online'** + String get online; + + /// No description provided for @offline. + /// + /// In en, this message translates to: + /// **'Offline'** + String get offline; + + /// No description provided for @away. + /// + /// In en, this message translates to: + /// **'Away'** + String get away; + + /// No description provided for @busy. + /// + /// In en, this message translates to: + /// **'Busy'** + String get busy; + + /// Last seen timestamp + /// + /// In en, this message translates to: + /// **'Last seen {time}'** + String lastSeenAt(String time); + + /// No description provided for @messageRead. + /// + /// In en, this message translates to: + /// **'Read'** + String get messageRead; + + /// No description provided for @messageDelivered. + /// + /// In en, this message translates to: + /// **'Delivered'** + String get messageDelivered; + + /// No description provided for @messageSent. + /// + /// In en, this message translates to: + /// **'Sent'** + String get messageSent; + + /// No description provided for @messageFailed. + /// + /// In en, this message translates to: + /// **'Failed'** + String get messageFailed; + + /// No description provided for @retryMessage. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retryMessage; + + /// No description provided for @deleteMessage. + /// + /// In en, this message translates to: + /// **'Delete Message'** + String get deleteMessage; + + /// No description provided for @deleteMessageConfirm. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this message?'** + String get deleteMessageConfirm; + + /// No description provided for @messageDeleted. + /// + /// In en, this message translates to: + /// **'Message Deleted'** + String get messageDeleted; + + /// No description provided for @filterBy. + /// + /// In en, this message translates to: + /// **'Filter By'** + String get filterBy; + + /// No description provided for @sortBy. + /// + /// In en, this message translates to: + /// **'Sort By'** + String get sortBy; + + /// No description provided for @priceAscending. + /// + /// In en, this message translates to: + /// **'Price: Low to High'** + String get priceAscending; + + /// No description provided for @priceDescending. + /// + /// In en, this message translates to: + /// **'Price: High to Low'** + String get priceDescending; + + /// No description provided for @nameAscending. + /// + /// In en, this message translates to: + /// **'Name: A-Z'** + String get nameAscending; + + /// No description provided for @nameDescending. + /// + /// In en, this message translates to: + /// **'Name: Z-A'** + String get nameDescending; + + /// No description provided for @dateAscending. + /// + /// In en, this message translates to: + /// **'Oldest First'** + String get dateAscending; + + /// No description provided for @dateDescending. + /// + /// In en, this message translates to: + /// **'Newest First'** + String get dateDescending; + + /// No description provided for @popularityDescending. + /// + /// In en, this message translates to: + /// **'Most Popular'** + String get popularityDescending; + + /// No description provided for @applyFilters. + /// + /// In en, this message translates to: + /// **'Apply Filters'** + String get applyFilters; + + /// No description provided for @clearFilters. + /// + /// In en, this message translates to: + /// **'Clear Filters'** + String get clearFilters; + + /// No description provided for @filterApplied. + /// + /// In en, this message translates to: + /// **'Filter Applied'** + String get filterApplied; + + /// No description provided for @noFilterApplied. + /// + /// In en, this message translates to: + /// **'No Filter Applied'** + String get noFilterApplied; + + /// No description provided for @connectionError. + /// + /// In en, this message translates to: + /// **'Connection Error'** + String get connectionError; + + /// No description provided for @noInternetConnection. + /// + /// In en, this message translates to: + /// **'No Internet Connection'** + String get noInternetConnection; + + /// No description provided for @checkConnection. + /// + /// In en, this message translates to: + /// **'Check Connection'** + String get checkConnection; + + /// No description provided for @retryConnection. + /// + /// In en, this message translates to: + /// **'Retry Connection'** + String get retryConnection; + + /// No description provided for @offlineMode. + /// + /// In en, this message translates to: + /// **'Offline Mode'** + String get offlineMode; + + /// No description provided for @syncData. + /// + /// In en, this message translates to: + /// **'Sync Data'** + String get syncData; + + /// No description provided for @syncInProgress. + /// + /// In en, this message translates to: + /// **'Syncing...'** + String get syncInProgress; + + /// No description provided for @syncCompleted. + /// + /// In en, this message translates to: + /// **'Sync Completed'** + String get syncCompleted; + + /// No description provided for @syncFailed. + /// + /// In en, this message translates to: + /// **'Sync Failed'** + String get syncFailed; + + /// Last sync timestamp + /// + /// In en, this message translates to: + /// **'Last sync: {time}'** + String lastSyncAt(String time); + + /// No description provided for @minutesAgo. + /// + /// In en, this message translates to: + /// **'{minutes} minutes ago'** + String minutesAgo(int minutes); + + /// No description provided for @hoursAgo. + /// + /// In en, this message translates to: + /// **'{hours} hours ago'** + String hoursAgo(int hours); + + /// No description provided for @daysAgo. + /// + /// In en, this message translates to: + /// **'{days} days ago'** + String daysAgo(int days); + + /// No description provided for @weeksAgo. + /// + /// In en, this message translates to: + /// **'{weeks} weeks ago'** + String weeksAgo(int weeks); + + /// No description provided for @monthsAgo. + /// + /// In en, this message translates to: + /// **'{months} months ago'** + String monthsAgo(int months); + + /// No description provided for @yearsAgo. + /// + /// In en, this message translates to: + /// **'{years} years ago'** + String yearsAgo(int years); + + /// No description provided for @justNow. + /// + /// In en, this message translates to: + /// **'Just now'** + String get justNow; + + /// No description provided for @comingSoon. + /// + /// In en, this message translates to: + /// **'Coming Soon'** + String get comingSoon; + + /// No description provided for @underMaintenance. + /// + /// In en, this message translates to: + /// **'Under Maintenance'** + String get underMaintenance; + + /// No description provided for @featureNotAvailable. + /// + /// In en, this message translates to: + /// **'Feature Not Available'** + String get featureNotAvailable; + + /// No description provided for @pageNotFound. + /// + /// In en, this message translates to: + /// **'Page Not Found'** + String get pageNotFound; + + /// No description provided for @goToHomePage. + /// + /// In en, this message translates to: + /// **'Go to Home Page'** + String get goToHomePage; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'vi'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'vi': + return AppLocalizationsVi(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/generated/l10n/app_localizations_en.dart b/lib/generated/l10n/app_localizations_en.dart new file mode 100644 index 0000000..44a8848 --- /dev/null +++ b/lib/generated/l10n/app_localizations_en.dart @@ -0,0 +1,1738 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Worker App'; + + @override + String get home => 'Home'; + + @override + String get products => 'Products'; + + @override + String get loyalty => 'Loyalty'; + + @override + String get account => 'Account'; + + @override + String get more => 'More'; + + @override + String get login => 'Login'; + + @override + String get phone => 'Phone Number'; + + @override + String get enterPhone => 'Enter phone number'; + + @override + String get enterPhoneHint => 'Ex: 0912345678'; + + @override + String get continueButton => 'Continue'; + + @override + String get verifyOTP => 'Verify OTP'; + + @override + String get enterOTP => 'Enter 6-digit OTP code'; + + @override + String otpSentTo(String phone) { + return 'OTP code has been sent to $phone'; + } + + @override + String get resendOTP => 'Resend code'; + + @override + String resendOTPIn(int seconds) { + return 'Resend in ${seconds}s'; + } + + @override + String get register => 'Register'; + + @override + String get registerNewAccount => 'Register new account'; + + @override + String get logout => 'Logout'; + + @override + String get logoutConfirm => 'Are you sure you want to logout?'; + + @override + String get save => 'Save'; + + @override + String get cancel => 'Cancel'; + + @override + String get delete => 'Delete'; + + @override + String get edit => 'Edit'; + + @override + String get search => 'Search'; + + @override + String get filter => 'Filter'; + + @override + String get sort => 'Sort'; + + @override + String get confirm => 'Confirm'; + + @override + String get close => 'Close'; + + @override + String get back => 'Back'; + + @override + String get next => 'Next'; + + @override + String get submit => 'Submit'; + + @override + String get apply => 'Apply'; + + @override + String get clear => 'Clear'; + + @override + String get clearAll => 'Clear All'; + + @override + String get viewDetails => 'View Details'; + + @override + String get viewAll => 'View All'; + + @override + String get refresh => 'Refresh'; + + @override + String get share => 'Share'; + + @override + String get copy => 'Copy'; + + @override + String get copied => 'Copied'; + + @override + String get yes => 'Yes'; + + @override + String get no => 'No'; + + @override + String get pending => 'Pending'; + + @override + String get processing => 'Processing...'; + + @override + String get shipping => 'Shipping'; + + @override + String get completed => 'Completed'; + + @override + String get cancelled => 'Cancelled'; + + @override + String get active => 'Active'; + + @override + String get inactive => 'Inactive'; + + @override + String get expired => 'Expired'; + + @override + String get draft => 'Draft'; + + @override + String get sent => 'Sent'; + + @override + String get accepted => 'Accepted'; + + @override + String get rejected => 'Rejected'; + + @override + String get name => 'Name'; + + @override + String get fullName => 'Full Name'; + + @override + String get email => 'Email'; + + @override + String get password => 'Password'; + + @override + String get currentPassword => 'Current Password'; + + @override + String get newPassword => 'New Password'; + + @override + String get confirmPassword => 'Confirm Password'; + + @override + String get address => 'Address'; + + @override + String get street => 'Street'; + + @override + String get city => 'City'; + + @override + String get district => 'District'; + + @override + String get ward => 'Ward'; + + @override + String get postalCode => 'Postal Code'; + + @override + String get company => 'Company'; + + @override + String get taxId => 'Tax ID'; + + @override + String get dateOfBirth => 'Date of Birth'; + + @override + String get gender => 'Gender'; + + @override + String get male => 'Male'; + + @override + String get female => 'Female'; + + @override + String get other => 'Other'; + + @override + String get contractor => 'Contractor'; + + @override + String get architect => 'Architect'; + + @override + String get distributor => 'Distributor'; + + @override + String get broker => 'Broker'; + + @override + String get selectUserType => 'Select user type'; + + @override + String get points => 'Points'; + + @override + String get currentPoints => 'Current Points'; + + @override + String pointsBalance(int points) { + return '$points points'; + } + + @override + String earnedPoints(int points) { + return '+$points points'; + } + + @override + String spentPoints(int points) { + return '-$points points'; + } + + @override + String get memberTier => 'Member Tier'; + + @override + String get diamond => 'Diamond'; + + @override + String get platinum => 'Platinum'; + + @override + String get gold => 'Gold'; + + @override + String pointsToNextTier(int points, String tier) { + return '$points points to reach $tier'; + } + + @override + String get rewards => 'Rewards'; + + @override + String get redeemReward => 'Redeem Reward'; + + @override + String get pointsHistory => 'Points History'; + + @override + String get myGifts => 'My Gifts'; + + @override + String get referral => 'Refer Friends'; + + @override + String get referralCode => 'Referral Code'; + + @override + String get referralLink => 'Referral Link'; + + @override + String get totalReferrals => 'Total Referrals'; + + @override + String get shareReferralCode => 'Share Referral Code'; + + @override + String get copyReferralCode => 'Copy Code'; + + @override + String get copyReferralLink => 'Copy Link'; + + @override + String get product => 'Product'; + + @override + String get productName => 'Product Name'; + + @override + String get productCode => 'Product Code'; + + @override + String get price => 'Price'; + + @override + String get salePrice => 'Sale Price'; + + @override + String get quantity => 'Quantity'; + + @override + String get stock => 'Stock'; + + @override + String get inStock => 'In Stock'; + + @override + String get outOfStock => 'Out of Stock'; + + @override + String get category => 'Category'; + + @override + String get allCategories => 'All Categories'; + + @override + String get addToCart => 'Add to Cart'; + + @override + String get cart => 'Cart'; + + @override + String get cartEmpty => 'Cart is empty'; + + @override + String cartItemsCount(int count) { + return '$count items'; + } + + @override + String get removeFromCart => 'Remove from Cart'; + + @override + String get clearCart => 'Clear Cart'; + + @override + String get clearCartConfirm => + 'Are you sure you want to clear all items from the cart?'; + + @override + String get checkout => 'Checkout'; + + @override + String get subtotal => 'Subtotal'; + + @override + String get discount => 'Discount'; + + @override + String get total => 'Total'; + + @override + String get placeOrder => 'Place Order'; + + @override + String get orderPlaced => 'Order Placed'; + + @override + String get orderSuccess => 'Order Successful'; + + @override + String get orders => 'Orders'; + + @override + String get myOrders => 'My Orders'; + + @override + String get orderNumber => 'Order Number'; + + @override + String get orderDate => 'Order Date'; + + @override + String get orderStatus => 'Order Status'; + + @override + String get orderDetails => 'Order Details'; + + @override + String get trackOrder => 'Track Order'; + + @override + String get reorder => 'Reorder'; + + @override + String get paymentMethod => 'Payment Method'; + + @override + String get cashOnDelivery => 'Cash on Delivery'; + + @override + String get bankTransfer => 'Bank Transfer'; + + @override + String get creditCard => 'Credit Card'; + + @override + String get eWallet => 'E-Wallet'; + + @override + String get deliveryAddress => 'Delivery Address'; + + @override + String get estimatedDelivery => 'Estimated Delivery'; + + @override + String get payments => 'Payments'; + + @override + String get paymentId => 'Payment ID'; + + @override + String get paymentStatus => 'Payment Status'; + + @override + String get projects => 'Projects'; + + @override + String get myProjects => 'My Projects'; + + @override + String get createProject => 'Create Project'; + + @override + String get projectName => 'Project Name'; + + @override + String get projectCode => 'Project Code'; + + @override + String get projectType => 'Project Type'; + + @override + String get residential => 'Residential'; + + @override + String get commercial => 'Commercial'; + + @override + String get industrial => 'Industrial'; + + @override + String get client => 'Client'; + + @override + String get clientName => 'Client Name'; + + @override + String get clientPhone => 'Client Phone'; + + @override + String get location => 'Location'; + + @override + String get startDate => 'Start Date'; + + @override + String get endDate => 'End Date'; + + @override + String get progress => 'Progress'; + + @override + String get budget => 'Budget'; + + @override + String get description => 'Description'; + + @override + String get notes => 'Notes'; + + @override + String get quotes => 'Quotes'; + + @override + String get createQuote => 'Create Quote'; + + @override + String get quoteNumber => 'Quote Number'; + + @override + String get quoteDate => 'Quote Date'; + + @override + String get validity => 'Validity'; + + @override + String get convertToOrder => 'Convert to Order'; + + @override + String get duplicate => 'Duplicate'; + + @override + String get profile => 'Profile'; + + @override + String get editProfile => 'Edit Profile'; + + @override + String get avatar => 'Avatar'; + + @override + String get uploadAvatar => 'Upload Avatar'; + + @override + String get changePassword => 'Change Password'; + + @override + String get passwordChanged => 'Password changed successfully'; + + @override + String get addresses => 'Addresses'; + + @override + String get myAddresses => 'My Addresses'; + + @override + String get addAddress => 'Add Address'; + + @override + String get editAddress => 'Edit Address'; + + @override + String get deleteAddress => 'Delete Address'; + + @override + String get deleteAddressConfirm => + 'Are you sure you want to delete this address?'; + + @override + String get setAsDefault => 'Set as Default'; + + @override + String get defaultAddress => 'Default Address'; + + @override + String get homeAddress => 'Home'; + + @override + String get officeAddress => 'Office'; + + @override + String get settings => 'Settings'; + + @override + String get notifications => 'Notifications'; + + @override + String get notificationSettings => 'Notification Settings'; + + @override + String get language => 'Language'; + + @override + String get theme => 'Theme'; + + @override + String get lightMode => 'Light'; + + @override + String get darkMode => 'Dark'; + + @override + String get systemMode => 'System'; + + @override + String get promotions => 'Promotions'; + + @override + String get promotion => 'Promotion'; + + @override + String get activePromotions => 'Active Promotions'; + + @override + String get upcomingPromotions => 'Upcoming Promotions'; + + @override + String get expiredPromotions => 'Expired Promotions'; + + @override + String get claimPromotion => 'Claim Promotion'; + + @override + String get termsAndConditions => 'Terms & Conditions'; + + @override + String get chat => 'Chat'; + + @override + String get chatSupport => 'Chat Support'; + + @override + String get sendMessage => 'Send Message'; + + @override + String get typeMessage => 'Type a message...'; + + @override + String get typingIndicator => 'typing...'; + + @override + String get attachFile => 'Attach File'; + + @override + String get supportAgent => 'Support Agent'; + + @override + String get fieldRequired => 'This field is required'; + + @override + String get invalidPhone => 'Invalid phone number'; + + @override + String get invalidEmail => 'Invalid email'; + + @override + String get invalidOTP => 'Invalid OTP code'; + + @override + String get passwordTooShort => 'Password must be at least 8 characters'; + + @override + String get passwordsNotMatch => 'Passwords do not match'; + + @override + String get passwordRequirements => + 'Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters'; + + @override + String get invalidAmount => 'Invalid amount'; + + @override + String get insufficientPoints => 'Insufficient points to redeem'; + + @override + String get error => 'Error'; + + @override + String get errorOccurred => 'An error occurred'; + + @override + String get networkError => + 'Network error. Please check your internet connection.'; + + @override + String get serverError => 'Server error. Please try again later.'; + + @override + String get sessionExpired => 'Session expired. Please login again.'; + + @override + String get notFound => 'Not found'; + + @override + String get unauthorized => 'Unauthorized access'; + + @override + String get tryAgain => 'Try Again'; + + @override + String get contactSupport => 'Contact Support'; + + @override + String get success => 'Success'; + + @override + String get savedSuccessfully => 'Saved successfully'; + + @override + String get updatedSuccessfully => 'Updated successfully'; + + @override + String get deletedSuccessfully => 'Deleted successfully'; + + @override + String get sentSuccessfully => 'Sent successfully'; + + @override + String get redeemSuccessful => 'Reward redeemed successfully'; + + @override + String get giftCode => 'Gift Code'; + + @override + String get loading => 'Loading...'; + + @override + String get loadingData => 'Loading data...'; + + @override + String get pleaseWait => 'Please wait...'; + + @override + String get noData => 'No data'; + + @override + String get noResults => 'No results'; + + @override + String get noProductsFound => 'No products found'; + + @override + String get noOrdersYet => 'No orders yet'; + + @override + String get noProjectsYet => 'No projects yet'; + + @override + String get noNotifications => 'No notifications'; + + @override + String get noGiftsYet => 'No gifts yet'; + + @override + String get startShopping => 'Start Shopping'; + + @override + String get createFirstProject => 'Create Your First Project'; + + @override + String get today => 'Today'; + + @override + String get yesterday => 'Yesterday'; + + @override + String get thisWeek => 'This Week'; + + @override + String get thisMonth => 'This Month'; + + @override + String get all => 'All'; + + @override + String get dateRange => 'Date Range'; + + @override + String get from => 'From'; + + @override + String get to => 'To'; + + @override + String get date => 'Date'; + + @override + String get time => 'Time'; + + @override + String get version => 'Version'; + + @override + String get appVersion => 'App Version'; + + @override + String get help => 'Help'; + + @override + String get helpCenter => 'Help Center'; + + @override + String get aboutUs => 'About Us'; + + @override + String get privacyPolicy => 'Privacy Policy'; + + @override + String get termsOfService => 'Terms of Service'; + + @override + String get rateApp => 'Rate App'; + + @override + String get feedback => 'Feedback'; + + @override + String get sendFeedback => 'Send Feedback'; + + @override + String get unsavedChanges => 'Unsaved Changes'; + + @override + String get unsavedChangesMessage => + 'Do you want to save changes before leaving?'; + + @override + String get welcome => 'Welcome'; + + @override + String get welcomeBack => 'Welcome Back'; + + @override + String welcomeTo(String appName) { + return 'Welcome to $appName'; + } + + @override + String itemsInCart(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + zero: 'No items', + ); + return '$_temp0'; + } + + @override + String ordersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count orders', + one: '1 order', + zero: 'No orders', + ); + return '$_temp0'; + } + + @override + String projectsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count projects', + one: '1 project', + zero: 'No projects', + ); + return '$_temp0'; + } + + @override + String daysRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days left', + one: '1 day left', + zero: 'Today', + ); + return '$_temp0'; + } + + @override + String formatCurrency(String amount) { + return '$amount ₫'; + } + + @override + String formatDate(String day, String month, String year) { + return '$month/$day/$year'; + } + + @override + String formatDateTime( + String day, + String month, + String year, + String hour, + String minute, + ) { + return '$month/$day/$year at $hour:$minute'; + } + + @override + String memberSince(String date) { + return 'Member since $date'; + } + + @override + String validUntil(String date) { + return 'Valid until $date'; + } + + @override + String get used => 'Used'; + + @override + String get unused => 'Unused'; + + @override + String get available => 'Available'; + + @override + String get unavailable => 'Unavailable'; + + @override + String get validFrom => 'Valid from'; + + @override + String get validTo => 'Valid to'; + + @override + String get usageInstructions => 'Usage Instructions'; + + @override + String get useNow => 'Use Now'; + + @override + String get scanQRCode => 'Scan QR Code'; + + @override + String get scanBarcode => 'Scan Barcode'; + + @override + String get qrCodeScanner => 'QR Code Scanner'; + + @override + String get memberId => 'Member ID'; + + @override + String get showQRCode => 'Show QR Code'; + + @override + String get tier => 'Tier'; + + @override + String get tierBenefits => 'Tier Benefits'; + + @override + String get pointsMultiplier => 'Points Multiplier'; + + @override + String multiplierX(String multiplier) { + return 'x$multiplier'; + } + + @override + String get specialOffers => 'Special Offers'; + + @override + String get exclusiveDiscounts => 'Exclusive Discounts'; + + @override + String get prioritySupport => 'Priority Support'; + + @override + String get earlyAccess => 'Early Access'; + + @override + String get birthdayGift => 'Birthday Gift'; + + @override + String get transactionType => 'Transaction Type'; + + @override + String get earnPoints => 'Earn Points'; + + @override + String get redeemPoints => 'Redeem Points'; + + @override + String get bonusPoints => 'Bonus Points'; + + @override + String get refundPoints => 'Refund Points'; + + @override + String get expiredPoints => 'Expired Points'; + + @override + String get transferPoints => 'Transfer Points'; + + @override + String get pointsExpiry => 'Points Expiry'; + + @override + String pointsWillExpireOn(String date) { + return 'Points will expire on $date'; + } + + @override + String pointsExpiringSoon(int points) { + return '$points points expiring soon'; + } + + @override + String get newBalance => 'New Balance'; + + @override + String get previousBalance => 'Previous Balance'; + + @override + String get balanceAfter => 'Balance After Transaction'; + + @override + String get disputeTransaction => 'Dispute Transaction'; + + @override + String get disputeReason => 'Dispute Reason'; + + @override + String get disputeSubmitted => 'Dispute Submitted'; + + @override + String get rewardCategory => 'Reward Category'; + + @override + String get vouchers => 'Vouchers'; + + @override + String get productRewards => 'Product Rewards'; + + @override + String get services => 'Services'; + + @override + String get experiences => 'Experiences'; + + @override + String get pointsCost => 'Points Cost'; + + @override + String pointsRequired(int points) { + return 'Requires $points points'; + } + + @override + String get expiryDate => 'Expiry Date'; + + @override + String expiresOn(String date) { + return 'Expires on $date'; + } + + @override + String get redeemConfirm => 'Confirm Redemption'; + + @override + String redeemConfirmMessage(int points, String reward) { + return 'Are you sure you want to redeem $points points for $reward?'; + } + + @override + String get giftStatus => 'Gift Status'; + + @override + String get activeGifts => 'Active Gifts'; + + @override + String get usedGifts => 'Used Gifts'; + + @override + String get expiredGifts => 'Expired Gifts'; + + @override + String get giftDetails => 'Gift Details'; + + @override + String get howToUse => 'How to Use'; + + @override + String get referralInvite => 'Invite Friends'; + + @override + String get referralReward => 'Referral Reward'; + + @override + String get referralSuccess => 'Referral Successful'; + + @override + String get friendsReferred => 'Friends Referred'; + + @override + String get pointsEarned => 'Points Earned'; + + @override + String get referralSteps => 'How It Works'; + + @override + String get step1 => 'Step 1'; + + @override + String get step2 => 'Step 2'; + + @override + String get step3 => 'Step 3'; + + @override + String get shareYourCode => 'Share Your Code'; + + @override + String get friendRegisters => 'Friend Registers'; + + @override + String get bothGetRewards => 'Both Get Rewards'; + + @override + String get inviteFriends => 'Invite Friends'; + + @override + String get sku => 'SKU'; + + @override + String get brand => 'Brand'; + + @override + String get model => 'Model'; + + @override + String get specification => 'Specification'; + + @override + String get specifications => 'Specifications'; + + @override + String get material => 'Material'; + + @override + String get size => 'Size'; + + @override + String get color => 'Color'; + + @override + String get weight => 'Weight'; + + @override + String get dimensions => 'Dimensions'; + + @override + String get availability => 'Availability'; + + @override + String get addedToCart => 'Added to Cart'; + + @override + String get productDetails => 'Product Details'; + + @override + String get relatedProducts => 'Related Products'; + + @override + String get recommended => 'Recommended'; + + @override + String get newArrival => 'New Arrival'; + + @override + String get bestSeller => 'Best Seller'; + + @override + String get onSale => 'On Sale'; + + @override + String get limitedStock => 'Limited Stock'; + + @override + String get lowStock => 'Low Stock'; + + @override + String get updateQuantity => 'Update Quantity'; + + @override + String get itemRemoved => 'Item Removed'; + + @override + String get cartUpdated => 'Cart Updated'; + + @override + String get proceedToCheckout => 'Proceed to Checkout'; + + @override + String get continueShopping => 'Continue Shopping'; + + @override + String get emptyCart => 'Empty Cart'; + + @override + String get emptyCartMessage => 'You don\'t have any items in your cart'; + + @override + String get selectAddress => 'Select Address'; + + @override + String get selectPaymentMethod => 'Select Payment Method'; + + @override + String get orderSummary => 'Order Summary'; + + @override + String get orderConfirmation => 'Order Confirmation'; + + @override + String get orderSuccessMessage => 'Your order has been placed successfully!'; + + @override + String orderNumberIs(String orderNumber) { + return 'Order Number: $orderNumber'; + } + + @override + String estimatedDeliveryDate(String date) { + return 'Estimated Delivery: $date'; + } + + @override + String get viewOrder => 'View Order'; + + @override + String get backToHome => 'Back to Home'; + + @override + String get allOrders => 'All Orders'; + + @override + String get pendingOrders => 'Pending'; + + @override + String get processingOrders => 'Processing'; + + @override + String get shippingOrders => 'Shipping'; + + @override + String get completedOrders => 'Completed'; + + @override + String get cancelledOrders => 'Cancelled'; + + @override + String get cancelOrder => 'Cancel Order'; + + @override + String get cancelOrderConfirm => + 'Are you sure you want to cancel this order?'; + + @override + String get cancelReason => 'Cancellation Reason'; + + @override + String get orderCancelled => 'Order Cancelled'; + + @override + String get orderTimeline => 'Order Timeline'; + + @override + String get orderPlacedAt => 'Order placed at'; + + @override + String get orderProcessedAt => 'Order processed at'; + + @override + String get orderShippedAt => 'Order shipped at'; + + @override + String get orderDeliveredAt => 'Order delivered at'; + + @override + String get trackingNumber => 'Tracking Number'; + + @override + String get shippingCarrier => 'Shipping Carrier'; + + @override + String get allProjects => 'All Projects'; + + @override + String get planningProjects => 'Planning'; + + @override + String get inProgressProjects => 'In Progress'; + + @override + String get completedProjects => 'Completed'; + + @override + String get projectDetails => 'Project Details'; + + @override + String get projectStatus => 'Project Status'; + + @override + String get updateProgress => 'Update Progress'; + + @override + String get progressUpdated => 'Progress Updated'; + + @override + String get projectCompleted => 'Project Completed'; + + @override + String get completeProject => 'Complete Project'; + + @override + String get completeProjectConfirm => + 'Are you sure you want to mark this project as completed?'; + + @override + String get deleteProject => 'Delete Project'; + + @override + String get deleteProjectConfirm => + 'Are you sure you want to delete this project?'; + + @override + String get projectPhotos => 'Project Photos'; + + @override + String get addPhotos => 'Add Photos'; + + @override + String get projectDocuments => 'Project Documents'; + + @override + String get uploadDocument => 'Upload Document'; + + @override + String get allQuotes => 'All Quotes'; + + @override + String get draftQuotes => 'Drafts'; + + @override + String get sentQuotes => 'Sent'; + + @override + String get acceptedQuotes => 'Accepted'; + + @override + String get rejectedQuotes => 'Rejected'; + + @override + String get expiredQuotes => 'Expired'; + + @override + String get quoteDetails => 'Quote Details'; + + @override + String get sendQuote => 'Send Quote'; + + @override + String get sendQuoteConfirm => + 'Are you sure you want to send this quote to the client?'; + + @override + String get quoteSent => 'Quote Sent'; + + @override + String get acceptQuote => 'Accept Quote'; + + @override + String get rejectQuote => 'Reject Quote'; + + @override + String get deleteQuote => 'Delete Quote'; + + @override + String get deleteQuoteConfirm => + 'Are you sure you want to delete this quote?'; + + @override + String get quoteItems => 'Quote Items'; + + @override + String get addItem => 'Add Item'; + + @override + String get editItem => 'Edit Item'; + + @override + String get removeItem => 'Remove Item'; + + @override + String get recipient => 'Recipient'; + + @override + String get recipientName => 'Recipient Name'; + + @override + String get recipientPhone => 'Recipient Phone'; + + @override + String get addressType => 'Address Type'; + + @override + String get addressLabel => 'Address Label'; + + @override + String get setDefault => 'Set as Default'; + + @override + String get defaultLabel => 'Default'; + + @override + String get addressSaved => 'Address Saved'; + + @override + String get currentPasswordRequired => 'Please enter current password'; + + @override + String get newPasswordRequired => 'Please enter new password'; + + @override + String get confirmPasswordRequired => 'Please confirm new password'; + + @override + String get incorrectPassword => 'Incorrect password'; + + @override + String get passwordStrength => 'Password Strength'; + + @override + String get weak => 'Weak'; + + @override + String get medium => 'Medium'; + + @override + String get strong => 'Strong'; + + @override + String get veryStrong => 'Very Strong'; + + @override + String get passwordRequirement1 => 'At least 8 characters'; + + @override + String get passwordRequirement2 => 'Include uppercase letter'; + + @override + String get passwordRequirement3 => 'Include lowercase letter'; + + @override + String get passwordRequirement4 => 'Include number'; + + @override + String get passwordRequirement5 => 'Include special character'; + + @override + String get uploadPhoto => 'Upload Photo'; + + @override + String get takePhoto => 'Take Photo'; + + @override + String get chooseFromGallery => 'Choose from Gallery'; + + @override + String get removePhoto => 'Remove Photo'; + + @override + String get cropPhoto => 'Crop Photo'; + + @override + String get photoUploaded => 'Photo Uploaded'; + + @override + String get enableNotifications => 'Enable Notifications'; + + @override + String get disableNotifications => 'Disable Notifications'; + + @override + String get orderNotifications => 'Order Notifications'; + + @override + String get promotionNotifications => 'Promotion Notifications'; + + @override + String get systemNotifications => 'System Notifications'; + + @override + String get chatNotifications => 'Chat Notifications'; + + @override + String get pushNotifications => 'Push Notifications'; + + @override + String get emailNotifications => 'Email Notifications'; + + @override + String get smsNotifications => 'SMS Notifications'; + + @override + String get vietnamese => 'Vietnamese'; + + @override + String get english => 'English'; + + @override + String get selectLanguage => 'Select Language'; + + @override + String get languageChanged => 'Language Changed'; + + @override + String get selectTheme => 'Select Theme'; + + @override + String get themeChanged => 'Theme Changed'; + + @override + String get autoTheme => 'Auto'; + + @override + String get allNotifications => 'All'; + + @override + String get orderNotification => 'Orders'; + + @override + String get systemNotification => 'System'; + + @override + String get promotionNotification => 'Promotions'; + + @override + String get markAsRead => 'Mark as Read'; + + @override + String get markAllAsRead => 'Mark All as Read'; + + @override + String get deleteNotification => 'Delete Notification'; + + @override + String get clearNotifications => 'Clear All Notifications'; + + @override + String get clearNotificationsConfirm => + 'Are you sure you want to clear all notifications?'; + + @override + String get notificationCleared => 'Notification Cleared'; + + @override + String unreadNotifications(int count) { + return '$count unread notifications'; + } + + @override + String get online => 'Online'; + + @override + String get offline => 'Offline'; + + @override + String get away => 'Away'; + + @override + String get busy => 'Busy'; + + @override + String lastSeenAt(String time) { + return 'Last seen $time'; + } + + @override + String get messageRead => 'Read'; + + @override + String get messageDelivered => 'Delivered'; + + @override + String get messageSent => 'Sent'; + + @override + String get messageFailed => 'Failed'; + + @override + String get retryMessage => 'Retry'; + + @override + String get deleteMessage => 'Delete Message'; + + @override + String get deleteMessageConfirm => + 'Are you sure you want to delete this message?'; + + @override + String get messageDeleted => 'Message Deleted'; + + @override + String get filterBy => 'Filter By'; + + @override + String get sortBy => 'Sort By'; + + @override + String get priceAscending => 'Price: Low to High'; + + @override + String get priceDescending => 'Price: High to Low'; + + @override + String get nameAscending => 'Name: A-Z'; + + @override + String get nameDescending => 'Name: Z-A'; + + @override + String get dateAscending => 'Oldest First'; + + @override + String get dateDescending => 'Newest First'; + + @override + String get popularityDescending => 'Most Popular'; + + @override + String get applyFilters => 'Apply Filters'; + + @override + String get clearFilters => 'Clear Filters'; + + @override + String get filterApplied => 'Filter Applied'; + + @override + String get noFilterApplied => 'No Filter Applied'; + + @override + String get connectionError => 'Connection Error'; + + @override + String get noInternetConnection => 'No Internet Connection'; + + @override + String get checkConnection => 'Check Connection'; + + @override + String get retryConnection => 'Retry Connection'; + + @override + String get offlineMode => 'Offline Mode'; + + @override + String get syncData => 'Sync Data'; + + @override + String get syncInProgress => 'Syncing...'; + + @override + String get syncCompleted => 'Sync Completed'; + + @override + String get syncFailed => 'Sync Failed'; + + @override + String lastSyncAt(String time) { + return 'Last sync: $time'; + } + + @override + String minutesAgo(int minutes) { + return '$minutes minutes ago'; + } + + @override + String hoursAgo(int hours) { + return '$hours hours ago'; + } + + @override + String daysAgo(int days) { + return '$days days ago'; + } + + @override + String weeksAgo(int weeks) { + return '$weeks weeks ago'; + } + + @override + String monthsAgo(int months) { + return '$months months ago'; + } + + @override + String yearsAgo(int years) { + return '$years years ago'; + } + + @override + String get justNow => 'Just now'; + + @override + String get comingSoon => 'Coming Soon'; + + @override + String get underMaintenance => 'Under Maintenance'; + + @override + String get featureNotAvailable => 'Feature Not Available'; + + @override + String get pageNotFound => 'Page Not Found'; + + @override + String get goToHomePage => 'Go to Home Page'; +} diff --git a/lib/generated/l10n/app_localizations_vi.dart b/lib/generated/l10n/app_localizations_vi.dart new file mode 100644 index 0000000..4bd4432 --- /dev/null +++ b/lib/generated/l10n/app_localizations_vi.dart @@ -0,0 +1,1735 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Vietnamese (`vi`). +class AppLocalizationsVi extends AppLocalizations { + AppLocalizationsVi([String locale = 'vi']) : super(locale); + + @override + String get appTitle => 'Worker App'; + + @override + String get home => 'Trang chủ'; + + @override + String get products => 'Sản phẩm'; + + @override + String get loyalty => 'Hội viên'; + + @override + String get account => 'Tài khoản'; + + @override + String get more => 'Thêm'; + + @override + String get login => 'Đăng nhập'; + + @override + String get phone => 'Số điện thoại'; + + @override + String get enterPhone => 'Nhập số điện thoại'; + + @override + String get enterPhoneHint => 'VD: 0912345678'; + + @override + String get continueButton => 'Tiếp tục'; + + @override + String get verifyOTP => 'Xác thực OTP'; + + @override + String get enterOTP => 'Nhập mã OTP 6 số'; + + @override + String otpSentTo(String phone) { + return 'Mã OTP đã được gửi đến $phone'; + } + + @override + String get resendOTP => 'Gửi lại mã'; + + @override + String resendOTPIn(int seconds) { + return 'Gửi lại sau ${seconds}s'; + } + + @override + String get register => 'Đăng ký'; + + @override + String get registerNewAccount => 'Đăng ký tài khoản mới'; + + @override + String get logout => 'Đăng xuất'; + + @override + String get logoutConfirm => 'Bạn có chắc chắn muốn đăng xuất?'; + + @override + String get save => 'Lưu'; + + @override + String get cancel => 'Hủy'; + + @override + String get delete => 'Xóa'; + + @override + String get edit => 'Sửa'; + + @override + String get search => 'Tìm kiếm'; + + @override + String get filter => 'Lọc'; + + @override + String get sort => 'Sắp xếp'; + + @override + String get confirm => 'Xác nhận'; + + @override + String get close => 'Đóng'; + + @override + String get back => 'Quay lại'; + + @override + String get next => 'Tiếp theo'; + + @override + String get submit => 'Gửi'; + + @override + String get apply => 'Áp dụng'; + + @override + String get clear => 'Xóa'; + + @override + String get clearAll => 'Xóa tất cả'; + + @override + String get viewDetails => 'Xem chi tiết'; + + @override + String get viewAll => 'Xem tất cả'; + + @override + String get refresh => 'Làm mới'; + + @override + String get share => 'Chia sẻ'; + + @override + String get copy => 'Sao chép'; + + @override + String get copied => 'Đã sao chép'; + + @override + String get yes => 'Có'; + + @override + String get no => 'Không'; + + @override + String get pending => 'Chờ xử lý'; + + @override + String get processing => 'Đang xử lý...'; + + @override + String get shipping => 'Phí vận chuyển'; + + @override + String get completed => 'Hoàn thành'; + + @override + String get cancelled => 'Đã hủy'; + + @override + String get active => 'Đang hoạt động'; + + @override + String get inactive => 'Ngưng hoạt động'; + + @override + String get expired => 'Hết hạn'; + + @override + String get draft => 'Bản nháp'; + + @override + String get sent => 'Đã gửi'; + + @override + String get accepted => 'Đã chấp nhận'; + + @override + String get rejected => 'Đã từ chối'; + + @override + String get name => 'Tên'; + + @override + String get fullName => 'Họ và tên'; + + @override + String get email => 'Email'; + + @override + String get password => 'Mật khẩu'; + + @override + String get currentPassword => 'Mật khẩu hiện tại'; + + @override + String get newPassword => 'Mật khẩu mới'; + + @override + String get confirmPassword => 'Xác nhận mật khẩu'; + + @override + String get address => 'Địa chỉ'; + + @override + String get street => 'Đường'; + + @override + String get city => 'Thành phố'; + + @override + String get district => 'Quận/Huyện'; + + @override + String get ward => 'Phường/Xã'; + + @override + String get postalCode => 'Mã bưu điện'; + + @override + String get company => 'Công ty'; + + @override + String get taxId => 'Mã số thuế'; + + @override + String get dateOfBirth => 'Ngày sinh'; + + @override + String get gender => 'Giới tính'; + + @override + String get male => 'Nam'; + + @override + String get female => 'Nữ'; + + @override + String get other => 'Khác'; + + @override + String get contractor => 'Thầu thợ'; + + @override + String get architect => 'Kiến trúc sư'; + + @override + String get distributor => 'Đại lý phân phối'; + + @override + String get broker => 'Môi giới'; + + @override + String get selectUserType => 'Chọn loại người dùng'; + + @override + String get points => 'Điểm'; + + @override + String get currentPoints => 'Điểm hiện tại'; + + @override + String pointsBalance(int points) { + return '$points điểm'; + } + + @override + String earnedPoints(int points) { + return '+$points điểm'; + } + + @override + String spentPoints(int points) { + return '-$points điểm'; + } + + @override + String get memberTier => 'Hạng thành viên'; + + @override + String get diamond => 'Kim cương'; + + @override + String get platinum => 'Bạch kim'; + + @override + String get gold => 'Vàng'; + + @override + String pointsToNextTier(int points, String tier) { + return 'Còn $points điểm để lên hạng $tier'; + } + + @override + String get rewards => 'Quà tặng'; + + @override + String get redeemReward => 'Đổi quà'; + + @override + String get pointsHistory => 'Lịch sử điểm'; + + @override + String get myGifts => 'Quà của tôi'; + + @override + String get referral => 'Giới thiệu bạn bè'; + + @override + String get referralCode => 'Mã giới thiệu'; + + @override + String get referralLink => 'Link giới thiệu'; + + @override + String get totalReferrals => 'Tổng số người giới thiệu'; + + @override + String get shareReferralCode => 'Chia sẻ mã giới thiệu'; + + @override + String get copyReferralCode => 'Sao chép mã'; + + @override + String get copyReferralLink => 'Sao chép link'; + + @override + String get product => 'Sản phẩm'; + + @override + String get productName => 'Tên sản phẩm'; + + @override + String get productCode => 'Mã sản phẩm'; + + @override + String get price => 'Giá'; + + @override + String get salePrice => 'Giá khuyến mãi'; + + @override + String get quantity => 'Số lượng'; + + @override + String get stock => 'Kho'; + + @override + String get inStock => 'Còn hàng'; + + @override + String get outOfStock => 'Hết hàng'; + + @override + String get category => 'Danh mục'; + + @override + String get allCategories => 'Tất cả danh mục'; + + @override + String get addToCart => 'Thêm vào giỏ'; + + @override + String get cart => 'Giỏ hàng'; + + @override + String get cartEmpty => 'Giỏ hàng trống'; + + @override + String cartItemsCount(int count) { + return '$count sản phẩm'; + } + + @override + String get removeFromCart => 'Xóa khỏi giỏ'; + + @override + String get clearCart => 'Xóa giỏ hàng'; + + @override + String get clearCartConfirm => + 'Bạn có chắc chắn muốn xóa tất cả sản phẩm trong giỏ hàng?'; + + @override + String get checkout => 'Thanh toán'; + + @override + String get subtotal => 'Tạm tính'; + + @override + String get discount => 'Giảm giá'; + + @override + String get total => 'Tổng cộng'; + + @override + String get placeOrder => 'Đặt hàng'; + + @override + String get orderPlaced => 'Đơn hàng đã được đặt'; + + @override + String get orderSuccess => 'Đặt hàng thành công'; + + @override + String get orders => 'Đơn hàng'; + + @override + String get myOrders => 'Đơn hàng của tôi'; + + @override + String get orderNumber => 'Số đơn hàng'; + + @override + String get orderDate => 'Ngày đặt'; + + @override + String get orderStatus => 'Trạng thái đơn hàng'; + + @override + String get orderDetails => 'Chi tiết đơn hàng'; + + @override + String get trackOrder => 'Theo dõi đơn hàng'; + + @override + String get reorder => 'Đặt lại'; + + @override + String get paymentMethod => 'Phương thức thanh toán'; + + @override + String get cashOnDelivery => 'Thanh toán khi nhận hàng'; + + @override + String get bankTransfer => 'Chuyển khoản ngân hàng'; + + @override + String get creditCard => 'Thẻ tín dụng'; + + @override + String get eWallet => 'Ví điện tử'; + + @override + String get deliveryAddress => 'Địa chỉ giao hàng'; + + @override + String get estimatedDelivery => 'Dự kiến giao hàng'; + + @override + String get payments => 'Thanh toán'; + + @override + String get paymentId => 'Mã thanh toán'; + + @override + String get paymentStatus => 'Trạng thái thanh toán'; + + @override + String get projects => 'Công trình'; + + @override + String get myProjects => 'Công trình của tôi'; + + @override + String get createProject => 'Tạo công trình'; + + @override + String get projectName => 'Tên công trình'; + + @override + String get projectCode => 'Mã công trình'; + + @override + String get projectType => 'Loại công trình'; + + @override + String get residential => 'Dân dụng'; + + @override + String get commercial => 'Thương mại'; + + @override + String get industrial => 'Công nghiệp'; + + @override + String get client => 'Khách hàng'; + + @override + String get clientName => 'Tên khách hàng'; + + @override + String get clientPhone => 'SĐT khách hàng'; + + @override + String get location => 'Vị trí'; + + @override + String get startDate => 'Ngày bắt đầu'; + + @override + String get endDate => 'Ngày kết thúc'; + + @override + String get progress => 'Tiến độ'; + + @override + String get budget => 'Ngân sách'; + + @override + String get description => 'Mô tả'; + + @override + String get notes => 'Ghi chú'; + + @override + String get quotes => 'Báo giá'; + + @override + String get createQuote => 'Tạo báo giá'; + + @override + String get quoteNumber => 'Số báo giá'; + + @override + String get quoteDate => 'Ngày báo giá'; + + @override + String get validity => 'Hiệu lực'; + + @override + String get convertToOrder => 'Chuyển thành đơn hàng'; + + @override + String get duplicate => 'Nhân bản'; + + @override + String get profile => 'Hồ sơ'; + + @override + String get editProfile => 'Chỉnh sửa hồ sơ'; + + @override + String get avatar => 'Ảnh đại diện'; + + @override + String get uploadAvatar => 'Tải lên ảnh đại diện'; + + @override + String get changePassword => 'Đổi mật khẩu'; + + @override + String get passwordChanged => 'Mật khẩu đã được thay đổi'; + + @override + String get addresses => 'Địa chỉ'; + + @override + String get myAddresses => 'Địa chỉ của tôi'; + + @override + String get addAddress => 'Thêm địa chỉ'; + + @override + String get editAddress => 'Sửa địa chỉ'; + + @override + String get deleteAddress => 'Xóa địa chỉ'; + + @override + String get deleteAddressConfirm => 'Bạn có chắc chắn muốn xóa địa chỉ này?'; + + @override + String get setAsDefault => 'Đặt làm mặc định'; + + @override + String get defaultAddress => 'Địa chỉ mặc định'; + + @override + String get homeAddress => 'Nhà riêng'; + + @override + String get officeAddress => 'Văn phòng'; + + @override + String get settings => 'Cài đặt'; + + @override + String get notifications => 'Thông báo'; + + @override + String get notificationSettings => 'Cài đặt thông báo'; + + @override + String get language => 'Ngôn ngữ'; + + @override + String get theme => 'Giao diện'; + + @override + String get lightMode => 'Sáng'; + + @override + String get darkMode => 'Tối'; + + @override + String get systemMode => 'Theo hệ thống'; + + @override + String get promotions => 'Khuyến mãi'; + + @override + String get promotion => 'Chương trình khuyến mãi'; + + @override + String get activePromotions => 'Khuyến mãi đang diễn ra'; + + @override + String get upcomingPromotions => 'Khuyến mãi sắp diễn ra'; + + @override + String get expiredPromotions => 'Khuyến mãi đã kết thúc'; + + @override + String get claimPromotion => 'Nhận ưu đãi'; + + @override + String get termsAndConditions => 'Điều khoản & Điều kiện'; + + @override + String get chat => 'Trò chuyện'; + + @override + String get chatSupport => 'Hỗ trợ trực tuyến'; + + @override + String get sendMessage => 'Gửi tin nhắn'; + + @override + String get typeMessage => 'Nhập tin nhắn...'; + + @override + String get typingIndicator => 'đang nhập...'; + + @override + String get attachFile => 'Đính kèm tệp'; + + @override + String get supportAgent => 'Nhân viên hỗ trợ'; + + @override + String get fieldRequired => 'Trường này là bắt buộc'; + + @override + String get invalidPhone => 'Số điện thoại không hợp lệ'; + + @override + String get invalidEmail => 'Email không hợp lệ'; + + @override + String get invalidOTP => 'Mã OTP không hợp lệ'; + + @override + String get passwordTooShort => 'Mật khẩu phải có ít nhất 8 ký tự'; + + @override + String get passwordsNotMatch => 'Mật khẩu không khớp'; + + @override + String get passwordRequirements => + 'Mật khẩu phải có ít nhất 8 ký tự, bao gồm chữ hoa, chữ thường, số và ký tự đặc biệt'; + + @override + String get invalidAmount => 'Số tiền không hợp lệ'; + + @override + String get insufficientPoints => 'Không đủ điểm để đổi quà'; + + @override + String get error => 'Lỗi'; + + @override + String get errorOccurred => 'Đã xảy ra lỗi'; + + @override + String get networkError => + 'Lỗi kết nối mạng. Vui lòng kiểm tra kết nối internet của bạn.'; + + @override + String get serverError => 'Lỗi máy chủ. Vui lòng thử lại sau.'; + + @override + String get sessionExpired => + 'Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.'; + + @override + String get notFound => 'Không tìm thấy'; + + @override + String get unauthorized => 'Không có quyền truy cập'; + + @override + String get tryAgain => 'Thử lại'; + + @override + String get contactSupport => 'Liên hệ hỗ trợ'; + + @override + String get success => 'Thành công'; + + @override + String get savedSuccessfully => 'Đã lưu thành công'; + + @override + String get updatedSuccessfully => 'Đã cập nhật thành công'; + + @override + String get deletedSuccessfully => 'Đã xóa thành công'; + + @override + String get sentSuccessfully => 'Đã gửi thành công'; + + @override + String get redeemSuccessful => 'Đổi quà thành công'; + + @override + String get giftCode => 'Mã quà tặng'; + + @override + String get loading => 'Đang tải...'; + + @override + String get loadingData => 'Đang tải dữ liệu...'; + + @override + String get pleaseWait => 'Vui lòng đợi...'; + + @override + String get noData => 'Không có dữ liệu'; + + @override + String get noResults => 'Không có kết quả'; + + @override + String get noProductsFound => 'Không tìm thấy sản phẩm'; + + @override + String get noOrdersYet => 'Chưa có đơn hàng nào'; + + @override + String get noProjectsYet => 'Chưa có công trình nào'; + + @override + String get noNotifications => 'Không có thông báo'; + + @override + String get noGiftsYet => 'Chưa có quà tặng nào'; + + @override + String get startShopping => 'Bắt đầu mua sắm'; + + @override + String get createFirstProject => 'Tạo công trình đầu tiên'; + + @override + String get today => 'Hôm nay'; + + @override + String get yesterday => 'Hôm qua'; + + @override + String get thisWeek => 'Tuần này'; + + @override + String get thisMonth => 'Tháng này'; + + @override + String get all => 'Tất cả'; + + @override + String get dateRange => 'Khoảng thời gian'; + + @override + String get from => 'Từ'; + + @override + String get to => 'Đến'; + + @override + String get date => 'Ngày'; + + @override + String get time => 'Giờ'; + + @override + String get version => 'Phiên bản'; + + @override + String get appVersion => 'Phiên bản ứng dụng'; + + @override + String get help => 'Trợ giúp'; + + @override + String get helpCenter => 'Trung tâm trợ giúp'; + + @override + String get aboutUs => 'Về chúng tôi'; + + @override + String get privacyPolicy => 'Chính sách bảo mật'; + + @override + String get termsOfService => 'Điều khoản sử dụng'; + + @override + String get rateApp => 'Đánh giá ứng dụng'; + + @override + String get feedback => 'Phản hồi'; + + @override + String get sendFeedback => 'Gửi phản hồi'; + + @override + String get unsavedChanges => 'Có thay đổi chưa được lưu'; + + @override + String get unsavedChangesMessage => + 'Bạn có muốn lưu các thay đổi trước khi thoát?'; + + @override + String get welcome => 'Chào mừng'; + + @override + String get welcomeBack => 'Chào mừng trở lại'; + + @override + String welcomeTo(String appName) { + return 'Chào mừng đến với $appName'; + } + + @override + String itemsInCart(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sản phẩm', + one: '1 sản phẩm', + zero: 'Không có sản phẩm', + ); + return '$_temp0'; + } + + @override + String ordersCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count đơn hàng', + one: '1 đơn hàng', + zero: 'Không có đơn hàng', + ); + return '$_temp0'; + } + + @override + String projectsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count công trình', + one: '1 công trình', + zero: 'Không có công trình', + ); + return '$_temp0'; + } + + @override + String daysRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Còn $count ngày', + one: 'Còn 1 ngày', + zero: 'Hôm nay', + ); + return '$_temp0'; + } + + @override + String formatCurrency(String amount) { + return '$amount ₫'; + } + + @override + String formatDate(String day, String month, String year) { + return '$day/$month/$year'; + } + + @override + String formatDateTime( + String day, + String month, + String year, + String hour, + String minute, + ) { + return '$day/$month/$year lúc $hour:$minute'; + } + + @override + String memberSince(String date) { + return 'Thành viên từ $date'; + } + + @override + String validUntil(String date) { + return 'Có hiệu lực đến $date'; + } + + @override + String get used => 'Đã sử dụng'; + + @override + String get unused => 'Chưa sử dụng'; + + @override + String get available => 'Có sẵn'; + + @override + String get unavailable => 'Không có sẵn'; + + @override + String get validFrom => 'Có hiệu lực từ'; + + @override + String get validTo => 'Có hiệu lực đến'; + + @override + String get usageInstructions => 'Hướng dẫn sử dụng'; + + @override + String get useNow => 'Sử dụng ngay'; + + @override + String get scanQRCode => 'Quét mã QR'; + + @override + String get scanBarcode => 'Quét mã vạch'; + + @override + String get qrCodeScanner => 'Quét mã QR'; + + @override + String get memberId => 'Mã thành viên'; + + @override + String get showQRCode => 'Hiển thị mã QR'; + + @override + String get tier => 'Hạng'; + + @override + String get tierBenefits => 'Quyền lợi hạng thành viên'; + + @override + String get pointsMultiplier => 'Hệ số điểm'; + + @override + String multiplierX(String multiplier) { + return 'x$multiplier'; + } + + @override + String get specialOffers => 'Ưu đãi đặc biệt'; + + @override + String get exclusiveDiscounts => 'Giảm giá độc quyền'; + + @override + String get prioritySupport => 'Hỗ trợ ưu tiên'; + + @override + String get earlyAccess => 'Truy cập sớm'; + + @override + String get birthdayGift => 'Quà sinh nhật'; + + @override + String get transactionType => 'Loại giao dịch'; + + @override + String get earnPoints => 'Tích điểm'; + + @override + String get redeemPoints => 'Đổi điểm'; + + @override + String get bonusPoints => 'Điểm thưởng'; + + @override + String get refundPoints => 'Hoàn điểm'; + + @override + String get expiredPoints => 'Điểm hết hạn'; + + @override + String get transferPoints => 'Chuyển điểm'; + + @override + String get pointsExpiry => 'Điểm hết hạn'; + + @override + String pointsWillExpireOn(String date) { + return 'Điểm sẽ hết hạn vào $date'; + } + + @override + String pointsExpiringSoon(int points) { + return '$points điểm sắp hết hạn'; + } + + @override + String get newBalance => 'Số dư mới'; + + @override + String get previousBalance => 'Số dư trước đó'; + + @override + String get balanceAfter => 'Số dư sau giao dịch'; + + @override + String get disputeTransaction => 'Khiếu nại giao dịch'; + + @override + String get disputeReason => 'Lý do khiếu nại'; + + @override + String get disputeSubmitted => 'Khiếu nại đã được gửi'; + + @override + String get rewardCategory => 'Danh mục quà tặng'; + + @override + String get vouchers => 'Phiếu quà tặng'; + + @override + String get productRewards => 'Quà tặng sản phẩm'; + + @override + String get services => 'Dịch vụ'; + + @override + String get experiences => 'Trải nghiệm'; + + @override + String get pointsCost => 'Chi phí điểm'; + + @override + String pointsRequired(int points) { + return 'Yêu cầu $points điểm'; + } + + @override + String get expiryDate => 'Ngày hết hạn'; + + @override + String expiresOn(String date) { + return 'Hết hạn vào $date'; + } + + @override + String get redeemConfirm => 'Xác nhận đổi quà'; + + @override + String redeemConfirmMessage(int points, String reward) { + return 'Bạn có chắc chắn muốn đổi $points điểm để nhận $reward?'; + } + + @override + String get giftStatus => 'Trạng thái quà'; + + @override + String get activeGifts => 'Quà đang dùng'; + + @override + String get usedGifts => 'Quà đã dùng'; + + @override + String get expiredGifts => 'Quà hết hạn'; + + @override + String get giftDetails => 'Chi tiết quà tặng'; + + @override + String get howToUse => 'Cách sử dụng'; + + @override + String get referralInvite => 'Mời bạn bè'; + + @override + String get referralReward => 'Phần thưởng giới thiệu'; + + @override + String get referralSuccess => 'Giới thiệu thành công'; + + @override + String get friendsReferred => 'Bạn bè đã giới thiệu'; + + @override + String get pointsEarned => 'Điểm đã kiếm'; + + @override + String get referralSteps => 'Cách thức giới thiệu'; + + @override + String get step1 => 'Bước 1'; + + @override + String get step2 => 'Bước 2'; + + @override + String get step3 => 'Bước 3'; + + @override + String get shareYourCode => 'Chia sẻ mã của bạn'; + + @override + String get friendRegisters => 'Bạn bè đăng ký'; + + @override + String get bothGetRewards => 'Cả hai nhận thưởng'; + + @override + String get inviteFriends => 'Mời bạn bè'; + + @override + String get sku => 'SKU'; + + @override + String get brand => 'Thương hiệu'; + + @override + String get model => 'Mẫu'; + + @override + String get specification => 'Thông số kỹ thuật'; + + @override + String get specifications => 'Chi tiết kỹ thuật'; + + @override + String get material => 'Chất liệu'; + + @override + String get size => 'Kích thước'; + + @override + String get color => 'Màu sắc'; + + @override + String get weight => 'Trọng lượng'; + + @override + String get dimensions => 'Kích thước'; + + @override + String get availability => 'Tình trạng'; + + @override + String get addedToCart => 'Đã thêm vào giỏ hàng'; + + @override + String get productDetails => 'Chi tiết sản phẩm'; + + @override + String get relatedProducts => 'Sản phẩm liên quan'; + + @override + String get recommended => 'Đề xuất'; + + @override + String get newArrival => 'Hàng mới về'; + + @override + String get bestSeller => 'Bán chạy nhất'; + + @override + String get onSale => 'Đang giảm giá'; + + @override + String get limitedStock => 'Số lượng có hạn'; + + @override + String get lowStock => 'Sắp hết hàng'; + + @override + String get updateQuantity => 'Cập nhật số lượng'; + + @override + String get itemRemoved => 'Đã xóa sản phẩm'; + + @override + String get cartUpdated => 'Giỏ hàng đã được cập nhật'; + + @override + String get proceedToCheckout => 'Tiến hành thanh toán'; + + @override + String get continueShopping => 'Tiếp tục mua sắm'; + + @override + String get emptyCart => 'Giỏ hàng trống'; + + @override + String get emptyCartMessage => 'Bạn chưa có sản phẩm nào trong giỏ hàng'; + + @override + String get selectAddress => 'Chọn địa chỉ'; + + @override + String get selectPaymentMethod => 'Chọn phương thức thanh toán'; + + @override + String get orderSummary => 'Tóm tắt đơn hàng'; + + @override + String get orderConfirmation => 'Xác nhận đơn hàng'; + + @override + String get orderSuccessMessage => 'Đơn hàng của bạn đã được đặt thành công!'; + + @override + String orderNumberIs(String orderNumber) { + return 'Số đơn hàng: $orderNumber'; + } + + @override + String estimatedDeliveryDate(String date) { + return 'Dự kiến giao hàng: $date'; + } + + @override + String get viewOrder => 'Xem đơn hàng'; + + @override + String get backToHome => 'Về trang chủ'; + + @override + String get allOrders => 'Tất cả đơn hàng'; + + @override + String get pendingOrders => 'Chờ xử lý'; + + @override + String get processingOrders => 'Đang xử lý'; + + @override + String get shippingOrders => 'Đang giao'; + + @override + String get completedOrders => 'Hoàn thành'; + + @override + String get cancelledOrders => 'Đã hủy'; + + @override + String get cancelOrder => 'Hủy đơn hàng'; + + @override + String get cancelOrderConfirm => 'Bạn có chắc chắn muốn hủy đơn hàng này?'; + + @override + String get cancelReason => 'Lý do hủy'; + + @override + String get orderCancelled => 'Đơn hàng đã được hủy'; + + @override + String get orderTimeline => 'Lịch sử đơn hàng'; + + @override + String get orderPlacedAt => 'Đơn hàng đã đặt lúc'; + + @override + String get orderProcessedAt => 'Đơn hàng đã xử lý lúc'; + + @override + String get orderShippedAt => 'Đơn hàng đã giao lúc'; + + @override + String get orderDeliveredAt => 'Đơn hàng đã nhận lúc'; + + @override + String get trackingNumber => 'Mã vận đơn'; + + @override + String get shippingCarrier => 'Đơn vị vận chuyển'; + + @override + String get allProjects => 'Tất cả công trình'; + + @override + String get planningProjects => 'Đang lập kế hoạch'; + + @override + String get inProgressProjects => 'Đang thực hiện'; + + @override + String get completedProjects => 'Đã hoàn thành'; + + @override + String get projectDetails => 'Chi tiết công trình'; + + @override + String get projectStatus => 'Trạng thái công trình'; + + @override + String get updateProgress => 'Cập nhật tiến độ'; + + @override + String get progressUpdated => 'Tiến độ đã được cập nhật'; + + @override + String get projectCompleted => 'Công trình đã hoàn thành'; + + @override + String get completeProject => 'Hoàn thành công trình'; + + @override + String get completeProjectConfirm => + 'Bạn có chắc chắn muốn đánh dấu công trình này là hoàn thành?'; + + @override + String get deleteProject => 'Xóa công trình'; + + @override + String get deleteProjectConfirm => + 'Bạn có chắc chắn muốn xóa công trình này?'; + + @override + String get projectPhotos => 'Hình ảnh công trình'; + + @override + String get addPhotos => 'Thêm hình ảnh'; + + @override + String get projectDocuments => 'Tài liệu công trình'; + + @override + String get uploadDocument => 'Tải lên tài liệu'; + + @override + String get allQuotes => 'Tất cả báo giá'; + + @override + String get draftQuotes => 'Bản nháp'; + + @override + String get sentQuotes => 'Đã gửi'; + + @override + String get acceptedQuotes => 'Đã chấp nhận'; + + @override + String get rejectedQuotes => 'Đã từ chối'; + + @override + String get expiredQuotes => 'Hết hạn'; + + @override + String get quoteDetails => 'Chi tiết báo giá'; + + @override + String get sendQuote => 'Gửi báo giá'; + + @override + String get sendQuoteConfirm => + 'Bạn có chắc chắn muốn gửi báo giá này cho khách hàng?'; + + @override + String get quoteSent => 'Báo giá đã được gửi'; + + @override + String get acceptQuote => 'Chấp nhận báo giá'; + + @override + String get rejectQuote => 'Từ chối báo giá'; + + @override + String get deleteQuote => 'Xóa báo giá'; + + @override + String get deleteQuoteConfirm => 'Bạn có chắc chắn muốn xóa báo giá này?'; + + @override + String get quoteItems => 'Các hạng mục'; + + @override + String get addItem => 'Thêm hạng mục'; + + @override + String get editItem => 'Sửa hạng mục'; + + @override + String get removeItem => 'Xóa hạng mục'; + + @override + String get recipient => 'Người nhận'; + + @override + String get recipientName => 'Tên người nhận'; + + @override + String get recipientPhone => 'SĐT người nhận'; + + @override + String get addressType => 'Loại địa chỉ'; + + @override + String get addressLabel => 'Nhãn địa chỉ'; + + @override + String get setDefault => 'Đặt làm mặc định'; + + @override + String get defaultLabel => 'Mặc định'; + + @override + String get addressSaved => 'Địa chỉ đã được lưu'; + + @override + String get currentPasswordRequired => 'Vui lòng nhập mật khẩu hiện tại'; + + @override + String get newPasswordRequired => 'Vui lòng nhập mật khẩu mới'; + + @override + String get confirmPasswordRequired => 'Vui lòng xác nhận mật khẩu mới'; + + @override + String get incorrectPassword => 'Mật khẩu không chính xác'; + + @override + String get passwordStrength => 'Độ mạnh mật khẩu'; + + @override + String get weak => 'Yếu'; + + @override + String get medium => 'Trung bình'; + + @override + String get strong => 'Mạnh'; + + @override + String get veryStrong => 'Rất mạnh'; + + @override + String get passwordRequirement1 => 'Ít nhất 8 ký tự'; + + @override + String get passwordRequirement2 => 'Có chữ hoa'; + + @override + String get passwordRequirement3 => 'Có chữ thường'; + + @override + String get passwordRequirement4 => 'Có số'; + + @override + String get passwordRequirement5 => 'Có ký tự đặc biệt'; + + @override + String get uploadPhoto => 'Tải lên ảnh'; + + @override + String get takePhoto => 'Chụp ảnh'; + + @override + String get chooseFromGallery => 'Chọn từ thư viện'; + + @override + String get removePhoto => 'Xóa ảnh'; + + @override + String get cropPhoto => 'Cắt ảnh'; + + @override + String get photoUploaded => 'Ảnh đã được tải lên'; + + @override + String get enableNotifications => 'Bật thông báo'; + + @override + String get disableNotifications => 'Tắt thông báo'; + + @override + String get orderNotifications => 'Thông báo đơn hàng'; + + @override + String get promotionNotifications => 'Thông báo khuyến mãi'; + + @override + String get systemNotifications => 'Thông báo hệ thống'; + + @override + String get chatNotifications => 'Thông báo trò chuyện'; + + @override + String get pushNotifications => 'Thông báo đẩy'; + + @override + String get emailNotifications => 'Thông báo email'; + + @override + String get smsNotifications => 'Thông báo SMS'; + + @override + String get vietnamese => 'Tiếng Việt'; + + @override + String get english => 'Tiếng Anh'; + + @override + String get selectLanguage => 'Chọn ngôn ngữ'; + + @override + String get languageChanged => 'Ngôn ngữ đã được thay đổi'; + + @override + String get selectTheme => 'Chọn giao diện'; + + @override + String get themeChanged => 'Giao diện đã được thay đổi'; + + @override + String get autoTheme => 'Tự động'; + + @override + String get allNotifications => 'Tất cả'; + + @override + String get orderNotification => 'Đơn hàng'; + + @override + String get systemNotification => 'Hệ thống'; + + @override + String get promotionNotification => 'Khuyến mãi'; + + @override + String get markAsRead => 'Đánh dấu đã đọc'; + + @override + String get markAllAsRead => 'Đánh dấu tất cả đã đọc'; + + @override + String get deleteNotification => 'Xóa thông báo'; + + @override + String get clearNotifications => 'Xóa tất cả thông báo'; + + @override + String get clearNotificationsConfirm => + 'Bạn có chắc chắn muốn xóa tất cả thông báo?'; + + @override + String get notificationCleared => 'Thông báo đã được xóa'; + + @override + String unreadNotifications(int count) { + return '$count thông báo chưa đọc'; + } + + @override + String get online => 'Trực tuyến'; + + @override + String get offline => 'Ngoại tuyến'; + + @override + String get away => 'Vắng mặt'; + + @override + String get busy => 'Bận'; + + @override + String lastSeenAt(String time) { + return 'Hoạt động lần cuối $time'; + } + + @override + String get messageRead => 'Đã đọc'; + + @override + String get messageDelivered => 'Đã gửi'; + + @override + String get messageSent => 'Đã gửi'; + + @override + String get messageFailed => 'Gửi thất bại'; + + @override + String get retryMessage => 'Gửi lại'; + + @override + String get deleteMessage => 'Xóa tin nhắn'; + + @override + String get deleteMessageConfirm => 'Bạn có chắc chắn muốn xóa tin nhắn này?'; + + @override + String get messageDeleted => 'Tin nhắn đã được xóa'; + + @override + String get filterBy => 'Lọc theo'; + + @override + String get sortBy => 'Sắp xếp theo'; + + @override + String get priceAscending => 'Giá tăng dần'; + + @override + String get priceDescending => 'Giá giảm dần'; + + @override + String get nameAscending => 'Tên A-Z'; + + @override + String get nameDescending => 'Tên Z-A'; + + @override + String get dateAscending => 'Cũ nhất'; + + @override + String get dateDescending => 'Mới nhất'; + + @override + String get popularityDescending => 'Phổ biến nhất'; + + @override + String get applyFilters => 'Áp dụng bộ lọc'; + + @override + String get clearFilters => 'Xóa bộ lọc'; + + @override + String get filterApplied => 'Đã áp dụng bộ lọc'; + + @override + String get noFilterApplied => 'Chưa có bộ lọc nào'; + + @override + String get connectionError => 'Lỗi kết nối'; + + @override + String get noInternetConnection => 'Không có kết nối Internet'; + + @override + String get checkConnection => 'Kiểm tra kết nối'; + + @override + String get retryConnection => 'Thử kết nối lại'; + + @override + String get offlineMode => 'Chế độ ngoại tuyến'; + + @override + String get syncData => 'Đồng bộ dữ liệu'; + + @override + String get syncInProgress => 'Đang đồng bộ...'; + + @override + String get syncCompleted => 'Đồng bộ hoàn tất'; + + @override + String get syncFailed => 'Đồng bộ thất bại'; + + @override + String lastSyncAt(String time) { + return 'Đồng bộ lần cuối: $time'; + } + + @override + String minutesAgo(int minutes) { + return '$minutes phút trước'; + } + + @override + String hoursAgo(int hours) { + return '$hours giờ trước'; + } + + @override + String daysAgo(int days) { + return '$days ngày trước'; + } + + @override + String weeksAgo(int weeks) { + return '$weeks tuần trước'; + } + + @override + String monthsAgo(int months) { + return '$months tháng trước'; + } + + @override + String yearsAgo(int years) { + return '$years năm trước'; + } + + @override + String get justNow => 'Vừa xong'; + + @override + String get comingSoon => 'Sắp ra mắt'; + + @override + String get underMaintenance => 'Đang bảo trì'; + + @override + String get featureNotAvailable => 'Tính năng chưa khả dụng'; + + @override + String get pageNotFound => 'Không tìm thấy trang'; + + @override + String get goToHomePage => 'Về trang chủ'; +} diff --git a/lib/hive_registrar.g.dart b/lib/hive_registrar.g.dart new file mode 100644 index 0000000..16d56db --- /dev/null +++ b/lib/hive_registrar.g.dart @@ -0,0 +1,39 @@ +// Generated by Hive CE +// Do not modify +// Check in to version control + +import 'package:hive_ce/hive.dart'; +import 'package:worker/core/database/models/cached_data.dart'; +import 'package:worker/core/database/models/enums.dart'; + +extension HiveRegistrar on HiveInterface { + void registerAdapters() { + registerAdapter(CachedDataAdapter()); + registerAdapter(GiftStatusAdapter()); + registerAdapter(MemberTierAdapter()); + registerAdapter(NotificationTypeAdapter()); + registerAdapter(OrderStatusAdapter()); + registerAdapter(PaymentMethodAdapter()); + registerAdapter(PaymentStatusAdapter()); + registerAdapter(ProjectStatusAdapter()); + registerAdapter(ProjectTypeAdapter()); + registerAdapter(TransactionTypeAdapter()); + registerAdapter(UserTypeAdapter()); + } +} + +extension IsolatedHiveRegistrar on IsolatedHiveInterface { + void registerAdapters() { + registerAdapter(CachedDataAdapter()); + registerAdapter(GiftStatusAdapter()); + registerAdapter(MemberTierAdapter()); + registerAdapter(NotificationTypeAdapter()); + registerAdapter(OrderStatusAdapter()); + registerAdapter(PaymentMethodAdapter()); + registerAdapter(PaymentStatusAdapter()); + registerAdapter(ProjectStatusAdapter()); + registerAdapter(ProjectTypeAdapter()); + registerAdapter(TransactionTypeAdapter()); + registerAdapter(UserTypeAdapter()); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..4192fcc --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,914 @@ +{ + "@@locale": "en", + + "appTitle": "Worker App", + "@appTitle": { + "description": "The application title" + }, + + "home": "Home", + "@home": { + "description": "Home navigation item" + }, + "products": "Products", + "@products": { + "description": "Products navigation item" + }, + "loyalty": "Loyalty", + "@loyalty": { + "description": "Loyalty navigation item" + }, + "account": "Account", + "@account": { + "description": "Account navigation item" + }, + "more": "More", + "@more": { + "description": "More navigation item" + }, + + "login": "Login", + "phone": "Phone Number", + "enterPhone": "Enter phone number", + "enterPhoneHint": "Ex: 0912345678", + "continueButton": "Continue", + "verifyOTP": "Verify OTP", + "enterOTP": "Enter 6-digit OTP code", + "otpSentTo": "OTP code has been sent to {phone}", + "@otpSentTo": { + "description": "OTP sent message", + "placeholders": { + "phone": { + "type": "String", + "example": "0912345678" + } + } + }, + "resendOTP": "Resend code", + "resendOTPIn": "Resend in {seconds}s", + "@resendOTPIn": { + "description": "Resend OTP countdown", + "placeholders": { + "seconds": { + "type": "int", + "example": "60" + } + } + }, + "register": "Register", + "registerNewAccount": "Register new account", + "logout": "Logout", + "logoutConfirm": "Are you sure you want to logout?", + + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "search": "Search", + "filter": "Filter", + "sort": "Sort", + "confirm": "Confirm", + "close": "Close", + "back": "Back", + "next": "Next", + "submit": "Submit", + "apply": "Apply", + "clear": "Clear", + "clearAll": "Clear All", + "viewDetails": "View Details", + "viewAll": "View All", + "refresh": "Refresh", + "share": "Share", + "copy": "Copy", + "copied": "Copied", + "yes": "Yes", + "no": "No", + + "pending": "Pending", + "processing": "Processing", + "shipping": "Shipping", + "completed": "Completed", + "cancelled": "Cancelled", + "active": "Active", + "inactive": "Inactive", + "expired": "Expired", + "draft": "Draft", + "sent": "Sent", + "accepted": "Accepted", + "rejected": "Rejected", + + "name": "Name", + "fullName": "Full Name", + "email": "Email", + "password": "Password", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmPassword": "Confirm Password", + "address": "Address", + "street": "Street", + "city": "City", + "district": "District", + "ward": "Ward", + "postalCode": "Postal Code", + "company": "Company", + "taxId": "Tax ID", + "dateOfBirth": "Date of Birth", + "gender": "Gender", + "male": "Male", + "female": "Female", + "other": "Other", + + "contractor": "Contractor", + "architect": "Architect", + "distributor": "Distributor", + "broker": "Broker", + "selectUserType": "Select user type", + + "points": "Points", + "currentPoints": "Current Points", + "pointsBalance": "{points} points", + "@pointsBalance": { + "description": "Points balance display", + "placeholders": { + "points": { + "type": "int", + "example": "1000" + } + } + }, + "earnedPoints": "+{points} points", + "@earnedPoints": { + "description": "Points earned", + "placeholders": { + "points": { + "type": "int", + "example": "100" + } + } + }, + "spentPoints": "-{points} points", + "@spentPoints": { + "description": "Points spent", + "placeholders": { + "points": { + "type": "int", + "example": "50" + } + } + }, + "memberTier": "Member Tier", + "diamond": "Diamond", + "platinum": "Platinum", + "gold": "Gold", + "pointsToNextTier": "{points} points to reach {tier}", + "@pointsToNextTier": { + "description": "Points needed for next tier", + "placeholders": { + "points": { + "type": "int", + "example": "500" + }, + "tier": { + "type": "String", + "example": "Platinum" + } + } + }, + "rewards": "Rewards", + "redeemReward": "Redeem Reward", + "pointsHistory": "Points History", + "myGifts": "My Gifts", + "referral": "Refer Friends", + "referralCode": "Referral Code", + "referralLink": "Referral Link", + "totalReferrals": "Total Referrals", + "shareReferralCode": "Share Referral Code", + "copyReferralCode": "Copy Code", + "copyReferralLink": "Copy Link", + + "product": "Product", + "productName": "Product Name", + "productCode": "Product Code", + "price": "Price", + "salePrice": "Sale Price", + "quantity": "Quantity", + "stock": "Stock", + "inStock": "In Stock", + "outOfStock": "Out of Stock", + "category": "Category", + "allCategories": "All Categories", + "addToCart": "Add to Cart", + "cart": "Cart", + "cartEmpty": "Cart is empty", + "cartItemsCount": "{count} items", + "@cartItemsCount": { + "description": "Number of items in cart", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + "removeFromCart": "Remove from Cart", + "clearCart": "Clear Cart", + "clearCartConfirm": "Are you sure you want to clear all items from the cart?", + + "checkout": "Checkout", + "subtotal": "Subtotal", + "discount": "Discount", + "shipping": "Shipping", + "total": "Total", + "placeOrder": "Place Order", + "orderPlaced": "Order Placed", + "orderSuccess": "Order Successful", + "orders": "Orders", + "myOrders": "My Orders", + "orderNumber": "Order Number", + "orderDate": "Order Date", + "orderStatus": "Order Status", + "orderDetails": "Order Details", + "trackOrder": "Track Order", + "reorder": "Reorder", + "paymentMethod": "Payment Method", + "cashOnDelivery": "Cash on Delivery", + "bankTransfer": "Bank Transfer", + "creditCard": "Credit Card", + "eWallet": "E-Wallet", + "deliveryAddress": "Delivery Address", + "estimatedDelivery": "Estimated Delivery", + "payments": "Payments", + "paymentId": "Payment ID", + "paymentStatus": "Payment Status", + + "projects": "Projects", + "myProjects": "My Projects", + "createProject": "Create Project", + "projectName": "Project Name", + "projectCode": "Project Code", + "projectType": "Project Type", + "residential": "Residential", + "commercial": "Commercial", + "industrial": "Industrial", + "client": "Client", + "clientName": "Client Name", + "clientPhone": "Client Phone", + "location": "Location", + "startDate": "Start Date", + "endDate": "End Date", + "progress": "Progress", + "budget": "Budget", + "description": "Description", + "notes": "Notes", + "quotes": "Quotes", + "createQuote": "Create Quote", + "quoteNumber": "Quote Number", + "quoteDate": "Quote Date", + "validity": "Validity", + "convertToOrder": "Convert to Order", + "duplicate": "Duplicate", + + "profile": "Profile", + "editProfile": "Edit Profile", + "avatar": "Avatar", + "uploadAvatar": "Upload Avatar", + "changePassword": "Change Password", + "passwordChanged": "Password changed successfully", + "addresses": "Addresses", + "myAddresses": "My Addresses", + "addAddress": "Add Address", + "editAddress": "Edit Address", + "deleteAddress": "Delete Address", + "deleteAddressConfirm": "Are you sure you want to delete this address?", + "setAsDefault": "Set as Default", + "defaultAddress": "Default Address", + "homeAddress": "Home", + "officeAddress": "Office", + "settings": "Settings", + "notifications": "Notifications", + "notificationSettings": "Notification Settings", + "language": "Language", + "theme": "Theme", + "lightMode": "Light", + "darkMode": "Dark", + "systemMode": "System", + + "promotions": "Promotions", + "promotion": "Promotion", + "activePromotions": "Active Promotions", + "upcomingPromotions": "Upcoming Promotions", + "expiredPromotions": "Expired Promotions", + "claimPromotion": "Claim Promotion", + "termsAndConditions": "Terms & Conditions", + + "chat": "Chat", + "chatSupport": "Chat Support", + "sendMessage": "Send Message", + "typeMessage": "Type a message...", + "typingIndicator": "typing...", + "attachFile": "Attach File", + "supportAgent": "Support Agent", + + "fieldRequired": "This field is required", + "invalidPhone": "Invalid phone number", + "invalidEmail": "Invalid email", + "invalidOTP": "Invalid OTP code", + "passwordTooShort": "Password must be at least 8 characters", + "passwordsNotMatch": "Passwords do not match", + "passwordRequirements": "Password must be at least 8 characters and include uppercase, lowercase, numbers, and special characters", + "invalidAmount": "Invalid amount", + "insufficientPoints": "Insufficient points to redeem", + + "error": "Error", + "errorOccurred": "An error occurred", + "networkError": "Network error. Please check your internet connection.", + "serverError": "Server error. Please try again later.", + "sessionExpired": "Session expired. Please login again.", + "notFound": "Not found", + "unauthorized": "Unauthorized access", + "tryAgain": "Try Again", + "contactSupport": "Contact Support", + + "success": "Success", + "savedSuccessfully": "Saved successfully", + "updatedSuccessfully": "Updated successfully", + "deletedSuccessfully": "Deleted successfully", + "sentSuccessfully": "Sent successfully", + "redeemSuccessful": "Reward redeemed successfully", + "giftCode": "Gift Code", + + "loading": "Loading...", + "loadingData": "Loading data...", + "processing": "Processing...", + "pleaseWait": "Please wait...", + + "noData": "No data", + "noResults": "No results", + "noProductsFound": "No products found", + "noOrdersYet": "No orders yet", + "noProjectsYet": "No projects yet", + "noNotifications": "No notifications", + "noGiftsYet": "No gifts yet", + "startShopping": "Start Shopping", + "createFirstProject": "Create Your First Project", + + "today": "Today", + "yesterday": "Yesterday", + "thisWeek": "This Week", + "thisMonth": "This Month", + "all": "All", + "dateRange": "Date Range", + "from": "From", + "to": "To", + "date": "Date", + "time": "Time", + + "version": "Version", + "appVersion": "App Version", + "help": "Help", + "helpCenter": "Help Center", + "aboutUs": "About Us", + "privacyPolicy": "Privacy Policy", + "termsOfService": "Terms of Service", + "rateApp": "Rate App", + "feedback": "Feedback", + "sendFeedback": "Send Feedback", + "unsavedChanges": "Unsaved Changes", + "unsavedChangesMessage": "Do you want to save changes before leaving?", + + "welcome": "Welcome", + "welcomeBack": "Welcome Back", + "welcomeTo": "Welcome to {appName}", + "@welcomeTo": { + "description": "Welcome message with app name", + "placeholders": { + "appName": { + "type": "String", + "example": "Worker App" + } + } + }, + + "itemsInCart": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", + "@itemsInCart": { + "description": "Number of items in cart with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + + "ordersCount": "{count, plural, =0{No orders} =1{1 order} other{{count} orders}}", + "@ordersCount": { + "description": "Number of orders with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "5" + } + } + }, + + "projectsCount": "{count, plural, =0{No projects} =1{1 project} other{{count} projects}}", + "@projectsCount": { + "description": "Number of projects with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + + "daysRemaining": "{count, plural, =0{Today} =1{1 day left} other{{count} days left}}", + "@daysRemaining": { + "description": "Days remaining with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "7" + } + } + }, + + "formatCurrency": "{amount} ₫", + "@formatCurrency": { + "description": "Format currency in Vietnamese Dong", + "placeholders": { + "amount": { + "type": "String", + "example": "1,000,000" + } + } + }, + + "formatDate": "{month}/{day}/{year}", + "@formatDate": { + "description": "Date format MM/DD/YYYY", + "placeholders": { + "day": { + "type": "String" + }, + "month": { + "type": "String" + }, + "year": { + "type": "String" + } + } + }, + + "formatDateTime": "{month}/{day}/{year} at {hour}:{minute}", + "@formatDateTime": { + "description": "DateTime format MM/DD/YYYY at HH:mm", + "placeholders": { + "day": {"type": "String"}, + "month": {"type": "String"}, + "year": {"type": "String"}, + "hour": {"type": "String"}, + "minute": {"type": "String"} + } + }, + + "memberSince": "Member since {date}", + "@memberSince": { + "description": "Member since date", + "placeholders": { + "date": { + "type": "String", + "example": "01/2024" + } + } + }, + + "validUntil": "Valid until {date}", + "@validUntil": { + "description": "Valid until date", + "placeholders": { + "date": { + "type": "String", + "example": "12/31/2024" + } + } + }, + + "used": "Used", + "unused": "Unused", + "available": "Available", + "unavailable": "Unavailable", + "validFrom": "Valid from", + "validTo": "Valid to", + "usageInstructions": "Usage Instructions", + "useNow": "Use Now", + + "scanQRCode": "Scan QR Code", + "scanBarcode": "Scan Barcode", + "qrCodeScanner": "QR Code Scanner", + "memberId": "Member ID", + "showQRCode": "Show QR Code", + + "tier": "Tier", + "tierBenefits": "Tier Benefits", + "pointsMultiplier": "Points Multiplier", + "multiplierX": "x{multiplier}", + "@multiplierX": { + "description": "Points multiplier display", + "placeholders": { + "multiplier": { + "type": "String", + "example": "1.5" + } + } + }, + "specialOffers": "Special Offers", + "exclusiveDiscounts": "Exclusive Discounts", + "prioritySupport": "Priority Support", + "earlyAccess": "Early Access", + "birthdayGift": "Birthday Gift", + + "transactionType": "Transaction Type", + "earnPoints": "Earn Points", + "redeemPoints": "Redeem Points", + "bonusPoints": "Bonus Points", + "refundPoints": "Refund Points", + "expiredPoints": "Expired Points", + "transferPoints": "Transfer Points", + "pointsExpiry": "Points Expiry", + "pointsWillExpireOn": "Points will expire on {date}", + "@pointsWillExpireOn": { + "description": "Points expiration date", + "placeholders": { + "date": { + "type": "String", + "example": "12/31/2024" + } + } + }, + "pointsExpiringSoon": "{points} points expiring soon", + "@pointsExpiringSoon": { + "description": "Points expiring soon warning", + "placeholders": { + "points": { + "type": "int", + "example": "100" + } + } + }, + + "newBalance": "New Balance", + "previousBalance": "Previous Balance", + "balanceAfter": "Balance After Transaction", + "disputeTransaction": "Dispute Transaction", + "disputeReason": "Dispute Reason", + "disputeSubmitted": "Dispute Submitted", + + "rewardCategory": "Reward Category", + "vouchers": "Vouchers", + "productRewards": "Product Rewards", + "services": "Services", + "experiences": "Experiences", + "pointsCost": "Points Cost", + "pointsRequired": "Requires {points} points", + "@pointsRequired": { + "description": "Points required for reward", + "placeholders": { + "points": { + "type": "int", + "example": "500" + } + } + }, + "expiryDate": "Expiry Date", + "expiresOn": "Expires on {date}", + "@expiresOn": { + "description": "Expiration date", + "placeholders": { + "date": { + "type": "String", + "example": "12/31/2024" + } + } + }, + "redeemConfirm": "Confirm Redemption", + "redeemConfirmMessage": "Are you sure you want to redeem {points} points for {reward}?", + "@redeemConfirmMessage": { + "description": "Redeem confirmation message", + "placeholders": { + "points": { + "type": "int", + "example": "500" + }, + "reward": { + "type": "String", + "example": "Gift Voucher" + } + } + }, + + "giftStatus": "Gift Status", + "activeGifts": "Active Gifts", + "usedGifts": "Used Gifts", + "expiredGifts": "Expired Gifts", + "giftDetails": "Gift Details", + "howToUse": "How to Use", + + "referralInvite": "Invite Friends", + "referralReward": "Referral Reward", + "referralSuccess": "Referral Successful", + "friendsReferred": "Friends Referred", + "pointsEarned": "Points Earned", + "referralSteps": "How It Works", + "step1": "Step 1", + "step2": "Step 2", + "step3": "Step 3", + "shareYourCode": "Share Your Code", + "friendRegisters": "Friend Registers", + "bothGetRewards": "Both Get Rewards", + "inviteFriends": "Invite Friends", + + "sku": "SKU", + "brand": "Brand", + "model": "Model", + "specification": "Specification", + "specifications": "Specifications", + "material": "Material", + "size": "Size", + "color": "Color", + "weight": "Weight", + "dimensions": "Dimensions", + "availability": "Availability", + "addedToCart": "Added to Cart", + "productDetails": "Product Details", + "relatedProducts": "Related Products", + "recommended": "Recommended", + "newArrival": "New Arrival", + "bestSeller": "Best Seller", + "onSale": "On Sale", + "limitedStock": "Limited Stock", + "lowStock": "Low Stock", + + "updateQuantity": "Update Quantity", + "itemRemoved": "Item Removed", + "cartUpdated": "Cart Updated", + "proceedToCheckout": "Proceed to Checkout", + "continueShopping": "Continue Shopping", + "emptyCart": "Empty Cart", + "emptyCartMessage": "You don't have any items in your cart", + + "selectAddress": "Select Address", + "selectPaymentMethod": "Select Payment Method", + "orderSummary": "Order Summary", + "orderConfirmation": "Order Confirmation", + "orderSuccessMessage": "Your order has been placed successfully!", + "orderNumberIs": "Order Number: {orderNumber}", + "@orderNumberIs": { + "description": "Order number display", + "placeholders": { + "orderNumber": { + "type": "String", + "example": "ORD-2024-001" + } + } + }, + "estimatedDeliveryDate": "Estimated Delivery: {date}", + "@estimatedDeliveryDate": { + "description": "Estimated delivery date", + "placeholders": { + "date": { + "type": "String", + "example": "12/25/2024" + } + } + }, + "viewOrder": "View Order", + "backToHome": "Back to Home", + + "allOrders": "All Orders", + "pendingOrders": "Pending", + "processingOrders": "Processing", + "shippingOrders": "Shipping", + "completedOrders": "Completed", + "cancelledOrders": "Cancelled", + "cancelOrder": "Cancel Order", + "cancelOrderConfirm": "Are you sure you want to cancel this order?", + "cancelReason": "Cancellation Reason", + "orderCancelled": "Order Cancelled", + "orderTimeline": "Order Timeline", + "orderPlacedAt": "Order placed at", + "orderProcessedAt": "Order processed at", + "orderShippedAt": "Order shipped at", + "orderDeliveredAt": "Order delivered at", + "trackingNumber": "Tracking Number", + "shippingCarrier": "Shipping Carrier", + + "allProjects": "All Projects", + "planningProjects": "Planning", + "inProgressProjects": "In Progress", + "completedProjects": "Completed", + "projectDetails": "Project Details", + "projectStatus": "Project Status", + "updateProgress": "Update Progress", + "progressUpdated": "Progress Updated", + "projectCompleted": "Project Completed", + "completeProject": "Complete Project", + "completeProjectConfirm": "Are you sure you want to mark this project as completed?", + "deleteProject": "Delete Project", + "deleteProjectConfirm": "Are you sure you want to delete this project?", + "projectPhotos": "Project Photos", + "addPhotos": "Add Photos", + "projectDocuments": "Project Documents", + "uploadDocument": "Upload Document", + + "allQuotes": "All Quotes", + "draftQuotes": "Drafts", + "sentQuotes": "Sent", + "acceptedQuotes": "Accepted", + "rejectedQuotes": "Rejected", + "expiredQuotes": "Expired", + "quoteDetails": "Quote Details", + "sendQuote": "Send Quote", + "sendQuoteConfirm": "Are you sure you want to send this quote to the client?", + "quoteSent": "Quote Sent", + "acceptQuote": "Accept Quote", + "rejectQuote": "Reject Quote", + "deleteQuote": "Delete Quote", + "deleteQuoteConfirm": "Are you sure you want to delete this quote?", + "quoteItems": "Quote Items", + "addItem": "Add Item", + "editItem": "Edit Item", + "removeItem": "Remove Item", + + "recipient": "Recipient", + "recipientName": "Recipient Name", + "recipientPhone": "Recipient Phone", + "addressType": "Address Type", + "addressLabel": "Address Label", + "setDefault": "Set as Default", + "defaultLabel": "Default", + "addressSaved": "Address Saved", + + "currentPasswordRequired": "Please enter current password", + "newPasswordRequired": "Please enter new password", + "confirmPasswordRequired": "Please confirm new password", + "incorrectPassword": "Incorrect password", + "passwordStrength": "Password Strength", + "weak": "Weak", + "medium": "Medium", + "strong": "Strong", + "veryStrong": "Very Strong", + "passwordRequirement1": "At least 8 characters", + "passwordRequirement2": "Include uppercase letter", + "passwordRequirement3": "Include lowercase letter", + "passwordRequirement4": "Include number", + "passwordRequirement5": "Include special character", + + "uploadPhoto": "Upload Photo", + "takePhoto": "Take Photo", + "chooseFromGallery": "Choose from Gallery", + "removePhoto": "Remove Photo", + "cropPhoto": "Crop Photo", + "photoUploaded": "Photo Uploaded", + + "enableNotifications": "Enable Notifications", + "disableNotifications": "Disable Notifications", + "orderNotifications": "Order Notifications", + "promotionNotifications": "Promotion Notifications", + "systemNotifications": "System Notifications", + "chatNotifications": "Chat Notifications", + "pushNotifications": "Push Notifications", + "emailNotifications": "Email Notifications", + "smsNotifications": "SMS Notifications", + + "vietnamese": "Vietnamese", + "english": "English", + "selectLanguage": "Select Language", + "languageChanged": "Language Changed", + + "selectTheme": "Select Theme", + "themeChanged": "Theme Changed", + "autoTheme": "Auto", + + "allNotifications": "All", + "orderNotification": "Orders", + "systemNotification": "System", + "promotionNotification": "Promotions", + "markAsRead": "Mark as Read", + "markAllAsRead": "Mark All as Read", + "deleteNotification": "Delete Notification", + "clearNotifications": "Clear All Notifications", + "clearNotificationsConfirm": "Are you sure you want to clear all notifications?", + "notificationCleared": "Notification Cleared", + "unreadNotifications": "{count} unread notifications", + "@unreadNotifications": { + "description": "Unread notifications count", + "placeholders": { + "count": { + "type": "int", + "example": "5" + } + } + }, + + "online": "Online", + "offline": "Offline", + "away": "Away", + "busy": "Busy", + "lastSeenAt": "Last seen {time}", + "@lastSeenAt": { + "description": "Last seen timestamp", + "placeholders": { + "time": { + "type": "String", + "example": "10 minutes ago" + } + } + }, + "messageRead": "Read", + "messageDelivered": "Delivered", + "messageSent": "Sent", + "messageFailed": "Failed", + "retryMessage": "Retry", + "deleteMessage": "Delete Message", + "deleteMessageConfirm": "Are you sure you want to delete this message?", + "messageDeleted": "Message Deleted", + + "filterBy": "Filter By", + "sortBy": "Sort By", + "priceAscending": "Price: Low to High", + "priceDescending": "Price: High to Low", + "nameAscending": "Name: A-Z", + "nameDescending": "Name: Z-A", + "dateAscending": "Oldest First", + "dateDescending": "Newest First", + "popularityDescending": "Most Popular", + "applyFilters": "Apply Filters", + "clearFilters": "Clear Filters", + "filterApplied": "Filter Applied", + "noFilterApplied": "No Filter Applied", + + "connectionError": "Connection Error", + "noInternetConnection": "No Internet Connection", + "checkConnection": "Check Connection", + "retryConnection": "Retry Connection", + "offlineMode": "Offline Mode", + "syncData": "Sync Data", + "syncInProgress": "Syncing...", + "syncCompleted": "Sync Completed", + "syncFailed": "Sync Failed", + "lastSyncAt": "Last sync: {time}", + "@lastSyncAt": { + "description": "Last sync timestamp", + "placeholders": { + "time": { + "type": "String", + "example": "5 minutes ago" + } + } + }, + + "minutesAgo": "{minutes} minutes ago", + "@minutesAgo": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "hoursAgo": "{hours} hours ago", + "@hoursAgo": { + "placeholders": { + "hours": {"type": "int"} + } + }, + "daysAgo": "{days} days ago", + "@daysAgo": { + "placeholders": { + "days": {"type": "int"} + } + }, + "weeksAgo": "{weeks} weeks ago", + "@weeksAgo": { + "placeholders": { + "weeks": {"type": "int"} + } + }, + "monthsAgo": "{months} months ago", + "@monthsAgo": { + "placeholders": { + "months": {"type": "int"} + } + }, + "yearsAgo": "{years} years ago", + "@yearsAgo": { + "placeholders": { + "years": {"type": "int"} + } + }, + "justNow": "Just now", + + "comingSoon": "Coming Soon", + "underMaintenance": "Under Maintenance", + "featureNotAvailable": "Feature Not Available", + "pageNotFound": "Page Not Found", + "goToHomePage": "Go to Home Page" +} diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb new file mode 100644 index 0000000..3005be9 --- /dev/null +++ b/lib/l10n/app_vi.arb @@ -0,0 +1,914 @@ +{ + "@@locale": "vi", + + "appTitle": "Worker App", + "@appTitle": { + "description": "The application title" + }, + + "home": "Trang chủ", + "@home": { + "description": "Home navigation item" + }, + "products": "Sản phẩm", + "@products": { + "description": "Products navigation item" + }, + "loyalty": "Hội viên", + "@loyalty": { + "description": "Loyalty navigation item" + }, + "account": "Tài khoản", + "@account": { + "description": "Account navigation item" + }, + "more": "Thêm", + "@more": { + "description": "More navigation item" + }, + + "login": "Đăng nhập", + "phone": "Số điện thoại", + "enterPhone": "Nhập số điện thoại", + "enterPhoneHint": "VD: 0912345678", + "continueButton": "Tiếp tục", + "verifyOTP": "Xác thực OTP", + "enterOTP": "Nhập mã OTP 6 số", + "otpSentTo": "Mã OTP đã được gửi đến {phone}", + "@otpSentTo": { + "description": "OTP sent message", + "placeholders": { + "phone": { + "type": "String", + "example": "0912345678" + } + } + }, + "resendOTP": "Gửi lại mã", + "resendOTPIn": "Gửi lại sau {seconds}s", + "@resendOTPIn": { + "description": "Resend OTP countdown", + "placeholders": { + "seconds": { + "type": "int", + "example": "60" + } + } + }, + "register": "Đăng ký", + "registerNewAccount": "Đăng ký tài khoản mới", + "logout": "Đăng xuất", + "logoutConfirm": "Bạn có chắc chắn muốn đăng xuất?", + + "save": "Lưu", + "cancel": "Hủy", + "delete": "Xóa", + "edit": "Sửa", + "search": "Tìm kiếm", + "filter": "Lọc", + "sort": "Sắp xếp", + "confirm": "Xác nhận", + "close": "Đóng", + "back": "Quay lại", + "next": "Tiếp theo", + "submit": "Gửi", + "apply": "Áp dụng", + "clear": "Xóa", + "clearAll": "Xóa tất cả", + "viewDetails": "Xem chi tiết", + "viewAll": "Xem tất cả", + "refresh": "Làm mới", + "share": "Chia sẻ", + "copy": "Sao chép", + "copied": "Đã sao chép", + "yes": "Có", + "no": "Không", + + "pending": "Chờ xử lý", + "processing": "Đang xử lý", + "shipping": "Đang giao hàng", + "completed": "Hoàn thành", + "cancelled": "Đã hủy", + "active": "Đang hoạt động", + "inactive": "Ngưng hoạt động", + "expired": "Hết hạn", + "draft": "Bản nháp", + "sent": "Đã gửi", + "accepted": "Đã chấp nhận", + "rejected": "Đã từ chối", + + "name": "Tên", + "fullName": "Họ và tên", + "email": "Email", + "password": "Mật khẩu", + "currentPassword": "Mật khẩu hiện tại", + "newPassword": "Mật khẩu mới", + "confirmPassword": "Xác nhận mật khẩu", + "address": "Địa chỉ", + "street": "Đường", + "city": "Thành phố", + "district": "Quận/Huyện", + "ward": "Phường/Xã", + "postalCode": "Mã bưu điện", + "company": "Công ty", + "taxId": "Mã số thuế", + "dateOfBirth": "Ngày sinh", + "gender": "Giới tính", + "male": "Nam", + "female": "Nữ", + "other": "Khác", + + "contractor": "Thầu thợ", + "architect": "Kiến trúc sư", + "distributor": "Đại lý phân phối", + "broker": "Môi giới", + "selectUserType": "Chọn loại người dùng", + + "points": "Điểm", + "currentPoints": "Điểm hiện tại", + "pointsBalance": "{points} điểm", + "@pointsBalance": { + "description": "Points balance display", + "placeholders": { + "points": { + "type": "int", + "example": "1000" + } + } + }, + "earnedPoints": "+{points} điểm", + "@earnedPoints": { + "description": "Points earned", + "placeholders": { + "points": { + "type": "int", + "example": "100" + } + } + }, + "spentPoints": "-{points} điểm", + "@spentPoints": { + "description": "Points spent", + "placeholders": { + "points": { + "type": "int", + "example": "50" + } + } + }, + "memberTier": "Hạng thành viên", + "diamond": "Kim cương", + "platinum": "Bạch kim", + "gold": "Vàng", + "pointsToNextTier": "Còn {points} điểm để lên hạng {tier}", + "@pointsToNextTier": { + "description": "Points needed for next tier", + "placeholders": { + "points": { + "type": "int", + "example": "500" + }, + "tier": { + "type": "String", + "example": "Platinum" + } + } + }, + "rewards": "Quà tặng", + "redeemReward": "Đổi quà", + "pointsHistory": "Lịch sử điểm", + "myGifts": "Quà của tôi", + "referral": "Giới thiệu bạn bè", + "referralCode": "Mã giới thiệu", + "referralLink": "Link giới thiệu", + "totalReferrals": "Tổng số người giới thiệu", + "shareReferralCode": "Chia sẻ mã giới thiệu", + "copyReferralCode": "Sao chép mã", + "copyReferralLink": "Sao chép link", + + "product": "Sản phẩm", + "productName": "Tên sản phẩm", + "productCode": "Mã sản phẩm", + "price": "Giá", + "salePrice": "Giá khuyến mãi", + "quantity": "Số lượng", + "stock": "Kho", + "inStock": "Còn hàng", + "outOfStock": "Hết hàng", + "category": "Danh mục", + "allCategories": "Tất cả danh mục", + "addToCart": "Thêm vào giỏ", + "cart": "Giỏ hàng", + "cartEmpty": "Giỏ hàng trống", + "cartItemsCount": "{count} sản phẩm", + "@cartItemsCount": { + "description": "Number of items in cart", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + "removeFromCart": "Xóa khỏi giỏ", + "clearCart": "Xóa giỏ hàng", + "clearCartConfirm": "Bạn có chắc chắn muốn xóa tất cả sản phẩm trong giỏ hàng?", + + "checkout": "Thanh toán", + "subtotal": "Tạm tính", + "discount": "Giảm giá", + "shipping": "Phí vận chuyển", + "total": "Tổng cộng", + "placeOrder": "Đặt hàng", + "orderPlaced": "Đơn hàng đã được đặt", + "orderSuccess": "Đặt hàng thành công", + "orders": "Đơn hàng", + "myOrders": "Đơn hàng của tôi", + "orderNumber": "Số đơn hàng", + "orderDate": "Ngày đặt", + "orderStatus": "Trạng thái đơn hàng", + "orderDetails": "Chi tiết đơn hàng", + "trackOrder": "Theo dõi đơn hàng", + "reorder": "Đặt lại", + "paymentMethod": "Phương thức thanh toán", + "cashOnDelivery": "Thanh toán khi nhận hàng", + "bankTransfer": "Chuyển khoản ngân hàng", + "creditCard": "Thẻ tín dụng", + "eWallet": "Ví điện tử", + "deliveryAddress": "Địa chỉ giao hàng", + "estimatedDelivery": "Dự kiến giao hàng", + "payments": "Thanh toán", + "paymentId": "Mã thanh toán", + "paymentStatus": "Trạng thái thanh toán", + + "projects": "Công trình", + "myProjects": "Công trình của tôi", + "createProject": "Tạo công trình", + "projectName": "Tên công trình", + "projectCode": "Mã công trình", + "projectType": "Loại công trình", + "residential": "Dân dụng", + "commercial": "Thương mại", + "industrial": "Công nghiệp", + "client": "Khách hàng", + "clientName": "Tên khách hàng", + "clientPhone": "SĐT khách hàng", + "location": "Vị trí", + "startDate": "Ngày bắt đầu", + "endDate": "Ngày kết thúc", + "progress": "Tiến độ", + "budget": "Ngân sách", + "description": "Mô tả", + "notes": "Ghi chú", + "quotes": "Báo giá", + "createQuote": "Tạo báo giá", + "quoteNumber": "Số báo giá", + "quoteDate": "Ngày báo giá", + "validity": "Hiệu lực", + "convertToOrder": "Chuyển thành đơn hàng", + "duplicate": "Nhân bản", + + "profile": "Hồ sơ", + "editProfile": "Chỉnh sửa hồ sơ", + "avatar": "Ảnh đại diện", + "uploadAvatar": "Tải lên ảnh đại diện", + "changePassword": "Đổi mật khẩu", + "passwordChanged": "Mật khẩu đã được thay đổi", + "addresses": "Địa chỉ", + "myAddresses": "Địa chỉ của tôi", + "addAddress": "Thêm địa chỉ", + "editAddress": "Sửa địa chỉ", + "deleteAddress": "Xóa địa chỉ", + "deleteAddressConfirm": "Bạn có chắc chắn muốn xóa địa chỉ này?", + "setAsDefault": "Đặt làm mặc định", + "defaultAddress": "Địa chỉ mặc định", + "homeAddress": "Nhà riêng", + "officeAddress": "Văn phòng", + "settings": "Cài đặt", + "notifications": "Thông báo", + "notificationSettings": "Cài đặt thông báo", + "language": "Ngôn ngữ", + "theme": "Giao diện", + "lightMode": "Sáng", + "darkMode": "Tối", + "systemMode": "Theo hệ thống", + + "promotions": "Khuyến mãi", + "promotion": "Chương trình khuyến mãi", + "activePromotions": "Khuyến mãi đang diễn ra", + "upcomingPromotions": "Khuyến mãi sắp diễn ra", + "expiredPromotions": "Khuyến mãi đã kết thúc", + "claimPromotion": "Nhận ưu đãi", + "termsAndConditions": "Điều khoản & Điều kiện", + + "chat": "Trò chuyện", + "chatSupport": "Hỗ trợ trực tuyến", + "sendMessage": "Gửi tin nhắn", + "typeMessage": "Nhập tin nhắn...", + "typingIndicator": "đang nhập...", + "attachFile": "Đính kèm tệp", + "supportAgent": "Nhân viên hỗ trợ", + + "fieldRequired": "Trường này là bắt buộc", + "invalidPhone": "Số điện thoại không hợp lệ", + "invalidEmail": "Email không hợp lệ", + "invalidOTP": "Mã OTP không hợp lệ", + "passwordTooShort": "Mật khẩu phải có ít nhất 8 ký tự", + "passwordsNotMatch": "Mật khẩu không khớp", + "passwordRequirements": "Mật khẩu phải có ít nhất 8 ký tự, bao gồm chữ hoa, chữ thường, số và ký tự đặc biệt", + "invalidAmount": "Số tiền không hợp lệ", + "insufficientPoints": "Không đủ điểm để đổi quà", + + "error": "Lỗi", + "errorOccurred": "Đã xảy ra lỗi", + "networkError": "Lỗi kết nối mạng. Vui lòng kiểm tra kết nối internet của bạn.", + "serverError": "Lỗi máy chủ. Vui lòng thử lại sau.", + "sessionExpired": "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.", + "notFound": "Không tìm thấy", + "unauthorized": "Không có quyền truy cập", + "tryAgain": "Thử lại", + "contactSupport": "Liên hệ hỗ trợ", + + "success": "Thành công", + "savedSuccessfully": "Đã lưu thành công", + "updatedSuccessfully": "Đã cập nhật thành công", + "deletedSuccessfully": "Đã xóa thành công", + "sentSuccessfully": "Đã gửi thành công", + "redeemSuccessful": "Đổi quà thành công", + "giftCode": "Mã quà tặng", + + "loading": "Đang tải...", + "loadingData": "Đang tải dữ liệu...", + "processing": "Đang xử lý...", + "pleaseWait": "Vui lòng đợi...", + + "noData": "Không có dữ liệu", + "noResults": "Không có kết quả", + "noProductsFound": "Không tìm thấy sản phẩm", + "noOrdersYet": "Chưa có đơn hàng nào", + "noProjectsYet": "Chưa có công trình nào", + "noNotifications": "Không có thông báo", + "noGiftsYet": "Chưa có quà tặng nào", + "startShopping": "Bắt đầu mua sắm", + "createFirstProject": "Tạo công trình đầu tiên", + + "today": "Hôm nay", + "yesterday": "Hôm qua", + "thisWeek": "Tuần này", + "thisMonth": "Tháng này", + "all": "Tất cả", + "dateRange": "Khoảng thời gian", + "from": "Từ", + "to": "Đến", + "date": "Ngày", + "time": "Giờ", + + "version": "Phiên bản", + "appVersion": "Phiên bản ứng dụng", + "help": "Trợ giúp", + "helpCenter": "Trung tâm trợ giúp", + "aboutUs": "Về chúng tôi", + "privacyPolicy": "Chính sách bảo mật", + "termsOfService": "Điều khoản sử dụng", + "rateApp": "Đánh giá ứng dụng", + "feedback": "Phản hồi", + "sendFeedback": "Gửi phản hồi", + "unsavedChanges": "Có thay đổi chưa được lưu", + "unsavedChangesMessage": "Bạn có muốn lưu các thay đổi trước khi thoát?", + + "welcome": "Chào mừng", + "welcomeBack": "Chào mừng trở lại", + "welcomeTo": "Chào mừng đến với {appName}", + "@welcomeTo": { + "description": "Welcome message with app name", + "placeholders": { + "appName": { + "type": "String", + "example": "Worker App" + } + } + }, + + "itemsInCart": "{count, plural, =0{Không có sản phẩm} =1{1 sản phẩm} other{{count} sản phẩm}}", + "@itemsInCart": { + "description": "Number of items in cart with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + + "ordersCount": "{count, plural, =0{Không có đơn hàng} =1{1 đơn hàng} other{{count} đơn hàng}}", + "@ordersCount": { + "description": "Number of orders with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "5" + } + } + }, + + "projectsCount": "{count, plural, =0{Không có công trình} =1{1 công trình} other{{count} công trình}}", + "@projectsCount": { + "description": "Number of projects with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "3" + } + } + }, + + "daysRemaining": "{count, plural, =0{Hôm nay} =1{Còn 1 ngày} other{Còn {count} ngày}}", + "@daysRemaining": { + "description": "Days remaining with pluralization", + "placeholders": { + "count": { + "type": "int", + "example": "7" + } + } + }, + + "formatCurrency": "{amount} ₫", + "@formatCurrency": { + "description": "Format currency in Vietnamese Dong", + "placeholders": { + "amount": { + "type": "String", + "example": "1.000.000" + } + } + }, + + "formatDate": "{day}/{month}/{year}", + "@formatDate": { + "description": "Date format DD/MM/YYYY", + "placeholders": { + "day": { + "type": "String" + }, + "month": { + "type": "String" + }, + "year": { + "type": "String" + } + } + }, + + "formatDateTime": "{day}/{month}/{year} lúc {hour}:{minute}", + "@formatDateTime": { + "description": "DateTime format DD/MM/YYYY at HH:mm", + "placeholders": { + "day": {"type": "String"}, + "month": {"type": "String"}, + "year": {"type": "String"}, + "hour": {"type": "String"}, + "minute": {"type": "String"} + } + }, + + "memberSince": "Thành viên từ {date}", + "@memberSince": { + "description": "Member since date", + "placeholders": { + "date": { + "type": "String", + "example": "01/2024" + } + } + }, + + "validUntil": "Có hiệu lực đến {date}", + "@validUntil": { + "description": "Valid until date", + "placeholders": { + "date": { + "type": "String", + "example": "31/12/2024" + } + } + }, + + "used": "Đã sử dụng", + "unused": "Chưa sử dụng", + "available": "Có sẵn", + "unavailable": "Không có sẵn", + "validFrom": "Có hiệu lực từ", + "validTo": "Có hiệu lực đến", + "usageInstructions": "Hướng dẫn sử dụng", + "useNow": "Sử dụng ngay", + + "scanQRCode": "Quét mã QR", + "scanBarcode": "Quét mã vạch", + "qrCodeScanner": "Quét mã QR", + "memberId": "Mã thành viên", + "showQRCode": "Hiển thị mã QR", + + "tier": "Hạng", + "tierBenefits": "Quyền lợi hạng thành viên", + "pointsMultiplier": "Hệ số điểm", + "multiplierX": "x{multiplier}", + "@multiplierX": { + "description": "Points multiplier display", + "placeholders": { + "multiplier": { + "type": "String", + "example": "1.5" + } + } + }, + "specialOffers": "Ưu đãi đặc biệt", + "exclusiveDiscounts": "Giảm giá độc quyền", + "prioritySupport": "Hỗ trợ ưu tiên", + "earlyAccess": "Truy cập sớm", + "birthdayGift": "Quà sinh nhật", + + "transactionType": "Loại giao dịch", + "earnPoints": "Tích điểm", + "redeemPoints": "Đổi điểm", + "bonusPoints": "Điểm thưởng", + "refundPoints": "Hoàn điểm", + "expiredPoints": "Điểm hết hạn", + "transferPoints": "Chuyển điểm", + "pointsExpiry": "Điểm hết hạn", + "pointsWillExpireOn": "Điểm sẽ hết hạn vào {date}", + "@pointsWillExpireOn": { + "description": "Points expiration date", + "placeholders": { + "date": { + "type": "String", + "example": "31/12/2024" + } + } + }, + "pointsExpiringSoon": "{points} điểm sắp hết hạn", + "@pointsExpiringSoon": { + "description": "Points expiring soon warning", + "placeholders": { + "points": { + "type": "int", + "example": "100" + } + } + }, + + "newBalance": "Số dư mới", + "previousBalance": "Số dư trước đó", + "balanceAfter": "Số dư sau giao dịch", + "disputeTransaction": "Khiếu nại giao dịch", + "disputeReason": "Lý do khiếu nại", + "disputeSubmitted": "Khiếu nại đã được gửi", + + "rewardCategory": "Danh mục quà tặng", + "vouchers": "Phiếu quà tặng", + "productRewards": "Quà tặng sản phẩm", + "services": "Dịch vụ", + "experiences": "Trải nghiệm", + "pointsCost": "Chi phí điểm", + "pointsRequired": "Yêu cầu {points} điểm", + "@pointsRequired": { + "description": "Points required for reward", + "placeholders": { + "points": { + "type": "int", + "example": "500" + } + } + }, + "expiryDate": "Ngày hết hạn", + "expiresOn": "Hết hạn vào {date}", + "@expiresOn": { + "description": "Expiration date", + "placeholders": { + "date": { + "type": "String", + "example": "31/12/2024" + } + } + }, + "redeemConfirm": "Xác nhận đổi quà", + "redeemConfirmMessage": "Bạn có chắc chắn muốn đổi {points} điểm để nhận {reward}?", + "@redeemConfirmMessage": { + "description": "Redeem confirmation message", + "placeholders": { + "points": { + "type": "int", + "example": "500" + }, + "reward": { + "type": "String", + "example": "Gift Voucher" + } + } + }, + + "giftStatus": "Trạng thái quà", + "activeGifts": "Quà đang dùng", + "usedGifts": "Quà đã dùng", + "expiredGifts": "Quà hết hạn", + "giftDetails": "Chi tiết quà tặng", + "howToUse": "Cách sử dụng", + + "referralInvite": "Mời bạn bè", + "referralReward": "Phần thưởng giới thiệu", + "referralSuccess": "Giới thiệu thành công", + "friendsReferred": "Bạn bè đã giới thiệu", + "pointsEarned": "Điểm đã kiếm", + "referralSteps": "Cách thức giới thiệu", + "step1": "Bước 1", + "step2": "Bước 2", + "step3": "Bước 3", + "shareYourCode": "Chia sẻ mã của bạn", + "friendRegisters": "Bạn bè đăng ký", + "bothGetRewards": "Cả hai nhận thưởng", + "inviteFriends": "Mời bạn bè", + + "sku": "SKU", + "brand": "Thương hiệu", + "model": "Mẫu", + "specification": "Thông số kỹ thuật", + "specifications": "Chi tiết kỹ thuật", + "material": "Chất liệu", + "size": "Kích thước", + "color": "Màu sắc", + "weight": "Trọng lượng", + "dimensions": "Kích thước", + "availability": "Tình trạng", + "addedToCart": "Đã thêm vào giỏ hàng", + "productDetails": "Chi tiết sản phẩm", + "relatedProducts": "Sản phẩm liên quan", + "recommended": "Đề xuất", + "newArrival": "Hàng mới về", + "bestSeller": "Bán chạy nhất", + "onSale": "Đang giảm giá", + "limitedStock": "Số lượng có hạn", + "lowStock": "Sắp hết hàng", + + "updateQuantity": "Cập nhật số lượng", + "itemRemoved": "Đã xóa sản phẩm", + "cartUpdated": "Giỏ hàng đã được cập nhật", + "proceedToCheckout": "Tiến hành thanh toán", + "continueShopping": "Tiếp tục mua sắm", + "emptyCart": "Giỏ hàng trống", + "emptyCartMessage": "Bạn chưa có sản phẩm nào trong giỏ hàng", + + "selectAddress": "Chọn địa chỉ", + "selectPaymentMethod": "Chọn phương thức thanh toán", + "orderSummary": "Tóm tắt đơn hàng", + "orderConfirmation": "Xác nhận đơn hàng", + "orderSuccessMessage": "Đơn hàng của bạn đã được đặt thành công!", + "orderNumberIs": "Số đơn hàng: {orderNumber}", + "@orderNumberIs": { + "description": "Order number display", + "placeholders": { + "orderNumber": { + "type": "String", + "example": "ORD-2024-001" + } + } + }, + "estimatedDeliveryDate": "Dự kiến giao hàng: {date}", + "@estimatedDeliveryDate": { + "description": "Estimated delivery date", + "placeholders": { + "date": { + "type": "String", + "example": "25/12/2024" + } + } + }, + "viewOrder": "Xem đơn hàng", + "backToHome": "Về trang chủ", + + "allOrders": "Tất cả đơn hàng", + "pendingOrders": "Chờ xử lý", + "processingOrders": "Đang xử lý", + "shippingOrders": "Đang giao", + "completedOrders": "Hoàn thành", + "cancelledOrders": "Đã hủy", + "cancelOrder": "Hủy đơn hàng", + "cancelOrderConfirm": "Bạn có chắc chắn muốn hủy đơn hàng này?", + "cancelReason": "Lý do hủy", + "orderCancelled": "Đơn hàng đã được hủy", + "orderTimeline": "Lịch sử đơn hàng", + "orderPlacedAt": "Đơn hàng đã đặt lúc", + "orderProcessedAt": "Đơn hàng đã xử lý lúc", + "orderShippedAt": "Đơn hàng đã giao lúc", + "orderDeliveredAt": "Đơn hàng đã nhận lúc", + "trackingNumber": "Mã vận đơn", + "shippingCarrier": "Đơn vị vận chuyển", + + "allProjects": "Tất cả công trình", + "planningProjects": "Đang lập kế hoạch", + "inProgressProjects": "Đang thực hiện", + "completedProjects": "Đã hoàn thành", + "projectDetails": "Chi tiết công trình", + "projectStatus": "Trạng thái công trình", + "updateProgress": "Cập nhật tiến độ", + "progressUpdated": "Tiến độ đã được cập nhật", + "projectCompleted": "Công trình đã hoàn thành", + "completeProject": "Hoàn thành công trình", + "completeProjectConfirm": "Bạn có chắc chắn muốn đánh dấu công trình này là hoàn thành?", + "deleteProject": "Xóa công trình", + "deleteProjectConfirm": "Bạn có chắc chắn muốn xóa công trình này?", + "projectPhotos": "Hình ảnh công trình", + "addPhotos": "Thêm hình ảnh", + "projectDocuments": "Tài liệu công trình", + "uploadDocument": "Tải lên tài liệu", + + "allQuotes": "Tất cả báo giá", + "draftQuotes": "Bản nháp", + "sentQuotes": "Đã gửi", + "acceptedQuotes": "Đã chấp nhận", + "rejectedQuotes": "Đã từ chối", + "expiredQuotes": "Hết hạn", + "quoteDetails": "Chi tiết báo giá", + "sendQuote": "Gửi báo giá", + "sendQuoteConfirm": "Bạn có chắc chắn muốn gửi báo giá này cho khách hàng?", + "quoteSent": "Báo giá đã được gửi", + "acceptQuote": "Chấp nhận báo giá", + "rejectQuote": "Từ chối báo giá", + "deleteQuote": "Xóa báo giá", + "deleteQuoteConfirm": "Bạn có chắc chắn muốn xóa báo giá này?", + "quoteItems": "Các hạng mục", + "addItem": "Thêm hạng mục", + "editItem": "Sửa hạng mục", + "removeItem": "Xóa hạng mục", + + "recipient": "Người nhận", + "recipientName": "Tên người nhận", + "recipientPhone": "SĐT người nhận", + "addressType": "Loại địa chỉ", + "addressLabel": "Nhãn địa chỉ", + "setDefault": "Đặt làm mặc định", + "defaultLabel": "Mặc định", + "addressSaved": "Địa chỉ đã được lưu", + + "currentPasswordRequired": "Vui lòng nhập mật khẩu hiện tại", + "newPasswordRequired": "Vui lòng nhập mật khẩu mới", + "confirmPasswordRequired": "Vui lòng xác nhận mật khẩu mới", + "incorrectPassword": "Mật khẩu không chính xác", + "passwordStrength": "Độ mạnh mật khẩu", + "weak": "Yếu", + "medium": "Trung bình", + "strong": "Mạnh", + "veryStrong": "Rất mạnh", + "passwordRequirement1": "Ít nhất 8 ký tự", + "passwordRequirement2": "Có chữ hoa", + "passwordRequirement3": "Có chữ thường", + "passwordRequirement4": "Có số", + "passwordRequirement5": "Có ký tự đặc biệt", + + "uploadPhoto": "Tải lên ảnh", + "takePhoto": "Chụp ảnh", + "chooseFromGallery": "Chọn từ thư viện", + "removePhoto": "Xóa ảnh", + "cropPhoto": "Cắt ảnh", + "photoUploaded": "Ảnh đã được tải lên", + + "enableNotifications": "Bật thông báo", + "disableNotifications": "Tắt thông báo", + "orderNotifications": "Thông báo đơn hàng", + "promotionNotifications": "Thông báo khuyến mãi", + "systemNotifications": "Thông báo hệ thống", + "chatNotifications": "Thông báo trò chuyện", + "pushNotifications": "Thông báo đẩy", + "emailNotifications": "Thông báo email", + "smsNotifications": "Thông báo SMS", + + "vietnamese": "Tiếng Việt", + "english": "Tiếng Anh", + "selectLanguage": "Chọn ngôn ngữ", + "languageChanged": "Ngôn ngữ đã được thay đổi", + + "selectTheme": "Chọn giao diện", + "themeChanged": "Giao diện đã được thay đổi", + "autoTheme": "Tự động", + + "allNotifications": "Tất cả", + "orderNotification": "Đơn hàng", + "systemNotification": "Hệ thống", + "promotionNotification": "Khuyến mãi", + "markAsRead": "Đánh dấu đã đọc", + "markAllAsRead": "Đánh dấu tất cả đã đọc", + "deleteNotification": "Xóa thông báo", + "clearNotifications": "Xóa tất cả thông báo", + "clearNotificationsConfirm": "Bạn có chắc chắn muốn xóa tất cả thông báo?", + "notificationCleared": "Thông báo đã được xóa", + "unreadNotifications": "{count} thông báo chưa đọc", + "@unreadNotifications": { + "description": "Unread notifications count", + "placeholders": { + "count": { + "type": "int", + "example": "5" + } + } + }, + + "online": "Trực tuyến", + "offline": "Ngoại tuyến", + "away": "Vắng mặt", + "busy": "Bận", + "lastSeenAt": "Hoạt động lần cuối {time}", + "@lastSeenAt": { + "description": "Last seen timestamp", + "placeholders": { + "time": { + "type": "String", + "example": "10 phút trước" + } + } + }, + "messageRead": "Đã đọc", + "messageDelivered": "Đã gửi", + "messageSent": "Đã gửi", + "messageFailed": "Gửi thất bại", + "retryMessage": "Gửi lại", + "deleteMessage": "Xóa tin nhắn", + "deleteMessageConfirm": "Bạn có chắc chắn muốn xóa tin nhắn này?", + "messageDeleted": "Tin nhắn đã được xóa", + + "filterBy": "Lọc theo", + "sortBy": "Sắp xếp theo", + "priceAscending": "Giá tăng dần", + "priceDescending": "Giá giảm dần", + "nameAscending": "Tên A-Z", + "nameDescending": "Tên Z-A", + "dateAscending": "Cũ nhất", + "dateDescending": "Mới nhất", + "popularityDescending": "Phổ biến nhất", + "applyFilters": "Áp dụng bộ lọc", + "clearFilters": "Xóa bộ lọc", + "filterApplied": "Đã áp dụng bộ lọc", + "noFilterApplied": "Chưa có bộ lọc nào", + + "connectionError": "Lỗi kết nối", + "noInternetConnection": "Không có kết nối Internet", + "checkConnection": "Kiểm tra kết nối", + "retryConnection": "Thử kết nối lại", + "offlineMode": "Chế độ ngoại tuyến", + "syncData": "Đồng bộ dữ liệu", + "syncInProgress": "Đang đồng bộ...", + "syncCompleted": "Đồng bộ hoàn tất", + "syncFailed": "Đồng bộ thất bại", + "lastSyncAt": "Đồng bộ lần cuối: {time}", + "@lastSyncAt": { + "description": "Last sync timestamp", + "placeholders": { + "time": { + "type": "String", + "example": "5 phút trước" + } + } + }, + + "minutesAgo": "{minutes} phút trước", + "@minutesAgo": { + "placeholders": { + "minutes": {"type": "int"} + } + }, + "hoursAgo": "{hours} giờ trước", + "@hoursAgo": { + "placeholders": { + "hours": {"type": "int"} + } + }, + "daysAgo": "{days} ngày trước", + "@daysAgo": { + "placeholders": { + "days": {"type": "int"} + } + }, + "weeksAgo": "{weeks} tuần trước", + "@weeksAgo": { + "placeholders": { + "weeks": {"type": "int"} + } + }, + "monthsAgo": "{months} tháng trước", + "@monthsAgo": { + "placeholders": { + "months": {"type": "int"} + } + }, + "yearsAgo": "{years} năm trước", + "@yearsAgo": { + "placeholders": { + "years": {"type": "int"} + } + }, + "justNow": "Vừa xong", + + "comingSoon": "Sắp ra mắt", + "underMaintenance": "Đang bảo trì", + "featureNotAvailable": "Tính năng chưa khả dụng", + "pageNotFound": "Không tìm thấy trang", + "goToHomePage": "Về trang chủ" +} diff --git a/lib/main.dart b/lib/main.dart index 7b7f5b6..6536c2e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,122 +1,267 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -void main() { - runApp(const MyApp()); +import 'package:worker/app.dart'; +import 'package:worker/core/database/hive_initializer.dart'; + +/// Main entry point of the Worker Mobile App +/// +/// Initializes core dependencies: +/// - Hive database with adapters and boxes +/// - SharedPreferences for simple key-value storage +/// - Riverpod ProviderScope for state management +/// - Error handling boundaries +/// - System UI customization +void main() async { + // Ensure Flutter is initialized before async operations + WidgetsFlutterBinding.ensureInitialized(); + + // Set preferred device orientations + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + // Initialize app with error handling + await _initializeApp(); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +/// Initialize all app dependencies with comprehensive error handling +Future _initializeApp() async { + // Set up error handlers before anything else + _setupErrorHandlers(); - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + try { + // Initialize core dependencies in parallel for faster startup + await Future.wait([ + _initializeHive(), + _initializeSharedPreferences(), + ]); + + // Run the app with Riverpod ProviderScope + runApp( + const ProviderScope( + child: WorkerApp(), ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), ); + } catch (error, stackTrace) { + // Critical initialization error - show error screen + debugPrint('Failed to initialize app: $error'); + debugPrint('StackTrace: $stackTrace'); + + // Run minimal error app + runApp(_buildErrorApp(error, stackTrace)); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +/// Initialize Hive database +/// +/// Sets up local database with: +/// - Type adapters for all models +/// - All required boxes (user, cart, products, etc.) +/// - Cache cleanup for expired data +/// - Encryption for sensitive data (in production) +Future _initializeHive() async { + try { + debugPrint('Initializing Hive database...'); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. + await HiveInitializer.initialize( + enableEncryption: kReleaseMode, // Enable encryption in release builds + verbose: kDebugMode, // Verbose logging in debug mode + ); - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); + debugPrint('Hive database initialized successfully'); + } catch (error, stackTrace) { + debugPrint('Failed to initialize Hive: $error'); + debugPrint('StackTrace: $stackTrace'); + rethrow; + } } -class _MyHomePageState extends State { - int _counter = 0; +/// Initialize SharedPreferences +/// +/// Used for simple key-value storage like: +/// - Last sync timestamp +/// - User preferences (language, theme) +/// - App settings +/// - Feature flags +Future _initializeSharedPreferences() async { + try { + debugPrint('Initializing SharedPreferences...'); - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + // Pre-initialize SharedPreferences instance + await SharedPreferences.getInstance(); + + debugPrint('SharedPreferences initialized successfully'); + } catch (error, stackTrace) { + debugPrint('Failed to initialize SharedPreferences: $error'); + debugPrint('StackTrace: $stackTrace'); + rethrow; } +} - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, +/// Set up global error handlers +/// +/// Captures and logs all Flutter framework errors and uncaught exceptions +void _setupErrorHandlers() { + // Handle Flutter framework errors + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + + // Log to console in debug mode + if (kDebugMode) { + debugPrint('Flutter Error: ${details.exceptionAsString()}'); + debugPrint('StackTrace: ${details.stack}'); + } + + // In production, you would send to crash analytics service + // Example: FirebaseCrashlytics.instance.recordFlutterError(details); + }; + + // Handle errors outside of Flutter framework + PlatformDispatcher.instance.onError = (error, stackTrace) { + if (kDebugMode) { + debugPrint('Platform Error: $error'); + debugPrint('StackTrace: $stackTrace'); + } + + // In production, you would send to crash analytics service + // Example: FirebaseCrashlytics.instance.recordError(error, stackTrace); + + return true; // Return true to indicate error was handled + }; + + // Handle zone errors (async errors not caught by Flutter) + runZonedGuarded( + () { + // App will run in this zone + }, + (error, stackTrace) { + if (kDebugMode) { + debugPrint('Zone Error: $error'); + debugPrint('StackTrace: $stackTrace'); + } + + // In production, you would send to crash analytics service + // Example: FirebaseCrashlytics.instance.recordError(error, stackTrace); + }, + ); +} + +/// Build minimal error app when initialization fails +/// +/// Shows a user-friendly error screen instead of crashing +Widget _buildErrorApp(Object error, StackTrace stackTrace) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Error icon + const Icon( + Icons.error_outline, + size: 80, + color: Color(0xFFDC3545), + ), + const SizedBox(height: 24), + + // Error title + const Text( + 'Không thể khởi động ứng dụng', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF212529), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Error message + const Text( + 'Đã xảy ra lỗi khi khởi động ứng dụng. ' + 'Vui lòng thử lại sau hoặc liên hệ hỗ trợ.', + style: TextStyle( + fontSize: 16, + color: Color(0xFF6C757D), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + + // Error details (debug mode only) + if (kDebugMode) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFFF3CD), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFFFECB5), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Debug Information:', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Color(0xFF856404), + ), + ), + const SizedBox(height: 8), + Text( + error.toString(), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF856404), + fontFamily: 'monospace', + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + + // Restart button + ElevatedButton.icon( + onPressed: () { + // Restart app + _initializeApp(); + }, + icon: const Icon(Icons.refresh), + label: const Text('Thử lại'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF005B9A), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], ), - ], + ), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } + ), + ); } diff --git a/lib/shared/widgets/custom_app_bar.dart b/lib/shared/widgets/custom_app_bar.dart new file mode 100644 index 0000000..1f4af4a --- /dev/null +++ b/lib/shared/widgets/custom_app_bar.dart @@ -0,0 +1,145 @@ +/// Custom App Bar Widget +/// +/// Reusable app bar with consistent styling across the app +library; + +import 'package:flutter/material.dart'; +import '../../core/constants/ui_constants.dart'; + +/// Custom app bar with consistent styling +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List? actions; + final Widget? leading; + final bool centerTitle; + final Color? backgroundColor; + final Color? foregroundColor; + final double elevation; + final PreferredSizeWidget? bottom; + final bool automaticallyImplyLeading; + + const CustomAppBar({ + super.key, + required this.title, + this.actions, + this.leading, + this.centerTitle = true, + this.backgroundColor, + this.foregroundColor, + this.elevation = AppElevation.none, + this.bottom, + this.automaticallyImplyLeading = true, + }); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(title), + actions: actions, + leading: leading, + centerTitle: centerTitle, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + elevation: elevation, + bottom: bottom, + automaticallyImplyLeading: automaticallyImplyLeading, + ); + } + + @override + Size get preferredSize => Size.fromHeight( + AppBarSpecs.height + (bottom?.preferredSize.height ?? 0), + ); +} + +/// Transparent app bar for overlay scenarios +class TransparentAppBar extends StatelessWidget implements PreferredSizeWidget { + final String? title; + final List? actions; + final Widget? leading; + final bool centerTitle; + final Color? foregroundColor; + + const TransparentAppBar({ + super.key, + this.title, + this.actions, + this.leading, + this.centerTitle = true, + this.foregroundColor, + }); + + @override + Widget build(BuildContext context) { + return AppBar( + title: title != null ? Text(title!) : null, + actions: actions, + leading: leading, + centerTitle: centerTitle, + backgroundColor: Colors.transparent, + foregroundColor: foregroundColor ?? Colors.white, + elevation: 0, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(AppBarSpecs.height); +} + +/// Search app bar with search field +class SearchAppBar extends StatelessWidget implements PreferredSizeWidget { + final String hintText; + final ValueChanged? onChanged; + final ValueChanged? onSubmitted; + final VoidCallback? onClear; + final TextEditingController? controller; + final bool autofocus; + final Widget? leading; + + const SearchAppBar({ + super.key, + this.hintText = 'Tìm kiếm...', + this.onChanged, + this.onSubmitted, + this.onClear, + this.controller, + this.autofocus = false, + this.leading, + }); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: leading ?? + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).pop(), + ), + title: TextField( + controller: controller, + autofocus: autofocus, + onChanged: onChanged, + onSubmitted: onSubmitted, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)), + border: InputBorder.none, + suffixIcon: controller?.text.isNotEmpty ?? false + ? IconButton( + icon: const Icon(Icons.clear, color: Colors.white), + onPressed: () { + controller?.clear(); + onClear?.call(); + }, + ) + : null, + ), + ), + elevation: AppElevation.none, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(AppBarSpecs.height); +} diff --git a/lib/shared/widgets/date_picker_field.dart b/lib/shared/widgets/date_picker_field.dart new file mode 100644 index 0000000..74b68ba --- /dev/null +++ b/lib/shared/widgets/date_picker_field.dart @@ -0,0 +1,375 @@ +/// Date Picker Input Field +/// +/// Input field that opens a date picker dialog when tapped. +/// Displays dates in Vietnamese format (dd/MM/yyyy). +library; + +import 'package:flutter/material.dart'; +import '../../core/utils/formatters.dart'; +import '../../core/utils/validators.dart'; +import '../../core/constants/ui_constants.dart'; + +/// Date picker input field +class DatePickerField extends StatefulWidget { + final TextEditingController? controller; + final String? labelText; + final String? hintText; + final DateTime? initialDate; + final DateTime? firstDate; + final DateTime? lastDate; + final ValueChanged? onDateSelected; + final FormFieldValidator? validator; + final bool enabled; + final bool required; + final Widget? prefixIcon; + final Widget? suffixIcon; + final InputDecoration? decoration; + + const DatePickerField({ + super.key, + this.controller, + this.labelText, + this.hintText, + this.initialDate, + this.firstDate, + this.lastDate, + this.onDateSelected, + this.validator, + this.enabled = true, + this.required = true, + this.prefixIcon, + this.suffixIcon, + this.decoration, + }); + + @override + State createState() => _DatePickerFieldState(); +} + +class _DatePickerFieldState extends State { + late TextEditingController _controller; + bool _isControllerInternal = false; + DateTime? _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.initialDate; + + if (widget.controller == null) { + _controller = TextEditingController( + text: _selectedDate != null + ? DateFormatter.formatDate(_selectedDate!) + : '', + ); + _isControllerInternal = true; + } else { + _controller = widget.controller!; + } + } + + @override + void dispose() { + if (_isControllerInternal) { + _controller.dispose(); + } + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + if (!widget.enabled) return; + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now(), + firstDate: widget.firstDate ?? DateTime(1900), + lastDate: widget.lastDate ?? DateTime(2100), + locale: const Locale('vi', 'VN'), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Theme.of(context).primaryColor, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _controller.text = DateFormatter.formatDate(picked); + }); + widget.onDateSelected?.call(picked); + } + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + readOnly: true, + enabled: widget.enabled, + onTap: () => _selectDate(context), + decoration: widget.decoration ?? + InputDecoration( + labelText: widget.labelText ?? 'Ngày', + hintText: widget.hintText ?? 'dd/MM/yyyy', + prefixIcon: widget.prefixIcon ?? const Icon(Icons.calendar_today), + suffixIcon: widget.suffixIcon, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + validator: widget.validator ?? + (widget.required ? Validators.date : null), + ); + } +} + +/// Date range picker field +class DateRangePickerField extends StatefulWidget { + final String? labelText; + final String? hintText; + final DateTimeRange? initialRange; + final DateTime? firstDate; + final DateTime? lastDate; + final ValueChanged? onRangeSelected; + final bool enabled; + final Widget? prefixIcon; + + const DateRangePickerField({ + super.key, + this.labelText, + this.hintText, + this.initialRange, + this.firstDate, + this.lastDate, + this.onRangeSelected, + this.enabled = true, + this.prefixIcon, + }); + + @override + State createState() => _DateRangePickerFieldState(); +} + +class _DateRangePickerFieldState extends State { + late TextEditingController _controller; + DateTimeRange? _selectedRange; + + @override + void initState() { + super.initState(); + _selectedRange = widget.initialRange; + _controller = TextEditingController( + text: _selectedRange != null + ? '${DateFormatter.formatDate(_selectedRange!.start)} - ${DateFormatter.formatDate(_selectedRange!.end)}' + : '', + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _selectDateRange(BuildContext context) async { + if (!widget.enabled) return; + + final DateTimeRange? picked = await showDateRangePicker( + context: context, + initialDateRange: _selectedRange, + firstDate: widget.firstDate ?? DateTime(1900), + lastDate: widget.lastDate ?? DateTime(2100), + locale: const Locale('vi', 'VN'), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Theme.of(context).primaryColor, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedRange) { + setState(() { + _selectedRange = picked; + _controller.text = + '${DateFormatter.formatDate(picked.start)} - ${DateFormatter.formatDate(picked.end)}'; + }); + widget.onRangeSelected?.call(picked); + } + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + readOnly: true, + enabled: widget.enabled, + onTap: () => _selectDateRange(context), + decoration: InputDecoration( + labelText: widget.labelText ?? 'Khoảng thời gian', + hintText: widget.hintText ?? 'Chọn khoảng thời gian', + prefixIcon: widget.prefixIcon ?? const Icon(Icons.date_range), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + ); + } +} + +/// Date of birth picker field +class DateOfBirthField extends StatelessWidget { + final TextEditingController? controller; + final String? labelText; + final String? hintText; + final ValueChanged? onDateSelected; + final FormFieldValidator? validator; + final bool enabled; + final int minAge; + + const DateOfBirthField({ + super.key, + this.controller, + this.labelText, + this.hintText, + this.onDateSelected, + this.validator, + this.enabled = true, + this.minAge = 18, + }); + + @override + Widget build(BuildContext context) { + final now = DateTime.now(); + final maxDate = DateTime(now.year - minAge, now.month, now.day); + final minDate = DateTime(now.year - 100, now.month, now.day); + + return DatePickerField( + controller: controller, + labelText: labelText ?? 'Ngày sinh', + hintText: hintText ?? 'dd/MM/yyyy', + initialDate: maxDate, + firstDate: minDate, + lastDate: maxDate, + onDateSelected: onDateSelected, + validator: validator ?? (value) => Validators.age(value, minAge: minAge), + enabled: enabled, + prefixIcon: const Icon(Icons.cake), + ); + } +} + +/// Time picker field +class TimePickerField extends StatefulWidget { + final TextEditingController? controller; + final String? labelText; + final String? hintText; + final TimeOfDay? initialTime; + final ValueChanged? onTimeSelected; + final bool enabled; + final Widget? prefixIcon; + + const TimePickerField({ + super.key, + this.controller, + this.labelText, + this.hintText, + this.initialTime, + this.onTimeSelected, + this.enabled = true, + this.prefixIcon, + }); + + @override + State createState() => _TimePickerFieldState(); +} + +class _TimePickerFieldState extends State { + late TextEditingController _controller; + bool _isControllerInternal = false; + TimeOfDay? _selectedTime; + + @override + void initState() { + super.initState(); + _selectedTime = widget.initialTime; + + if (widget.controller == null) { + _controller = TextEditingController( + text: _selectedTime != null + ? '${_selectedTime!.hour.toString().padLeft(2, '0')}:${_selectedTime!.minute.toString().padLeft(2, '0')}' + : '', + ); + _isControllerInternal = true; + } else { + _controller = widget.controller!; + } + } + + @override + void dispose() { + if (_isControllerInternal) { + _controller.dispose(); + } + super.dispose(); + } + + Future _selectTime(BuildContext context) async { + if (!widget.enabled) return; + + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Theme.of(context).primaryColor, + ), + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + _controller.text = + '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; + }); + widget.onTimeSelected?.call(picked); + } + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + readOnly: true, + enabled: widget.enabled, + onTap: () => _selectTime(context), + decoration: InputDecoration( + labelText: widget.labelText ?? 'Thời gian', + hintText: widget.hintText ?? 'HH:mm', + prefixIcon: widget.prefixIcon ?? const Icon(Icons.access_time), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + ); + } +} diff --git a/lib/shared/widgets/gradient_card.dart b/lib/shared/widgets/gradient_card.dart new file mode 100644 index 0000000..bf8fca4 --- /dev/null +++ b/lib/shared/widgets/gradient_card.dart @@ -0,0 +1,270 @@ +/// Gradient Card Widget +/// +/// Reusable card with gradient background used for member cards +/// and other gradient-based UI elements. +library; + +import 'package:flutter/material.dart'; +import '../../core/constants/ui_constants.dart'; + +/// Card with gradient background +class GradientCard extends StatelessWidget { + final Widget child; + final Gradient gradient; + final double borderRadius; + final double elevation; + final EdgeInsets padding; + final double? width; + final double? height; + final VoidCallback? onTap; + final List? shadows; + + const GradientCard({ + super.key, + required this.child, + required this.gradient, + this.borderRadius = AppRadius.card, + this.elevation = AppElevation.card, + this.padding = const EdgeInsets.all(AppSpacing.md), + this.width, + this.height, + this.onTap, + this.shadows, + }); + + @override + Widget build(BuildContext context) { + final cardContent = Container( + width: width, + height: height, + padding: padding, + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(borderRadius), + boxShadow: shadows ?? + [ + BoxShadow( + color: Colors.black.withOpacity(0.1 * (elevation / 4)), + blurRadius: elevation, + offset: Offset(0, elevation / 2), + ), + ], + ), + child: child, + ); + + if (onTap != null) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(borderRadius), + child: cardContent, + ); + } + + return cardContent; + } +} + +/// Diamond tier gradient card +class DiamondGradientCard extends StatelessWidget { + final Widget child; + final double borderRadius; + final double elevation; + final EdgeInsets padding; + final double? width; + final double? height; + final VoidCallback? onTap; + + const DiamondGradientCard({ + super.key, + required this.child, + this.borderRadius = MemberCardSpecs.borderRadius, + this.elevation = MemberCardSpecs.elevation, + this.padding = MemberCardSpecs.padding, + this.width = MemberCardSpecs.width, + this.height = MemberCardSpecs.height, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GradientCard( + gradient: const LinearGradient( + colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: borderRadius, + elevation: elevation, + padding: padding, + width: width, + height: height, + onTap: onTap, + child: child, + ); + } +} + +/// Platinum tier gradient card +class PlatinumGradientCard extends StatelessWidget { + final Widget child; + final double borderRadius; + final double elevation; + final EdgeInsets padding; + final double? width; + final double? height; + final VoidCallback? onTap; + + const PlatinumGradientCard({ + super.key, + required this.child, + this.borderRadius = MemberCardSpecs.borderRadius, + this.elevation = MemberCardSpecs.elevation, + this.padding = MemberCardSpecs.padding, + this.width = MemberCardSpecs.width, + this.height = MemberCardSpecs.height, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GradientCard( + gradient: const LinearGradient( + colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: borderRadius, + elevation: elevation, + padding: padding, + width: width, + height: height, + onTap: onTap, + child: child, + ); + } +} + +/// Gold tier gradient card +class GoldGradientCard extends StatelessWidget { + final Widget child; + final double borderRadius; + final double elevation; + final EdgeInsets padding; + final double? width; + final double? height; + final VoidCallback? onTap; + + const GoldGradientCard({ + super.key, + required this.child, + this.borderRadius = MemberCardSpecs.borderRadius, + this.elevation = MemberCardSpecs.elevation, + this.padding = MemberCardSpecs.padding, + this.width = MemberCardSpecs.width, + this.height = MemberCardSpecs.height, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GradientCard( + gradient: const LinearGradient( + colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: borderRadius, + elevation: elevation, + padding: padding, + width: width, + height: height, + onTap: onTap, + child: child, + ); + } +} + +/// Animated gradient card with shimmer effect +class ShimmerGradientCard extends StatefulWidget { + final Widget child; + final Gradient gradient; + final double borderRadius; + final double elevation; + final EdgeInsets padding; + final double? width; + final double? height; + final Duration shimmerDuration; + + const ShimmerGradientCard({ + super.key, + required this.child, + required this.gradient, + this.borderRadius = AppRadius.card, + this.elevation = AppElevation.card, + this.padding = const EdgeInsets.all(AppSpacing.md), + this.width, + this.height, + this.shimmerDuration = AppDuration.shimmer, + }); + + @override + State createState() => _ShimmerGradientCardState(); +} + +class _ShimmerGradientCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.shimmerDuration, + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return ShaderMask( + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white.withOpacity(0.1), + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1), + ], + stops: [ + _controller.value - 0.3, + _controller.value, + _controller.value + 0.3, + ], + ).createShader(bounds); + }, + blendMode: BlendMode.srcATop, + child: GradientCard( + gradient: widget.gradient, + borderRadius: widget.borderRadius, + elevation: widget.elevation, + padding: widget.padding, + width: widget.width, + height: widget.height, + child: widget.child, + ), + ); + }, + ); + } +} diff --git a/lib/shared/widgets/price_display.dart b/lib/shared/widgets/price_display.dart new file mode 100644 index 0000000..fb80e21 --- /dev/null +++ b/lib/shared/widgets/price_display.dart @@ -0,0 +1,269 @@ +/// Price Display Widget +/// +/// Formats and displays prices in Vietnamese currency format +library; + +import 'package:flutter/material.dart'; +import '../../core/utils/formatters.dart'; + +/// Price display with Vietnamese currency formatting +class PriceDisplay extends StatelessWidget { + final double price; + final TextStyle? style; + final bool showSymbol; + final int decimalDigits; + final Color? color; + final FontWeight? fontWeight; + final double? fontSize; + + const PriceDisplay({ + super.key, + required this.price, + this.style, + this.showSymbol = true, + this.decimalDigits = 0, + this.color, + this.fontWeight, + this.fontSize, + }); + + @override + Widget build(BuildContext context) { + final formattedPrice = CurrencyFormatter.formatWithDecimals( + price, + decimalDigits: decimalDigits, + showSymbol: showSymbol, + ); + + return Text( + formattedPrice, + style: style ?? + TextStyle( + color: color, + fontWeight: fontWeight ?? FontWeight.w600, + fontSize: fontSize, + ), + ); + } +} + +/// Price display with sale price comparison +class SalePriceDisplay extends StatelessWidget { + final double originalPrice; + final double salePrice; + final TextStyle? originalPriceStyle; + final TextStyle? salePriceStyle; + final bool showSymbol; + final MainAxisAlignment alignment; + + const SalePriceDisplay({ + super.key, + required this.originalPrice, + required this.salePrice, + this.originalPriceStyle, + this.salePriceStyle, + this.showSymbol = true, + this.alignment = MainAxisAlignment.start, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: alignment, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + // Sale price (larger, prominent) + Text( + CurrencyFormatter.format(salePrice, showSymbol: showSymbol), + style: salePriceStyle ?? + const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(width: 8), + // Original price (smaller, strikethrough) + Text( + CurrencyFormatter.format(originalPrice, showSymbol: showSymbol), + style: originalPriceStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.grey[600], + decoration: TextDecoration.lineThrough, + ), + ), + ], + ); + } +} + +/// Price display with discount percentage badge +class PriceWithDiscount extends StatelessWidget { + final double originalPrice; + final double salePrice; + final bool showSymbol; + final TextStyle? salePriceStyle; + final TextStyle? originalPriceStyle; + + const PriceWithDiscount({ + super.key, + required this.originalPrice, + required this.salePrice, + this.showSymbol = true, + this.salePriceStyle, + this.originalPriceStyle, + }); + + double get discountPercentage { + return ((originalPrice - salePrice) / originalPrice * 100); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Sale price + Text( + CurrencyFormatter.format(salePrice, showSymbol: showSymbol), + style: salePriceStyle ?? + const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(width: 8), + // Discount badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '-${discountPercentage.toStringAsFixed(0)}%', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + // Original price + Text( + CurrencyFormatter.format(originalPrice, showSymbol: showSymbol), + style: originalPriceStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.grey[600], + decoration: TextDecoration.lineThrough, + ), + ), + ], + ); + } +} + +/// Compact price display for lists/grids +class CompactPriceDisplay extends StatelessWidget { + final double price; + final double? salePrice; + final bool showSymbol; + + const CompactPriceDisplay({ + super.key, + required this.price, + this.salePrice, + this.showSymbol = true, + }); + + @override + Widget build(BuildContext context) { + final bool isOnSale = salePrice != null && salePrice! < price; + + if (isOnSale) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + CurrencyFormatter.format(salePrice!, showSymbol: showSymbol), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + Text( + CurrencyFormatter.format(price, showSymbol: showSymbol), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + decoration: TextDecoration.lineThrough, + ), + ), + ], + ); + } + + return Text( + CurrencyFormatter.format(price, showSymbol: showSymbol), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ); + } +} + +/// Large price display for checkout/order summary +class LargePriceDisplay extends StatelessWidget { + final double price; + final String? label; + final bool showSymbol; + final Color? color; + + const LargePriceDisplay({ + super.key, + required this.price, + this.label, + this.showSymbol = true, + this.color, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (label != null) ...[ + Text( + label!, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + ], + Text( + CurrencyFormatter.format(price, showSymbol: showSymbol), + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: color ?? Theme.of(context).primaryColor, + ), + ), + ], + ); + } +} diff --git a/lib/shared/widgets/status_badge.dart b/lib/shared/widgets/status_badge.dart new file mode 100644 index 0000000..5576ac3 --- /dev/null +++ b/lib/shared/widgets/status_badge.dart @@ -0,0 +1,245 @@ +/// Status Badge Widget +/// +/// Displays status indicators with color-coded badges for orders, +/// projects, payments, and other status-based entities. +library; + +import 'package:flutter/material.dart'; +import '../../core/constants/ui_constants.dart'; +import '../../core/theme/colors.dart'; + +/// Status badge with color-coded indicators +class StatusBadge extends StatelessWidget { + final String label; + final Color color; + final Color? textColor; + final double borderRadius; + final EdgeInsets padding; + final double fontSize; + final FontWeight fontWeight; + + const StatusBadge({ + super.key, + required this.label, + required this.color, + this.textColor, + this.borderRadius = StatusBadgeSpecs.borderRadius, + this.padding = StatusBadgeSpecs.padding, + this.fontSize = StatusBadgeSpecs.fontSize, + this.fontWeight = StatusBadgeSpecs.fontWeight, + }); + + /// Order status badges + factory StatusBadge.orderPending() => const StatusBadge( + label: 'Chờ xử lý', + color: AppColors.info, + ); + + factory StatusBadge.orderProcessing() => const StatusBadge( + label: 'Đang xử lý', + color: AppColors.warning, + ); + + factory StatusBadge.orderShipping() => const StatusBadge( + label: 'Đang giao', + color: AppColors.lightBlue, + ); + + factory StatusBadge.orderCompleted() => const StatusBadge( + label: 'Hoàn thành', + color: AppColors.success, + ); + + factory StatusBadge.orderCancelled() => const StatusBadge( + label: 'Đã hủy', + color: AppColors.danger, + ); + + /// Payment status badges + factory StatusBadge.paymentPending() => const StatusBadge( + label: 'Chờ thanh toán', + color: AppColors.warning, + ); + + factory StatusBadge.paymentProcessing() => const StatusBadge( + label: 'Đang xử lý', + color: AppColors.info, + ); + + factory StatusBadge.paymentCompleted() => const StatusBadge( + label: 'Đã thanh toán', + color: AppColors.success, + ); + + factory StatusBadge.paymentFailed() => const StatusBadge( + label: 'Thất bại', + color: AppColors.danger, + ); + + /// Project status badges + factory StatusBadge.projectPlanning() => const StatusBadge( + label: 'Lập kế hoạch', + color: AppColors.info, + ); + + factory StatusBadge.projectInProgress() => const StatusBadge( + label: 'Đang thực hiện', + color: AppColors.warning, + ); + + factory StatusBadge.projectCompleted() => const StatusBadge( + label: 'Hoàn thành', + color: AppColors.success, + ); + + factory StatusBadge.projectOnHold() => const StatusBadge( + label: 'Tạm dừng', + color: AppColors.grey500, + ); + + /// Gift status badges + factory StatusBadge.giftActive() => const StatusBadge( + label: 'Còn hạn', + color: AppColors.success, + ); + + factory StatusBadge.giftUsed() => const StatusBadge( + label: 'Đã sử dụng', + color: AppColors.grey500, + ); + + factory StatusBadge.giftExpired() => const StatusBadge( + label: 'Hết hạn', + color: AppColors.danger, + ); + + /// Member tier badges + factory StatusBadge.tierDiamond() => const StatusBadge( + label: 'Kim Cương', + color: Color(0xFF4A00E0), + ); + + factory StatusBadge.tierPlatinum() => const StatusBadge( + label: 'Bạch Kim', + color: Color(0xFF7F8C8D), + ); + + factory StatusBadge.tierGold() => const StatusBadge( + label: 'Vàng', + color: Color(0xFFf7b733), + ); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Text( + label, + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: fontSize, + fontWeight: fontWeight, + ), + ), + ); + } +} + +/// Outlined status badge +class OutlinedStatusBadge extends StatelessWidget { + final String label; + final Color color; + final double borderRadius; + final EdgeInsets padding; + final double fontSize; + final FontWeight fontWeight; + final double borderWidth; + + const OutlinedStatusBadge({ + super.key, + required this.label, + required this.color, + this.borderRadius = StatusBadgeSpecs.borderRadius, + this.padding = StatusBadgeSpecs.padding, + this.fontSize = StatusBadgeSpecs.fontSize, + this.fontWeight = StatusBadgeSpecs.fontWeight, + this.borderWidth = 1.5, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + border: Border.all(color: color, width: borderWidth), + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Text( + label, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: fontWeight, + ), + ), + ); + } +} + +/// Status badge with icon +class IconStatusBadge extends StatelessWidget { + final String label; + final Color color; + final IconData icon; + final Color? textColor; + final double borderRadius; + final EdgeInsets padding; + final double fontSize; + final double iconSize; + + const IconStatusBadge({ + super.key, + required this.label, + required this.color, + required this.icon, + this.textColor, + this.borderRadius = StatusBadgeSpecs.borderRadius, + this.padding = StatusBadgeSpecs.padding, + this.fontSize = StatusBadgeSpecs.fontSize, + this.iconSize = 14.0, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: iconSize, + color: textColor ?? Colors.white, + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: fontSize, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/lib/shared/widgets/vietnamese_phone_field.dart b/lib/shared/widgets/vietnamese_phone_field.dart new file mode 100644 index 0000000..771bbd4 --- /dev/null +++ b/lib/shared/widgets/vietnamese_phone_field.dart @@ -0,0 +1,271 @@ +/// Vietnamese Phone Number Input Field +/// +/// Specialized input field for Vietnamese phone numbers with +/// auto-formatting and validation. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../core/utils/validators.dart'; +import '../../core/utils/formatters.dart'; +import '../../core/constants/ui_constants.dart'; + +/// Phone number input field with Vietnamese formatting +class VietnamesePhoneField extends StatefulWidget { + final TextEditingController? controller; + final String? labelText; + final String? hintText; + final String? initialValue; + final ValueChanged? onChanged; + final ValueChanged? onSubmitted; + final FormFieldValidator? validator; + final bool enabled; + final bool autoFocus; + final TextInputAction? textInputAction; + final FocusNode? focusNode; + final bool required; + final Widget? prefixIcon; + final Widget? suffixIcon; + + const VietnamesePhoneField({ + super.key, + this.controller, + this.labelText, + this.hintText, + this.initialValue, + this.onChanged, + this.onSubmitted, + this.validator, + this.enabled = true, + this.autoFocus = false, + this.textInputAction, + this.focusNode, + this.required = true, + this.prefixIcon, + this.suffixIcon, + }); + + @override + State createState() => _VietnamesePhoneFieldState(); +} + +class _VietnamesePhoneFieldState extends State { + late TextEditingController _controller; + bool _isControllerInternal = false; + + @override + void initState() { + super.initState(); + if (widget.controller == null) { + _controller = TextEditingController(text: widget.initialValue); + _isControllerInternal = true; + } else { + _controller = widget.controller!; + } + } + + @override + void dispose() { + if (_isControllerInternal) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + focusNode: widget.focusNode, + enabled: widget.enabled, + autofocus: widget.autoFocus, + keyboardType: TextInputType.phone, + textInputAction: widget.textInputAction ?? TextInputAction.next, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(11), + _PhoneNumberFormatter(), + ], + decoration: InputDecoration( + labelText: widget.labelText ?? 'Số điện thoại', + hintText: widget.hintText ?? '0xxx xxx xxx', + prefixIcon: widget.prefixIcon ?? + const Icon(Icons.phone), + suffixIcon: widget.suffixIcon, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + validator: widget.validator ?? + (widget.required ? Validators.phone : Validators.phoneOptional), + onChanged: widget.onChanged, + onFieldSubmitted: widget.onSubmitted, + ); + } +} + +/// Phone number text input formatter +class _PhoneNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final text = newValue.text; + + if (text.isEmpty) { + return newValue; + } + + // Format as: 0xxx xxx xxx + String formatted = text; + if (text.length > 4 && text.length <= 7) { + formatted = '${text.substring(0, 4)} ${text.substring(4)}'; + } else if (text.length > 7) { + formatted = + '${text.substring(0, 4)} ${text.substring(4, 7)} ${text.substring(7)}'; + } + + return TextEditingValue( + text: formatted, + selection: TextSelection.collapsed(offset: formatted.length), + ); + } +} + +/// Read-only phone display field +class PhoneDisplayField extends StatelessWidget { + final String phoneNumber; + final String? labelText; + final Widget? prefixIcon; + final VoidCallback? onTap; + + const PhoneDisplayField({ + super.key, + required this.phoneNumber, + this.labelText, + this.prefixIcon, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: PhoneFormatter.format(phoneNumber), + readOnly: true, + enabled: onTap != null, + onTap: onTap, + decoration: InputDecoration( + labelText: labelText ?? 'Số điện thoại', + prefixIcon: prefixIcon ?? const Icon(Icons.phone), + suffixIcon: onTap != null ? const Icon(Icons.edit) : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + ); + } +} + +/// Phone field with country code selector +class InternationalPhoneField extends StatefulWidget { + final TextEditingController? controller; + final String? labelText; + final String? hintText; + final ValueChanged? onChanged; + final FormFieldValidator? validator; + final bool enabled; + final String defaultCountryCode; + + const InternationalPhoneField({ + super.key, + this.controller, + this.labelText, + this.hintText, + this.onChanged, + this.validator, + this.enabled = true, + this.defaultCountryCode = '+84', + }); + + @override + State createState() => + _InternationalPhoneFieldState(); +} + +class _InternationalPhoneFieldState extends State { + late TextEditingController _controller; + late String _selectedCountryCode; + bool _isControllerInternal = false; + + @override + void initState() { + super.initState(); + _selectedCountryCode = widget.defaultCountryCode; + + if (widget.controller == null) { + _controller = TextEditingController(); + _isControllerInternal = true; + } else { + _controller = widget.controller!; + } + } + + @override + void dispose() { + if (_isControllerInternal) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + enabled: widget.enabled, + keyboardType: TextInputType.phone, + textInputAction: TextInputAction.next, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + labelText: widget.labelText ?? 'Số điện thoại', + hintText: widget.hintText ?? 'xxx xxx xxx', + prefixIcon: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: DropdownButton( + value: _selectedCountryCode, + underline: const SizedBox(), + items: const [ + DropdownMenuItem(value: '+84', child: Text('+84')), + DropdownMenuItem(value: '+1', child: Text('+1')), + DropdownMenuItem(value: '+86', child: Text('+86')), + ], + onChanged: widget.enabled + ? (value) { + if (value != null) { + setState(() { + _selectedCountryCode = value; + }); + widget.onChanged?.call('$value${_controller.text}'); + } + } + : null, + ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius), + ), + contentPadding: InputFieldSpecs.contentPadding, + ), + validator: widget.validator, + onChanged: (value) { + widget.onChanged?.call('$_selectedCountryCode$value'); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c1b51d3..8bd0df7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,54 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + url: "https://pub.dev" + source: hosted + version: "7.6.0" + analyzer_buffer: + dependency: transitive + description: + name: analyzer_buffer + sha256: f7833bee67c03c37241c67f8741b17cc501b69d9758df7a5a4a13ed6c947be43 + url: "https://pub.dev" + source: hosted + version: "0.1.10" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce + url: "https://pub.dev" + source: hosted + version: "0.13.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -17,6 +65,94 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "7d95cbbb1526ab5ae977df9b4cc660963b9b27f6d1075c0b34653868911385e4" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "38c9c339333a09b090a638849a4c56e70a404c6bdd3b511493addfbc113b60c2" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: b971d4a1c789eba7be3e6fe6ce5e5b50fd3719e3cb485b3fad6d04358304351d + url: "https://pub.dev" + source: hosted + version: "2.6.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: c04e612ca801cd0928ccdb891c263a2b1391cb27940a5ea5afcf9ba894de5d62 + url: "https://pub.dev" + source: hosted + version: "9.2.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + url: "https://pub.dev" + source: hosted + version: "1.3.0" characters: dependency: transitive description: @@ -25,6 +161,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -33,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" collection: dependency: transitive description: @@ -41,6 +217,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -49,6 +273,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "78085fbe842de7c5bef92de811ca81536968dbcbbcdac5c316711add2d15e796" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: cc5532d5733d4eccfccaaec6070a1926e9f21e613d93ad0927fad020b95c9e52 + url: "https://pub.dev" + source: hosted + version: "0.8.0" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1 + url: "https://pub.dev" + source: hosted + version: "0.8.0" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.7.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_cache_interceptor: + dependency: "direct main" + description: + name: dio_cache_interceptor + sha256: "1346705a2057c265014d7696e3e2318b560bfb00b484dac7f9b01e2ceaebb07d" + url: "https://pub.dev" + source: hosted + version: "3.5.1" + dio_cache_interceptor_hive_store: + dependency: "direct main" + description: + name: dio_cache_interceptor_hive_store + sha256: "449b36541216cb20543228081125ad2995eb9712ec35bd030d85663ea1761895" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -57,11 +361,80 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -70,11 +443,263 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "306f0596590e077338312f38837f595c04f28d6cdeeac392d3d74df2f0003687" + url: "https://pub.dev" + source: hosted + version: "2.0.32" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" + url: "https://pub.dev" + source: hosted + version: "3.0.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_ce: + dependency: "direct main" + description: + name: hive_ce + sha256: e3a9d2608350ca5ae1fcaa594bbc24c443cf8e67cd16b89ec258fb0bcccb0847 + url: "https://pub.dev" + source: hosted + version: "2.15.0" + hive_ce_flutter: + dependency: "direct main" + description: + name: hive_ce_flutter + sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive_ce_generator: + dependency: "direct dev" + description: + name: hive_ce_generator + sha256: a169feeff2da9cc2c417ce5ae9bcebf7c8a95d7a700492b276909016ad70a786 + url: "https://pub.dev" + source: hosted + version: "1.9.3" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca" + url: "https://pub.dev" + source: hosted + version: "0.8.13+5" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe" + url: "https://pub.dev" + source: hosted + version: "6.11.1" leak_tracker: dependency: transitive description: @@ -107,6 +732,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.dev" + source: hosted + version: "3.3.2" matcher: dependency: transitive description: @@ -131,6 +772,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + url: "https://pub.dev" + source: hosted + version: "5.2.3" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -139,11 +836,339 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 + url: "https://pub.dev" + source: hosted + version: "2.2.20" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29 + url: "https://pub.dev" + source: hosted + version: "1.0.0-dev.7" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "7ef9c43469e9b5ac4e4c3b24d7c30642e47ce1b12cd7dcdd643534db0a72ed13" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" + url: "https://pub.dev" + source: hosted + version: "2.4.15" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -152,6 +1177,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -160,6 +1233,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -168,6 +1249,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -176,6 +1265,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -184,6 +1289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + url: "https://pub.dev" + source: hosted + version: "1.26.2" test_api: dependency: transitive description: @@ -192,6 +1305,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + url: "https://pub.dev" + source: hosted + version: "0.6.11" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -208,6 +1385,94 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_writer: + dependency: transitive + description: + name: yaml_writer + sha256: "69651cd7238411179ac32079937d4aa9a2970150d6b2ae2c6fe6de09402a5dc5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 36f8fd9..9f00ebd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,37 +30,90 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + # State Management - Riverpod 3.0 + flutter_riverpod: ^3.0.0 + riverpod_annotation: ^3.0.0 + + # Local Database + hive_ce: ^2.6.0 + hive_ce_flutter: ^2.1.0 + + # Code Generation + freezed_annotation: ^3.0.0 + json_annotation: ^4.9.0 + + # Network + dio: ^5.4.3+1 + connectivity_plus: ^6.0.3 + pretty_dio_logger: ^1.3.1 + dio_cache_interceptor: ^3.5.0 + dio_cache_interceptor_hive_store: ^3.2.2 + + # UI & Design + cached_network_image: ^3.3.1 + shimmer: ^3.0.0 + lottie: ^3.1.2 + qr_flutter: ^4.1.0 + mobile_scanner: ^5.2.3 + + # Utilities + intl: ^0.20.0 + share_plus: ^9.0.0 + image_picker: ^1.1.2 + path_provider: ^2.1.3 + shared_preferences: ^2.2.3 + + # Icons cupertino_icons: ^1.0.8 dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. + # Code Generation + build_runner: ^2.4.11 + riverpod_generator: ^3.0.0 + riverpod_lint: ^3.0.0 + custom_lint: ^0.8.0 + freezed: ^3.0.0 + json_serializable: ^6.8.0 + hive_ce_generator: ^1.6.0 + + # Linting flutter_lints: ^5.0.0 + # Testing + mockito: ^5.4.4 + integration_test: + sdk: flutter + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true + generate: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + # Assets + assets: + - assets/images/ + - assets/animations/ + - assets/icons/ + + # Fonts - Using system default fonts for now + # To add custom Roboto fonts, create fonts/ directory and add font files + # fonts: + # - family: Roboto + # fonts: + # - asset: fonts/Roboto-Regular.ttf + # - asset: fonts/Roboto-Medium.ttf + # weight: 500 + # - asset: fonts/Roboto-Bold.ttf + # weight: 700 # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/scripts/setup_riverpod.sh b/scripts/setup_riverpod.sh new file mode 100755 index 0000000..34ff288 --- /dev/null +++ b/scripts/setup_riverpod.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Riverpod Setup Script for Worker Flutter App +# This script sets up Riverpod 3.0 with code generation + +echo "🚀 Setting up Riverpod 3.0..." + +# 1. Get dependencies +echo "📦 Installing dependencies..." +flutter pub get + +# 2. Clean previous builds +echo "🧹 Cleaning previous builds..." +dart run build_runner clean + +# 3. Run code generation +echo "⚙️ Running code generation..." +dart run build_runner build --delete-conflicting-outputs + +# 4. Run custom lint +echo "🔍 Running Riverpod linting..." +dart run custom_lint + +echo "✅ Riverpod setup complete!" +echo "" +echo "To watch for changes and auto-generate code, run:" +echo " dart run build_runner watch -d" +echo "" +echo "To run linting:" +echo " dart run custom_lint" diff --git a/test/widget_test.dart b/test/widget_test.dart index 6c3afb2..08f2973 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,26 +5,18 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:worker/main.dart'; +import 'package:worker/app.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('App smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const ProviderScope(child: WorkerApp())); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that the placeholder home page is displayed + expect(find.text('Worker App'), findsOneWidget); + expect(find.text('Chào mừng đến với Worker App'), findsOneWidget); }); }