From 2495330bf55c8fb2c915ea52d8775e6b38bbe045 Mon Sep 17 00:00:00 2001 From: renolation Date: Sun, 2 Nov 2025 20:40:11 +0700 Subject: [PATCH] fix --- API_CLIENT_SETUP.md | 452 --------------- API_INTEGRATION_COMPLETE.md | 198 ------- APP_COMPLETE_SETUP_GUIDE.md | 526 ------------------ AUTHENTICATION_FEATURE_SUMMARY.md | 384 ------------- QUICK_REFERENCE.md | 257 --------- ROUTER_SETUP.md | 426 -------------- ios/Podfile.lock | 10 +- lib/core/services/print_service.dart | 30 + lib/core/storage/secure_storage.dart | 21 + .../repositories/auth_repository_impl.dart | 2 + .../pages/product_detail_page.dart | 44 +- 11 files changed, 101 insertions(+), 2249 deletions(-) delete mode 100644 API_CLIENT_SETUP.md delete mode 100644 API_INTEGRATION_COMPLETE.md delete mode 100644 APP_COMPLETE_SETUP_GUIDE.md delete mode 100644 AUTHENTICATION_FEATURE_SUMMARY.md delete mode 100644 QUICK_REFERENCE.md delete mode 100644 ROUTER_SETUP.md diff --git a/API_CLIENT_SETUP.md b/API_CLIENT_SETUP.md deleted file mode 100644 index 942599c..0000000 --- a/API_CLIENT_SETUP.md +++ /dev/null @@ -1,452 +0,0 @@ -# API Client Setup - Complete Implementation - -## Overview - -I have created a robust API client for your Flutter warehouse management app with comprehensive features including: - -- **Automatic token management** from secure storage -- **401 error handling** with automatic token clearing -- **Request/response logging** with sensitive data redaction -- **Configurable timeouts** (30 seconds) -- **Proper error transformation** to custom exceptions -- **Support for all HTTP methods** (GET, POST, PUT, DELETE) - -## Files Created - -### 1. Core Network Files - -#### `/lib/core/network/api_client.dart` -- Main API client implementation using Dio -- Automatic Bearer token injection from secure storage -- Request/response/error interceptors with comprehensive logging -- 401 error handler that clears tokens and triggers logout callback -- Methods: `get()`, `post()`, `put()`, `delete()` -- Utility methods: `testConnection()`, `isAuthenticated()`, `getAccessToken()`, `clearAuth()` - -#### `/lib/core/network/api_response.dart` -- Generic API response wrapper matching your backend format -- Structure: `Value`, `IsSuccess`, `IsFailure`, `Errors`, `ErrorCodes` -- Helper methods: `hasData`, `getErrorMessage()`, `getAllErrorsAsString()`, `hasErrorCode()` - -#### `/lib/core/network/api_client_example.dart` -- Comprehensive usage examples for all scenarios -- Examples for: Login, GET/POST/PUT/DELETE requests, error handling, etc. - -#### `/lib/core/network/README.md` -- Complete documentation for the API client -- Usage guides, best practices, troubleshooting - -### 2. Secure Storage - -#### `/lib/core/storage/secure_storage.dart` -- Singleton wrapper for flutter_secure_storage -- Token management: `saveAccessToken()`, `getAccessToken()`, `clearTokens()` -- User data: `saveUserId()`, `saveUsername()`, etc. -- Utility methods: `isAuthenticated()`, `clearAll()`, `containsKey()` -- Platform-specific secure options (Android: encrypted shared prefs, iOS: Keychain) - -### 3. Constants - -#### `/lib/core/constants/api_endpoints.dart` -- Centralized API endpoint definitions -- Authentication: `/auth/login`, `/auth/logout` -- Warehouses: `/warehouses` -- Products: `/products` with query parameter helpers -- Scans: `/api/scans` - -### 4. Core Exports - -#### `/lib/core/core.dart` (Updated) -- Added exports for new modules: - - `api_endpoints.dart` - - `api_response.dart` - - `secure_storage.dart` - -### 5. Dependencies - -#### `pubspec.yaml` (Updated) -- Added `flutter_secure_storage: ^9.0.0` - -## Key Features - -### 1. Automatic Token Management - -The API client automatically injects Bearer tokens into all requests: - -```dart -// Initialize with secure storage -final secureStorage = SecureStorage(); -final apiClient = ApiClient(secureStorage); - -// Token is automatically added to all requests -final response = await apiClient.get('/warehouses'); -// Request header: Authorization: Bearer -``` - -### 2. 401 Error Handling - -When a 401 Unauthorized error occurs: -1. Error is logged to console -2. All tokens are cleared from secure storage -3. `onUnauthorized` callback is triggered -4. App can navigate to login screen - -```dart -final apiClient = ApiClient( - secureStorage, - onUnauthorized: () { - // This callback is triggered on 401 errors - context.go('/login'); // Navigate to login - }, -); -``` - -### 3. Comprehensive Logging - -All requests, responses, and errors are logged with sensitive data redacted: - -``` -REQUEST[GET] => https://api.example.com/warehouses -Headers: {Authorization: ***REDACTED***, Content-Type: application/json} - -RESPONSE[200] => https://api.example.com/warehouses -Data: {...} - -ERROR[401] => https://api.example.com/products -401 Unauthorized - Clearing tokens and triggering logout -``` - -### 4. Error Handling - -Dio exceptions are transformed into custom app exceptions: - -```dart -try { - final response = await apiClient.get('/products'); -} on NetworkException catch (e) { - // Timeout, no internet, etc. - print('Network error: ${e.message}'); -} on ServerException catch (e) { - // 4xx, 5xx errors - print('Server error: ${e.message}'); - print('Error code: ${e.code}'); // e.g., '401', '404', '500' -} -``` - -Specific error codes: -- `401`: Unauthorized (automatically handled) -- `403`: Forbidden -- `404`: Not Found -- `422`: Validation Error -- `429`: Rate Limited -- `500-504`: Server Errors - -### 5. API Response Parsing - -All API responses follow the standard format: - -```dart -final response = await apiClient.get('/warehouses'); - -final apiResponse = ApiResponse.fromJson( - response.data, - (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), -); - -if (apiResponse.isSuccess && apiResponse.value != null) { - final warehouses = apiResponse.value; - print('Found ${warehouses.length} warehouses'); -} else { - print('Error: ${apiResponse.getErrorMessage()}'); -} -``` - -## Usage Examples - -### Initialize API Client - -```dart -import 'package:minhthu/core/core.dart'; - -final secureStorage = SecureStorage(); -final apiClient = ApiClient( - secureStorage, - onUnauthorized: () { - // Navigate to login on 401 errors - context.go('/login'); - }, -); -``` - -### Login Flow - -```dart -// 1. Login request -final response = await apiClient.post( - ApiEndpoints.login, - data: { - 'username': 'user@example.com', - 'password': 'password123', - }, -); - -// 2. Parse response -final apiResponse = ApiResponse.fromJson( - response.data, - (json) => User.fromJson(json), -); - -// 3. Save tokens (typically done in LoginUseCase) -if (apiResponse.isSuccess && apiResponse.value != null) { - final user = apiResponse.value!; - await secureStorage.saveAccessToken(user.accessToken); - await secureStorage.saveRefreshToken(user.refreshToken); - await secureStorage.saveUserId(user.userId); -} -``` - -### Get Warehouses (Authenticated) - -```dart -// Token is automatically added by the interceptor -final response = await apiClient.get(ApiEndpoints.warehouses); - -final apiResponse = ApiResponse.fromJson( - response.data, - (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), -); - -if (apiResponse.isSuccess && apiResponse.value != null) { - final warehouses = apiResponse.value!; - // Use the data -} -``` - -### Get Products with Query Parameters - -```dart -final response = await apiClient.get( - ApiEndpoints.products, - queryParameters: ApiEndpoints.productQueryParams( - warehouseId: 1, - type: 'import', - ), -); -``` - -### Save Scan Data - -```dart -final response = await apiClient.post( - ApiEndpoints.scans, - data: { - 'barcode': '1234567890', - 'field1': 'Value 1', - 'field2': 'Value 2', - 'field3': 'Value 3', - 'field4': 'Value 4', - }, -); -``` - -### Check Authentication Status - -```dart -final isAuthenticated = await apiClient.isAuthenticated(); -if (!isAuthenticated) { - // Navigate to login -} -``` - -### Logout - -```dart -await apiClient.clearAuth(); // Clears all tokens -``` - -## Integration with Repository Pattern - -The API client is designed to work with your clean architecture: - -```dart -class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { - final ApiClient apiClient; - - WarehouseRemoteDataSourceImpl(this.apiClient); - - @override - Future> getWarehouses() async { - final response = await apiClient.get(ApiEndpoints.warehouses); - - final apiResponse = ApiResponse.fromJson( - response.data, - (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), - ); - - if (apiResponse.isSuccess && apiResponse.value != null) { - return apiResponse.value!; - } else { - throw ServerException(apiResponse.getErrorMessage()); - } - } -} -``` - -## Configuration - -### Timeouts (in `app_constants.dart`) - -```dart -static const int connectionTimeout = 30000; // 30 seconds -static const int receiveTimeout = 30000; // 30 seconds -static const int sendTimeout = 30000; // 30 seconds -``` - -### Base URL (in `app_constants.dart`) - -```dart -static const String apiBaseUrl = 'https://api.example.com'; -``` - -Or update dynamically: - -```dart -apiClient.updateBaseUrl('https://dev-api.example.com'); -``` - -## Security Features - -1. **Token Encryption**: Tokens stored using platform-specific secure storage - - Android: Encrypted SharedPreferences - - iOS: Keychain with `first_unlock` accessibility - -2. **Automatic Token Clearing**: 401 errors automatically clear all tokens - -3. **Log Sanitization**: Authorization headers redacted in logs - ``` - Headers: {Authorization: ***REDACTED***} - ``` - -4. **Singleton Pattern**: SecureStorage uses singleton to prevent multiple instances - -## Testing - -To test the API connection: - -```dart -final isConnected = await apiClient.testConnection(); -if (!isConnected) { - print('Cannot connect to API'); -} -``` - -## Dependency Injection (GetIt) - -Register with GetIt service locator: - -```dart -final getIt = GetIt.instance; - -// Register SecureStorage -getIt.registerLazySingleton(() => SecureStorage()); - -// Register ApiClient -getIt.registerLazySingleton( - () => ApiClient( - getIt(), - onUnauthorized: () { - // Handle unauthorized - }, - ), -); -``` - -## File Structure - -``` -lib/ - core/ - constants/ - app_constants.dart # Existing - timeouts and base URL - api_endpoints.dart # NEW - API endpoint constants - network/ - api_client.dart # UPDATED - Full implementation - api_response.dart # NEW - API response wrapper - api_client_example.dart # NEW - Usage examples - README.md # NEW - Documentation - storage/ - secure_storage.dart # NEW - Secure storage wrapper - errors/ - exceptions.dart # Existing - Used by API client - failures.dart # Existing - Used by repositories - core.dart # UPDATED - Added new exports -``` - -## Next Steps - -1. **Update Existing Repositories**: Update your remote data sources to use the new API client -2. **Configure Base URL**: Set the correct API base URL in `app_constants.dart` -3. **Set Up Navigation**: Implement the `onUnauthorized` callback to navigate to login -4. **Add API Endpoints**: Add any missing endpoints to `api_endpoints.dart` -5. **Test Authentication Flow**: Test login, token injection, and 401 handling - -## Testing the Setup - -Run the example: - -```dart -import 'package:minhthu/core/network/api_client_example.dart'; - -void main() async { - await runExamples(); -} -``` - -## Troubleshooting - -### Token not being injected -- Verify token is saved: `await secureStorage.getAccessToken()` -- Check logs for: `REQUEST[...] Headers: {Authorization: ***REDACTED***}` - -### 401 errors not clearing tokens -- Verify `onUnauthorized` callback is set -- Check logs for: `401 Unauthorized - Clearing tokens and triggering logout` - -### Connection timeouts -- Check network connectivity -- Verify base URL is correct -- Increase timeout values if needed - -## Analysis Results - -All files pass Flutter analysis with no issues: -- ✅ `api_client.dart` - No issues found -- ✅ `secure_storage.dart` - No issues found -- ✅ `api_response.dart` - No issues found -- ✅ `api_endpoints.dart` - No issues found - -## Documentation - -For detailed documentation, see: -- `/lib/core/network/README.md` - Complete API client documentation -- `/lib/core/network/api_client_example.dart` - Code examples - -## Summary - -The API client is production-ready with: -- ✅ Automatic token management from secure storage -- ✅ Request interceptor to inject Bearer tokens -- ✅ Response interceptor for logging -- ✅ Error interceptor to handle 401 errors -- ✅ Automatic token clearing on unauthorized access -- ✅ Comprehensive error handling -- ✅ Request/response logging with sensitive data redaction -- ✅ Support for all HTTP methods (GET, POST, PUT, DELETE) -- ✅ Configurable timeouts (30 seconds) -- ✅ Environment-specific base URLs -- ✅ Connection testing -- ✅ Clean integration with repository pattern -- ✅ Comprehensive documentation and examples -- ✅ All files pass static analysis - -The API client is ready to use and follows Flutter best practices and clean architecture principles! diff --git a/API_INTEGRATION_COMPLETE.md b/API_INTEGRATION_COMPLETE.md deleted file mode 100644 index 8dbbf7b..0000000 --- a/API_INTEGRATION_COMPLETE.md +++ /dev/null @@ -1,198 +0,0 @@ -# API Integration Complete - -## ✅ API Configuration Updated - -All API endpoints and authentication have been updated to match the actual backend API from `/lib/docs/api.sh`. - -### 🔗 API Base URL -``` -Base URL: https://dotnet.elidev.info:8157/ws -App ID: Minhthu2016 -``` - -### 🔐 Authentication Updates - -#### Headers Changed: -- ❌ Old: `Authorization: Bearer {token}` -- ✅ New: `AccessToken: {token}` -- ✅ Added: `AppID: Minhthu2016` - -#### Login Request Format: -- ❌ Old fields: `username`, `password` -- ✅ New fields: `EmailPhone`, `Password` - -### 📍 API Endpoints Updated - -| Feature | Endpoint | Method | Notes | -|---------|----------|--------|-------| -| Login | `/PortalAuth/Login` | POST | EmailPhone + Password | -| Warehouses | `/portalWareHouse/search` | POST | Pagination params required | -| Products | `/portalProduct/getAllProduct` | GET | Returns all products | - -## 🛠️ Files Modified - -### 1. **app_constants.dart** -```dart -static const String apiBaseUrl = 'https://dotnet.elidev.info:8157/ws'; -static const String appId = 'Minhthu2016'; -``` - -### 2. **api_endpoints.dart** -```dart -static const String login = '/PortalAuth/Login'; -static const String warehouses = '/portalWareHouse/search'; -static const String products = '/portalProduct/getAllProduct'; -``` - -### 3. **api_client.dart** -```dart -// Changed from Authorization: Bearer to AccessToken -options.headers['AccessToken'] = token; -options.headers['AppID'] = AppConstants.appId; -``` - -### 4. **login_request_model.dart** -```dart -Map toJson() { - return { - 'EmailPhone': username, // Changed from 'username' - 'Password': password, // Changed from 'password' - }; -} -``` - -### 5. **warehouse_remote_datasource.dart** -```dart -// Changed from GET to POST with pagination -final response = await apiClient.post( - '/portalWareHouse/search', - data: { - 'pageIndex': 0, - 'pageSize': 100, - 'Name': null, - 'Code': null, - 'sortExpression': null, - 'sortDirection': null, - }, -); -``` - -### 6. **products_remote_datasource.dart** -```dart -// Updated to use correct endpoint -final response = await apiClient.get('/portalProduct/getAllProduct'); -``` - -## 🎯 Pre-filled Test Credentials - -The login form is pre-filled with test credentials: -- **Email**: `yesterday305@gmail.com` -- **Password**: `123456` - -## 🚀 Ready to Test - -The app is now configured to connect to the actual backend API. You can: - -1. **Run the app**: - ```bash - flutter run - ``` - -2. **Test the flow**: - - Login with pre-filled credentials - - View warehouses list - - Select a warehouse - - Choose Import or Export - - View products - -## 📝 API Request Examples - -### Login Request: -```bash -POST https://dotnet.elidev.info:8157/ws/PortalAuth/Login -Headers: - Content-Type: application/json - AppID: Minhthu2016 -Body: -{ - "EmailPhone": "yesterday305@gmail.com", - "Password": "123456" -} -``` - -### Get Warehouses Request: -```bash -POST https://dotnet.elidev.info:8157/ws/portalWareHouse/search -Headers: - Content-Type: application/json - AppID: Minhthu2016 - AccessToken: {token_from_login} -Body: -{ - "pageIndex": 0, - "pageSize": 100, - "Name": null, - "Code": null, - "sortExpression": null, - "sortDirection": null -} -``` - -### Get Products Request: -```bash -GET https://dotnet.elidev.info:8157/ws/portalProduct/getAllProduct -Headers: - AppID: Minhthu2016 - AccessToken: {token_from_login} -``` - -## ⚠️ Important Notes - -1. **HTTPS Certificate**: The API uses a self-signed certificate. You may need to handle SSL certificate validation in production. - -2. **CORS**: Make sure CORS is properly configured on the backend for mobile apps. - -3. **Token Storage**: Access tokens are securely stored using `flutter_secure_storage`. - -4. **Error Handling**: All API errors are properly handled and displayed to users. - -5. **Logging**: API requests and responses are logged in debug mode for troubleshooting. - -## 🔍 Testing Checklist - -- [ ] Login with test credentials works -- [ ] Access token is saved in secure storage -- [ ] Warehouses list loads successfully -- [ ] Warehouse selection works -- [ ] Navigation to operations page works -- [ ] Products list loads successfully -- [ ] All UI states work (loading, error, success, empty) -- [ ] Refresh functionality works -- [ ] Logout clears the token - -## 🐛 Troubleshooting - -### If login fails: -1. Check internet connection -2. Verify API is accessible at `https://dotnet.elidev.info:8157` -3. Check credentials are correct -4. Look at debug logs for detailed error messages - -### If API calls fail after login: -1. Verify access token is being saved -2. Check that AccessToken and AppID headers are being sent -3. Verify token hasn't expired -4. Check API logs for detailed error information - -## 📚 Related Files - -- `/lib/docs/api.sh` - Original curl commands -- `/lib/core/constants/app_constants.dart` - API configuration -- `/lib/core/constants/api_endpoints.dart` - Endpoint definitions -- `/lib/core/network/api_client.dart` - HTTP client configuration -- `/lib/features/auth/data/models/login_request_model.dart` - Login request format - ---- - -**Status**: ✅ Ready for testing with production API -**Last Updated**: $(date) diff --git a/APP_COMPLETE_SETUP_GUIDE.md b/APP_COMPLETE_SETUP_GUIDE.md deleted file mode 100644 index b4ae03a..0000000 --- a/APP_COMPLETE_SETUP_GUIDE.md +++ /dev/null @@ -1,526 +0,0 @@ -# Complete App Setup Guide - Warehouse Management App - -## Overview -This guide provides a complete overview of the rewritten warehouse management app following clean architecture principles as specified in CLAUDE.md. - -## App Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ (UI, Widgets, State Management with Riverpod) │ -├─────────────────────────────────────────────────────────┤ -│ Domain Layer │ -│ (Business Logic, Use Cases, Entities, Interfaces) │ -├─────────────────────────────────────────────────────────┤ -│ Data Layer │ -│ (API Client, Models, Data Sources, Repositories) │ -├─────────────────────────────────────────────────────────┤ -│ Core Layer │ -│ (Network, Storage, Theme, Constants, Utilities) │ -└─────────────────────────────────────────────────────────┘ -``` - -## Project Structure - -``` -lib/ -├── core/ # Core infrastructure -│ ├── constants/ -│ │ ├── api_endpoints.dart # API endpoint constants -│ │ └── app_constants.dart # App-wide constants -│ ├── di/ -│ │ └── providers.dart # Riverpod dependency injection -│ ├── errors/ -│ │ ├── exceptions.dart # Exception classes -│ │ └── failures.dart # Failure classes for Either -│ ├── network/ -│ │ ├── api_client.dart # Dio HTTP client with interceptors -│ │ └── api_response.dart # Generic API response wrapper -│ ├── router/ -│ │ └── app_router.dart # GoRouter configuration -│ ├── storage/ -│ │ └── secure_storage.dart # Secure token storage -│ ├── theme/ -│ │ └── app_theme.dart # Material 3 theme -│ └── widgets/ -│ ├── custom_button.dart # Reusable button widgets -│ └── loading_indicator.dart # Loading indicators -│ -├── features/ # Feature-first organization -│ ├── auth/ # Authentication feature -│ │ ├── data/ -│ │ │ ├── datasources/ -│ │ │ │ └── auth_remote_datasource.dart -│ │ │ ├── models/ -│ │ │ │ ├── login_request_model.dart -│ │ │ │ └── user_model.dart -│ │ │ └── repositories/ -│ │ │ └── auth_repository_impl.dart -│ │ ├── domain/ -│ │ │ ├── entities/ -│ │ │ │ └── user_entity.dart -│ │ │ ├── repositories/ -│ │ │ │ └── auth_repository.dart -│ │ │ └── usecases/ -│ │ │ └── login_usecase.dart -│ │ └── presentation/ -│ │ ├── pages/ -│ │ │ └── login_page.dart -│ │ ├── providers/ -│ │ │ └── auth_provider.dart -│ │ └── widgets/ -│ │ └── login_form.dart -│ │ -│ ├── warehouse/ # Warehouse feature -│ │ ├── data/ -│ │ │ ├── datasources/ -│ │ │ │ └── warehouse_remote_datasource.dart -│ │ │ ├── models/ -│ │ │ │ └── warehouse_model.dart -│ │ │ └── repositories/ -│ │ │ └── warehouse_repository_impl.dart -│ │ ├── domain/ -│ │ │ ├── entities/ -│ │ │ │ └── warehouse_entity.dart -│ │ │ ├── repositories/ -│ │ │ │ └── warehouse_repository.dart -│ │ │ └── usecases/ -│ │ │ └── get_warehouses_usecase.dart -│ │ └── presentation/ -│ │ ├── pages/ -│ │ │ └── warehouse_selection_page.dart -│ │ ├── providers/ -│ │ │ └── warehouse_provider.dart -│ │ └── widgets/ -│ │ └── warehouse_card.dart -│ │ -│ ├── operation/ # Operation selection feature -│ │ └── presentation/ -│ │ ├── pages/ -│ │ │ └── operation_selection_page.dart -│ │ └── widgets/ -│ │ └── operation_card.dart -│ │ -│ └── products/ # Products feature -│ ├── data/ -│ │ ├── datasources/ -│ │ │ └── products_remote_datasource.dart -│ │ ├── models/ -│ │ │ └── product_model.dart -│ │ └── repositories/ -│ │ └── products_repository_impl.dart -│ ├── domain/ -│ │ ├── entities/ -│ │ │ └── product_entity.dart -│ │ ├── repositories/ -│ │ │ └── products_repository.dart -│ │ └── usecases/ -│ │ └── get_products_usecase.dart -│ └── presentation/ -│ ├── pages/ -│ │ └── products_page.dart -│ ├── providers/ -│ │ └── products_provider.dart -│ └── widgets/ -│ └── product_list_item.dart -│ -└── main.dart # App entry point -``` - -## App Flow - -``` -1. App Start - ↓ -2. Check Authentication (via SecureStorage) - ↓ - ├── Not Authenticated → Login Screen - │ ↓ - │ Enter credentials - │ ↓ - │ API: POST /auth/login - │ ↓ - │ Store access token - │ ↓ - └── Authenticated → Warehouse Selection Screen - ↓ - API: GET /warehouses - ↓ - Select warehouse - ↓ - Operation Selection Screen - ↓ - Choose Import or Export - ↓ - Products List Screen - ↓ - API: GET /products?warehouseId={id}&type={type} - ↓ - Display products -``` - -## Key Technologies - -- **Flutter SDK**: >=3.0.0 <4.0.0 -- **State Management**: Riverpod (flutter_riverpod ^2.4.9) -- **Navigation**: GoRouter (go_router ^13.2.0) -- **HTTP Client**: Dio (dio ^5.3.2) -- **Secure Storage**: FlutterSecureStorage (flutter_secure_storage ^9.0.0) -- **Functional Programming**: Dartz (dartz ^0.10.1) -- **Value Equality**: Equatable (equatable ^2.0.5) - -## Setup Instructions - -### 1. Install Dependencies - -```bash -cd /Users/phuocnguyen/Projects/minhthu -flutter pub get -``` - -### 2. Configure API Base URL - -Edit `/Users/phuocnguyen/Projects/minhthu/lib/core/constants/app_constants.dart`: - -```dart -static const String apiBaseUrl = 'https://your-api-domain.com'; -``` - -### 3. Configure API Endpoints (if needed) - -Edit `/Users/phuocnguyen/Projects/minhthu/lib/core/constants/api_endpoints.dart` to match your backend API paths. - -### 4. Run the App - -```bash -flutter run -``` - -## API Integration - -### API Response Format - -All APIs follow this response format: - -```json -{ - "Value": , - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -### Available APIs - -#### 1. Login -```bash -POST /auth/login -Content-Type: application/json - -{ - "username": "string", - "password": "string" -} - -Response: -{ - "Value": { - "userId": "string", - "username": "string", - "accessToken": "string", - "refreshToken": "string" - }, - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -#### 2. Get Warehouses -```bash -GET /warehouses -Authorization: Bearer {access_token} - -Response: -{ - "Value": [ - { - "Id": 1, - "Name": "Kho nguyên vật liệu", - "Code": "001", - "Description": null, - "IsNGWareHouse": false, - "TotalCount": 8 - } - ], - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -#### 3. Get Products -```bash -GET /products?warehouseId={id}&type={import/export} -Authorization: Bearer {access_token} - -Response: -{ - "Value": [ - { - "Id": 11, - "Name": "Thép 435", - "Code": "SCM435", - "FullName": "SCM435 | Thép 435", - "Weight": 120.00, - "Pieces": 1320, - "ConversionRate": 11.00, - ... (43 total fields) - } - ], - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -## Usage Examples - -### Using Auth Provider - -```dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:minhthu/core/di/providers.dart'; - -class MyWidget extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - // Watch auth state - final isAuthenticated = ref.watch(isAuthenticatedProvider); - final currentUser = ref.watch(currentUserProvider); - final isLoading = ref.watch(authProvider.select((s) => s.isLoading)); - - // Login - onLoginPressed() async { - await ref.read(authProvider.notifier).login(username, password); - } - - // Logout - onLogoutPressed() async { - await ref.read(authProvider.notifier).logout(); - } - - return Container(); - } -} -``` - -### Using Warehouse Provider - -```dart -class WarehousePage extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - // Watch warehouses - final warehouses = ref.watch(warehousesListProvider); - final selectedWarehouse = ref.watch(selectedWarehouseProvider); - final isLoading = ref.watch(warehouseProvider.select((s) => s.isLoading)); - - // Load warehouses - useEffect(() { - Future.microtask(() => - ref.read(warehouseProvider.notifier).loadWarehouses() - ); - return null; - }, []); - - // Select warehouse - onWarehouseTap(warehouse) { - ref.read(warehouseProvider.notifier).selectWarehouse(warehouse); - context.go('/operations', extra: warehouse); - } - - return ListView.builder(...); - } -} -``` - -### Using Products Provider - -```dart -class ProductsPage extends ConsumerWidget { - final int warehouseId; - final String warehouseName; - final String operationType; - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Watch products - final products = ref.watch(productsListProvider); - final isLoading = ref.watch(productsProvider.select((s) => s.isLoading)); - - // Load products - useEffect(() { - Future.microtask(() => - ref.read(productsProvider.notifier) - .loadProducts(warehouseId, warehouseName, operationType) - ); - return null; - }, [warehouseId, operationType]); - - return ListView.builder(...); - } -} -``` - -### Navigation - -```dart -// Navigate to login -context.go('/login'); - -// Navigate to warehouses -context.go('/warehouses'); - -// Navigate to operations -context.go('/operations', extra: warehouseEntity); - -// Navigate to products -context.go('/products', extra: { - 'warehouse': warehouseEntity, - 'warehouseName': 'Kho nguyên vật liệu', - 'operationType': 'import', // or 'export' -}); - -// Or use extension methods -context.goToOperations(warehouse); -context.goToProducts(warehouse: warehouse, operationType: 'import'); -``` - -## Error Handling - -### Using Either for Error Handling - -```dart -final result = await ref.read(authProvider.notifier).login(username, password); - -result.fold( - (failure) { - // Handle error - print('Error: ${failure.message}'); - }, - (user) { - // Handle success - print('Logged in as: ${user.username}'); - }, -); -``` - -### Error Types - -- **ServerFailure**: API returned an error -- **NetworkFailure**: Network connectivity issues -- **AuthenticationFailure**: Authentication/authorization errors - -## Testing - -### Run Tests - -```bash -flutter test -``` - -### Run Analysis - -```bash -flutter analyze -``` - -### Test Coverage - -```bash -flutter test --coverage -``` - -## Security Best Practices - -1. **Token Storage**: Access tokens are stored securely using `flutter_secure_storage` with platform-specific encryption: - - Android: EncryptedSharedPreferences - - iOS: Keychain - -2. **API Security**: - - All authenticated requests include Bearer token - - 401 errors automatically clear tokens and redirect to login - - Sensitive data redacted in logs - -3. **HTTPS**: All API calls should use HTTPS in production - -## Performance Optimizations - -1. **Riverpod**: Minimal rebuilds with fine-grained reactivity -2. **Lazy Loading**: Providers are created only when needed -3. **Caching**: SecureStorage caches auth tokens -4. **Error Boundaries**: Proper error handling prevents crashes - -## Troubleshooting - -### Issue: Login fails with 401 -- Check API base URL in `app_constants.dart` -- Verify credentials -- Check network connectivity - -### Issue: White screen after login -- Check if router redirect logic is working -- Verify `SecureStorage.isAuthenticated()` returns true -- Check console for errors - -### Issue: Products not loading -- Verify warehouse is selected -- Check API endpoint configuration -- Verify access token is valid - -### Issue: Build errors -```bash -flutter clean -flutter pub get -flutter run -``` - -## Documentation References - -- **Core Architecture**: `/lib/core/di/README.md` -- **Auth Feature**: `/lib/features/auth/README.md` -- **Warehouse Feature**: `/lib/features/warehouse/README.md` -- **Products Feature**: Inline documentation in code -- **API Client**: `/lib/core/network/README.md` -- **Router**: `/lib/core/router/README.md` - -## Next Steps - -1. ✅ Core architecture set up -2. ✅ Auth feature implemented -3. ✅ Warehouse feature implemented -4. ✅ Operation selection implemented -5. ✅ Products feature implemented -6. ✅ Routing configured -7. ✅ Dependency injection set up -8. ⏳ Configure production API URL -9. ⏳ Test with real API -10. ⏳ Add additional features as needed - -## Support - -For issues or questions: -1. Check inline documentation in code files -2. Review README files in each module -3. Check CLAUDE.md for specifications - -## License - -This project follows the specifications in CLAUDE.md and is built with clean architecture principles. diff --git a/AUTHENTICATION_FEATURE_SUMMARY.md b/AUTHENTICATION_FEATURE_SUMMARY.md deleted file mode 100644 index 7d97999..0000000 --- a/AUTHENTICATION_FEATURE_SUMMARY.md +++ /dev/null @@ -1,384 +0,0 @@ -# Authentication Feature - Implementation Summary - -Complete authentication feature following clean architecture for the warehouse management app. - -## Created Files - -### Data Layer (7 files) -1. `/lib/features/auth/data/models/login_request_model.dart` - - LoginRequest model with username and password - - toJson() method for API requests - -2. `/lib/features/auth/data/models/user_model.dart` - - UserModel extending UserEntity - - fromJson() and toJson() methods - - Conversion between model and entity - -3. `/lib/features/auth/data/datasources/auth_remote_datasource.dart` - - Abstract AuthRemoteDataSource interface - - AuthRemoteDataSourceImpl using ApiClient - - login(), logout(), refreshToken() methods - - Uses ApiResponse wrapper - -4. `/lib/features/auth/data/repositories/auth_repository_impl.dart` - - Implements AuthRepository interface - - Coordinates remote data source and secure storage - - Converts exceptions to failures - - Returns Either - -5. `/lib/features/auth/data/data.dart` - - Barrel export file for data layer - -### Domain Layer (4 files) -6. `/lib/features/auth/domain/entities/user_entity.dart` - - Pure domain entity (no dependencies) - - UserEntity with userId, username, accessToken, refreshToken - -7. `/lib/features/auth/domain/repositories/auth_repository.dart` - - Abstract repository interface - - Defines contract for authentication operations - - Returns Either - -8. `/lib/features/auth/domain/usecases/login_usecase.dart` - - LoginUseCase with input validation - - LogoutUseCase - - CheckAuthStatusUseCase - - GetCurrentUserUseCase - - RefreshTokenUseCase - -9. `/lib/features/auth/domain/domain.dart` - - Barrel export file for domain layer - -### Presentation Layer (5 files) -10. `/lib/features/auth/presentation/providers/auth_provider.dart` - - AuthState class (user, isAuthenticated, isLoading, error) - - AuthNotifier using Riverpod StateNotifier - - login(), logout(), checkAuthStatus() methods - - State management logic - -11. `/lib/features/auth/presentation/pages/login_page.dart` - - LoginPage using ConsumerStatefulWidget - - Material 3 design with app logo - - Error display and loading states - - Auto-navigation after successful login - - Integration with auth provider - -12. `/lib/features/auth/presentation/widgets/login_form.dart` - - Reusable LoginForm widget - - Form validation (username >= 3 chars, password >= 6 chars) - - Password visibility toggle - - TextField styling with Material 3 - -13. `/lib/features/auth/presentation/presentation.dart` - - Barrel export file for presentation layer - -### Dependency Injection (1 file) -14. `/lib/features/auth/di/auth_dependency_injection.dart` - - Complete Riverpod provider setup - - Data layer providers (data sources, storage) - - Domain layer providers (repository, use cases) - - Presentation layer providers (state notifier) - - Convenience providers for common use cases - -### Main Exports (1 file) -15. `/lib/features/auth/auth.dart` - - Main barrel export for the entire feature - - Public API for the auth module - -### Documentation (3 files) -16. `/lib/features/auth/README.md` - - Comprehensive feature documentation - - Architecture overview - - Usage examples - - API integration guide - - Testing guidelines - - Troubleshooting section - -17. `/lib/features/auth/INTEGRATION_GUIDE.md` - - Step-by-step integration guide - - Code examples for main.dart and router - - Testing checklist - - Environment configuration - - Common issues and solutions - -18. `/AUTHENTICATION_FEATURE_SUMMARY.md` - - This file - overview of all created files - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ ┌────────────┐ ┌──────────────┐ ┌──────────────────┐ │ -│ │ LoginPage │ │ AuthProvider │ │ LoginForm │ │ -│ └────────────┘ └──────────────┘ └──────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────────────────────────┐ -│ Domain Layer │ -│ ┌──────────────┐ ┌────────────────┐ ┌───────────────┐ │ -│ │ UseCases │ │ Repository │ │ Entities │ │ -│ │ │ │ Interface │ │ │ │ -│ └──────────────┘ └────────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ ┌──────────────┐ ┌────────────────┐ ┌───────────────┐ │ -│ │ Repository │ │ DataSources │ │ Models │ │ -│ │ Impl │ │ │ │ │ │ -│ └──────────────┘ └────────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - ↓ ↑ -┌─────────────────────────────────────────────────────────────┐ -│ External Services │ -│ ┌──────────────┐ ┌────────────────┐ │ -│ │ ApiClient │ │ SecureStorage │ │ -│ └──────────────┘ └────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Key Features Implemented - -### Core Functionality -- ✅ User login with username/password -- ✅ Secure token storage (encrypted) -- ✅ Authentication state management with Riverpod -- ✅ Form validation (username, password) -- ✅ Error handling with user-friendly messages -- ✅ Loading states during async operations -- ✅ Auto-navigation after successful login -- ✅ Check authentication status on app start -- ✅ Logout functionality with token cleanup -- ✅ Token refresh support (prepared for future use) - -### Architecture Quality -- ✅ Clean architecture (data/domain/presentation) -- ✅ SOLID principles -- ✅ Dependency injection with Riverpod -- ✅ Repository pattern -- ✅ Use case pattern -- ✅ Either type for error handling (dartz) -- ✅ Proper separation of concerns -- ✅ Testable architecture -- ✅ Feature-first organization -- ✅ Barrel exports for clean imports - -### UI/UX -- ✅ Material 3 design system -- ✅ Custom themed components -- ✅ Loading indicators -- ✅ Error messages with icons -- ✅ Password visibility toggle -- ✅ Form validation feedback -- ✅ Disabled state during loading -- ✅ Responsive layout -- ✅ Accessibility support - -### Security -- ✅ Tokens stored in encrypted secure storage -- ✅ Password field obscured -- ✅ Auth token automatically added to API headers -- ✅ Token cleared on logout -- ✅ No sensitive data in logs -- ✅ Input validation - -## Data Flow - -### Login Flow -``` -User Input (LoginPage) - ↓ -LoginForm Validation - ↓ -AuthNotifier.login() - ↓ -LoginUseCase (validation) - ↓ -AuthRepository.login() - ↓ -AuthRemoteDataSource.login() → API Call - ↓ -API Response → UserModel - ↓ -Save to SecureStorage - ↓ -Update AuthState (authenticated) - ↓ -Navigate to /warehouses -``` - -### Logout Flow -``` -User Action - ↓ -AuthNotifier.logout() - ↓ -LogoutUseCase - ↓ -AuthRepository.logout() - ↓ -API Logout (optional) - ↓ -Clear SecureStorage - ↓ -Reset AuthState - ↓ -Navigate to /login -``` - -## Integration Checklist - -### Prerequisites -- [x] flutter_riverpod ^2.4.9 -- [x] dartz ^0.10.1 -- [x] flutter_secure_storage ^9.0.0 -- [x] dio ^5.3.2 -- [x] equatable ^2.0.5 -- [x] go_router ^12.1.3 - -### Integration Steps -1. ⏳ Wrap app with ProviderScope in main.dart -2. ⏳ Configure router with login and protected routes -3. ⏳ Set API base URL in app_constants.dart -4. ⏳ Add /warehouses route (or your target route) -5. ⏳ Test login flow -6. ⏳ Test logout flow -7. ⏳ Test persistence (app restart) -8. ⏳ Test error handling - -### Testing TODO -- [ ] Unit tests for use cases -- [ ] Unit tests for repository -- [ ] Unit tests for data sources -- [ ] Widget tests for LoginPage -- [ ] Widget tests for LoginForm -- [ ] Integration tests for full flow - -## API Integration - -### Required Endpoints -- `POST /api/v1/auth/login` - Login endpoint -- `POST /api/v1/auth/logout` - Logout endpoint (optional) -- `POST /api/v1/auth/refresh` - Token refresh endpoint - -### Expected Response Format -```json -{ - "Value": { - "userId": "string", - "username": "string", - "accessToken": "string", - "refreshToken": "string" - }, - "IsSuccess": true, - "IsFailure": false, - "Errors": [], - "ErrorCodes": [] -} -``` - -## File Size Summary - -Total Files Created: 18 -- Dart Files: 15 -- Documentation: 3 -- Total Lines of Code: ~2,500 - -## Dependencies Used - -### Core -- `flutter_riverpod` - State management -- `dartz` - Functional programming (Either) -- `equatable` - Value equality - -### Storage -- `flutter_secure_storage` - Encrypted storage - -### Network -- `dio` - HTTP client (via ApiClient) - -### Navigation -- `go_router` - Routing - -### Internal -- `core/network/api_client.dart` -- `core/storage/secure_storage.dart` -- `core/errors/failures.dart` -- `core/errors/exceptions.dart` -- `core/widgets/custom_button.dart` -- `core/widgets/loading_indicator.dart` -- `core/constants/api_endpoints.dart` - -## Next Steps - -1. **Immediate** - - Configure API base URL - - Integrate into main.dart - - Configure router - - Test basic login flow - -2. **Short Term** - - Create warehouse selection feature - - Add token auto-refresh - - Implement remember me - - Add biometric authentication - -3. **Long Term** - - Add comprehensive tests - - Implement password reset - - Add multi-factor authentication - - Performance optimization - -## Code Quality Metrics - -- Clean Architecture: ✅ -- SOLID Principles: ✅ -- Testability: ✅ -- Documentation: ✅ -- Type Safety: ✅ -- Error Handling: ✅ -- Separation of Concerns: ✅ -- Dependency Injection: ✅ - -## Files Location Reference - -``` -lib/features/auth/ -├── data/ -│ ├── datasources/ -│ │ └── auth_remote_datasource.dart -│ ├── models/ -│ │ ├── login_request_model.dart -│ │ └── user_model.dart -│ ├── repositories/ -│ │ └── auth_repository_impl.dart -│ └── data.dart -├── domain/ -│ ├── entities/ -│ │ └── user_entity.dart -│ ├── repositories/ -│ │ └── auth_repository.dart -│ ├── usecases/ -│ │ └── login_usecase.dart -│ └── domain.dart -├── presentation/ -│ ├── pages/ -│ │ └── login_page.dart -│ ├── providers/ -│ │ └── auth_provider.dart -│ ├── widgets/ -│ │ └── login_form.dart -│ └── presentation.dart -├── di/ -│ └── auth_dependency_injection.dart -├── auth.dart -├── README.md -└── INTEGRATION_GUIDE.md -``` - -## Conclusion - -The authentication feature is complete and ready for integration. It follows clean architecture principles, uses industry-standard patterns, and provides a solid foundation for the warehouse management app. - -All code is documented, tested patterns are in place, and comprehensive guides are provided for integration and usage. diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md deleted file mode 100644 index 0dcab33..0000000 --- a/QUICK_REFERENCE.md +++ /dev/null @@ -1,257 +0,0 @@ -# API Client Quick Reference - -## Import - -```dart -import 'package:minhthu/core/core.dart'; -``` - -## Initialization - -```dart -final secureStorage = SecureStorage(); -final apiClient = ApiClient( - secureStorage, - onUnauthorized: () => context.go('/login'), -); -``` - -## HTTP Methods - -### GET Request - -```dart -final response = await apiClient.get( - '/warehouses', - queryParameters: {'limit': 10}, -); -``` - -### POST Request - -```dart -final response = await apiClient.post( - '/auth/login', - data: {'username': 'user', 'password': 'pass'}, -); -``` - -### PUT Request - -```dart -final response = await apiClient.put( - '/products/123', - data: {'name': 'Updated'}, -); -``` - -### DELETE Request - -```dart -final response = await apiClient.delete('/products/123'); -``` - -## Parse API Response - -```dart -final apiResponse = ApiResponse.fromJson( - response.data, - (json) => User.fromJson(json), // or your model -); - -if (apiResponse.isSuccess && apiResponse.value != null) { - final data = apiResponse.value; - // Use data -} else { - final error = apiResponse.getErrorMessage(); - // Handle error -} -``` - -## Error Handling - -```dart -try { - final response = await apiClient.get('/products'); -} on NetworkException catch (e) { - // Timeout, no internet - print('Network error: ${e.message}'); -} on ServerException catch (e) { - // HTTP errors (401, 404, 500, etc.) - print('Server error: ${e.message}'); - print('Error code: ${e.code}'); -} -``` - -## Token Management - -### Save Token - -```dart -await secureStorage.saveAccessToken('your_token'); -await secureStorage.saveRefreshToken('refresh_token'); -``` - -### Get Token - -```dart -final token = await secureStorage.getAccessToken(); -``` - -### Check Authentication - -```dart -final isAuthenticated = await apiClient.isAuthenticated(); -``` - -### Clear Tokens (Logout) - -```dart -await apiClient.clearAuth(); -``` - -## API Endpoints - -Use constants from `ApiEndpoints`: - -```dart -// Authentication -ApiEndpoints.login // /auth/login -ApiEndpoints.logout // /auth/logout - -// Warehouses -ApiEndpoints.warehouses // /warehouses -ApiEndpoints.warehouseById(1) // /warehouses/1 - -// Products -ApiEndpoints.products // /products -ApiEndpoints.productById(123) // /products/123 - -// Query parameters helper -ApiEndpoints.productQueryParams( - warehouseId: 1, - type: 'import', -) // {warehouseId: 1, type: 'import'} -``` - -## Utilities - -### Test Connection - -```dart -final isConnected = await apiClient.testConnection(); -``` - -### Update Base URL - -```dart -apiClient.updateBaseUrl('https://dev-api.example.com'); -``` - -### Get Current Token - -```dart -final token = await apiClient.getAccessToken(); -``` - -## Common Patterns - -### Login Flow - -```dart -// 1. Login -final response = await apiClient.post( - ApiEndpoints.login, - data: {'username': username, 'password': password}, -); - -// 2. Parse -final apiResponse = ApiResponse.fromJson( - response.data, - (json) => User.fromJson(json), -); - -// 3. Save tokens -if (apiResponse.isSuccess && apiResponse.value != null) { - final user = apiResponse.value!; - await secureStorage.saveAccessToken(user.accessToken); - await secureStorage.saveUserId(user.userId); -} -``` - -### Repository Pattern - -```dart -class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { - final ApiClient apiClient; - - WarehouseRemoteDataSourceImpl(this.apiClient); - - @override - Future> getWarehouses() async { - final response = await apiClient.get(ApiEndpoints.warehouses); - - final apiResponse = ApiResponse.fromJson( - response.data, - (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), - ); - - if (apiResponse.isSuccess && apiResponse.value != null) { - return apiResponse.value!; - } else { - throw ServerException(apiResponse.getErrorMessage()); - } - } -} -``` - -## Configuration - -### Set Base URL - -In `lib/core/constants/app_constants.dart`: - -```dart -static const String apiBaseUrl = 'https://api.example.com'; -``` - -### Set Timeouts - -In `lib/core/constants/app_constants.dart`: - -```dart -static const int connectionTimeout = 30000; // 30 seconds -static const int receiveTimeout = 30000; -static const int sendTimeout = 30000; -``` - -## Files Location - -- API Client: `/lib/core/network/api_client.dart` -- API Response: `/lib/core/network/api_response.dart` -- Secure Storage: `/lib/core/storage/secure_storage.dart` -- API Endpoints: `/lib/core/constants/api_endpoints.dart` -- Examples: `/lib/core/network/api_client_example.dart` -- Documentation: `/lib/core/network/README.md` - -## Important Notes - -1. **Automatic Token Injection**: Bearer token is automatically added to all requests -2. **401 Handling**: 401 errors automatically clear tokens and trigger `onUnauthorized` callback -3. **Logging**: All requests/responses are logged with sensitive data redacted -4. **Singleton Storage**: SecureStorage is a singleton - use `SecureStorage()` everywhere -5. **Error Codes**: ServerException includes error codes (e.g., '401', '404', '500') - -## Common Issues - -### Token not injected? -Check if token exists: `await secureStorage.getAccessToken()` - -### 401 not clearing tokens? -Verify `onUnauthorized` callback is set in ApiClient constructor - -### Connection timeout? -Check network, verify base URL, increase timeout in constants - -### Logs not showing? -Check Flutter DevTools console or developer.log output diff --git a/ROUTER_SETUP.md b/ROUTER_SETUP.md deleted file mode 100644 index 9d16535..0000000 --- a/ROUTER_SETUP.md +++ /dev/null @@ -1,426 +0,0 @@ -# GoRouter Navigation Setup - Complete Guide - -This document explains the complete navigation setup for the warehouse management app using GoRouter with authentication-based redirects. - -## Files Created/Modified - -### New Files -1. **`/lib/core/router/app_router.dart`** - Main router configuration -2. **`/lib/core/router/README.md`** - Detailed router documentation -3. **`/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart`** - Integration examples - -### Modified Files -1. **`/lib/main.dart`** - Updated to use new router provider -2. **`/lib/features/operation/presentation/pages/operation_selection_page.dart`** - Updated navigation - -## Architecture Overview - -### Route Structure -``` -/login → LoginPage -/warehouses → WarehouseSelectionPage -/operations → OperationSelectionPage (requires warehouse) -/products → ProductsPage (requires warehouse + operationType) -``` - -### Navigation Flow -``` -┌─────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ -│ Login │ --> │ Warehouses │ --> │ Operations │ --> │ Products │ -└─────────┘ └────────────┘ └────────────┘ └──────────┘ - │ │ │ │ - └─────────────────┴──────────────────┴──────────────────┘ - Protected Routes - (Require Authentication via SecureStorage) -``` - -## Key Features - -### 1. Authentication-Based Redirects -- **Unauthenticated users** → Redirected to `/login` -- **Authenticated users on /login** → Redirected to `/warehouses` -- Uses `SecureStorage.isAuthenticated()` to check access token - -### 2. Type-Safe Navigation -Extension methods provide type-safe navigation: -```dart -// Type-safe with auto-completion -context.goToOperations(warehouse); -context.goToProducts(warehouse: warehouse, operationType: 'import'); - -// vs. error-prone manual navigation -context.go('/operations', extra: warehouse); // Less safe -``` - -### 3. Parameter Validation -Routes validate required parameters and redirect on error: -```dart -final warehouse = state.extra as WarehouseEntity?; -if (warehouse == null) { - // Show error and redirect to safe page - return _ErrorScreen(message: 'Warehouse data is required'); -} -``` - -### 4. Reactive Navigation -Router automatically reacts to authentication state changes: -```dart -// Login → Router detects auth change → Redirects to /warehouses -await ref.read(authProvider.notifier).login(username, password); - -// Logout → Router detects auth change → Redirects to /login -await ref.read(authProvider.notifier).logout(); -``` - -## Usage Guide - -### Basic Navigation - -#### 1. Navigate to Login -```dart -context.goToLogin(); -``` - -#### 2. Navigate to Warehouses -```dart -context.goToWarehouses(); -``` - -#### 3. Navigate to Operations with Warehouse -```dart -// From warehouse selection page -void onWarehouseSelected(WarehouseEntity warehouse) { - context.goToOperations(warehouse); -} -``` - -#### 4. Navigate to Products with Warehouse and Operation -```dart -// From operation selection page -void onOperationSelected(WarehouseEntity warehouse, String operationType) { - context.goToProducts( - warehouse: warehouse, - operationType: operationType, // 'import' or 'export' - ); -} -``` - -### Complete Integration Example - -#### Warehouse Selection Page -```dart -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:minhthu/core/router/app_router.dart'; -import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart'; - -class WarehouseSelectionPage extends ConsumerWidget { - const WarehouseSelectionPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Watch warehouse state - final state = ref.watch(warehouseProvider); - - return Scaffold( - appBar: AppBar( - title: const Text('Select Warehouse'), - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () async { - // Logout - router will auto-redirect to login - await ref.read(authProvider.notifier).logout(); - }, - ), - ], - ), - body: ListView.builder( - itemCount: state.warehouses.length, - itemBuilder: (context, index) { - final warehouse = state.warehouses[index]; - return ListTile( - title: Text(warehouse.name), - subtitle: Text(warehouse.code), - onTap: () { - // Type-safe navigation to operations - context.goToOperations(warehouse); - }, - ); - }, - ), - ); - } -} -``` - -#### Operation Selection Page -```dart -import 'package:flutter/material.dart'; -import 'package:minhthu/core/router/app_router.dart'; -import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart'; - -class OperationSelectionPage extends StatelessWidget { - final WarehouseEntity warehouse; - - const OperationSelectionPage({required this.warehouse}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(warehouse.name)), - body: Column( - children: [ - ElevatedButton( - onPressed: () { - // Navigate to products with import operation - context.goToProducts( - warehouse: warehouse, - operationType: 'import', - ); - }, - child: const Text('Import Products'), - ), - ElevatedButton( - onPressed: () { - // Navigate to products with export operation - context.goToProducts( - warehouse: warehouse, - operationType: 'export', - ); - }, - child: const Text('Export Products'), - ), - ], - ), - ); - } -} -``` - -## Authentication Integration - -### How It Works - -1. **App Starts** - - Router checks `SecureStorage.isAuthenticated()` - - If no token → Redirects to `/login` - - If token exists → Allows navigation - -2. **User Logs In** - ```dart - // AuthNotifier saves token and updates state - await loginUseCase(request); // Saves to SecureStorage - state = AuthState.authenticated(user); - - // GoRouterRefreshStream detects auth state change - ref.listen(authProvider, (_, __) => notifyListeners()); - - // Router re-evaluates redirect logic - // User is now authenticated → Redirects to /warehouses - ``` - -3. **User Logs Out** - ```dart - // AuthNotifier clears token and resets state - await secureStorage.clearAll(); - state = const AuthState.initial(); - - // Router detects auth state change - // User is no longer authenticated → Redirects to /login - ``` - -### SecureStorage Methods Used -```dart -// Check authentication -Future isAuthenticated() async { - final token = await getAccessToken(); - return token != null && token.isNotEmpty; -} - -// Save tokens (during login) -Future saveAccessToken(String token); -Future saveRefreshToken(String token); - -// Clear tokens (during logout) -Future clearAll(); -``` - -## Error Handling - -### 1. Missing Route Parameters -If required parameters are missing, user sees error and gets redirected: -```dart -GoRoute( - path: '/operations', - builder: (context, state) { - final warehouse = state.extra as WarehouseEntity?; - - if (warehouse == null) { - // Show error screen and redirect after frame - WidgetsBinding.instance.addPostFrameCallback((_) { - context.go('/warehouses'); - }); - return const _ErrorScreen( - message: 'Warehouse data is required', - ); - } - - return OperationSelectionPage(warehouse: warehouse); - }, -) -``` - -### 2. Page Not Found -Custom 404 page with navigation back to login: -```dart -errorBuilder: (context, state) { - return Scaffold( - body: Center( - child: Column( - children: [ - Icon(Icons.error_outline, size: 64), - Text('Page "${state.uri.path}" does not exist'), - ElevatedButton( - onPressed: () => context.go('/login'), - child: const Text('Go to Login'), - ), - ], - ), - ), - ); -} -``` - -### 3. Authentication Errors -If `SecureStorage` throws an error, redirect to login for safety: -```dart -Future _handleRedirect(context, state) async { - try { - final isAuthenticated = await secureStorage.isAuthenticated(); - // ... redirect logic - } catch (e) { - debugPrint('Error in redirect: $e'); - return '/login'; // Safe fallback - } -} -``` - -## Extension Methods Reference - -### Path-Based Navigation -```dart -context.goToLogin(); // Go to /login -context.goToWarehouses(); // Go to /warehouses -context.goToOperations(warehouse); -context.goToProducts(warehouse: w, operationType: 'import'); -context.goBack(); // Pop current route -``` - -### Named Route Navigation -```dart -context.goToLoginNamed(); -context.goToWarehousesNamed(); -context.goToOperationsNamed(warehouse); -context.goToProductsNamed(warehouse: w, operationType: 'export'); -``` - -## Testing Authentication Flow - -### Test Case 1: Fresh Install -1. App starts → No token → Redirects to `/login` -2. User logs in → Token saved → Redirects to `/warehouses` -3. User selects warehouse → Navigates to `/operations` -4. User selects operation → Navigates to `/products` - -### Test Case 2: Logged In User -1. App starts → Token exists → Shows `/warehouses` -2. User navigates normally through app -3. User logs out → Token cleared → Redirects to `/login` - -### Test Case 3: Manual URL Entry -1. User tries to access `/products` directly -2. Router checks authentication -3. If not authenticated → Redirects to `/login` -4. If authenticated but missing params → Redirects to `/warehouses` - -## Troubleshooting - -### Problem: Stuck on login page after successful login -**Solution**: Check if token is being saved to SecureStorage -```dart -// In LoginUseCase -await secureStorage.saveAccessToken(user.accessToken); -``` - -### Problem: Redirect loop between login and warehouses -**Solution**: Verify `isAuthenticated()` logic -```dart -// Should return true only if token exists -Future isAuthenticated() async { - final token = await getAccessToken(); - return token != null && token.isNotEmpty; -} -``` - -### Problem: Navigation parameters are null -**Solution**: Use extension methods with correct types -```dart -// Correct -context.goToOperations(warehouse); - -// Wrong - may lose type information -context.go('/operations', extra: warehouse); -``` - -### Problem: Router doesn't react to auth changes -**Solution**: Verify GoRouterRefreshStream is listening -```dart -GoRouterRefreshStream(this.ref) { - ref.listen( - authProvider, // Must be the correct provider - (_, __) => notifyListeners(), - ); -} -``` - -## Next Steps - -1. **Implement Warehouse Provider** - - Create warehouse state management - - Load warehouses from API - - Integrate with warehouse selection page - -2. **Implement Products Provider** - - Create products state management - - Load products based on warehouse and operation - - Integrate with products page - -3. **Add Loading States** - - Show loading indicators during navigation - - Handle network errors gracefully - -4. **Add Analytics** - - Track navigation events - - Monitor authentication flow - -## Related Documentation - -- **Router Details**: `/lib/core/router/README.md` -- **Auth Setup**: `/lib/features/auth/di/auth_dependency_injection.dart` -- **SecureStorage**: `/lib/core/storage/secure_storage.dart` -- **Examples**: `/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart` - -## Summary - -The complete GoRouter setup provides: -- Authentication-based navigation with auto-redirect -- Type-safe parameter passing -- Reactive updates on auth state changes -- Proper error handling and validation -- Easy-to-use extension methods -- Integration with existing SecureStorage and Riverpod - -The app flow is: **Login → Warehouses → Operations → Products** - -All protected routes automatically redirect to login if user is not authenticated. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 60650de..11941d9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -38,11 +38,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - printing: 233e1b73bd1f4a05615548e9b5a324c98588640b - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/lib/core/services/print_service.dart b/lib/core/services/print_service.dart index e8570da..ec07369 100644 --- a/lib/core/services/print_service.dart +++ b/lib/core/services/print_service.dart @@ -19,6 +19,7 @@ class PrintService { required double issuedKg, required int issuedPcs, String? responsibleName, + String? receiverName, String? barcodeData, }) async { // Load Vietnamese-compatible fonts using PdfGoogleFonts @@ -323,6 +324,35 @@ class PrintService { pw.SizedBox(height: 12), + + pw.Container( + decoration: pw.BoxDecoration( + border: pw.Border.all(color: PdfColors.black, width: 0.5), + borderRadius: pw.BorderRadius.circular(8), + ), + padding: const pw.EdgeInsets.all(8), + child: pw.Row( + children: [ + pw.Text( + 'Nhân viên tiếp nhận: ', + style: pw.TextStyle( + fontSize: 10, + color: PdfColors.grey700, + ), + ), + pw.Text( + receiverName ?? '-', + style: pw.TextStyle( + fontSize: 12, + fontWeight: pw.FontWeight.bold, + ), + ), + ], + ), + ), + + pw.SizedBox(height: 12), + // Barcode section if (barcodeData != null && barcodeData.isNotEmpty) pw.Center( diff --git a/lib/core/storage/secure_storage.dart b/lib/core/storage/secure_storage.dart index 0351c29..c0b2542 100644 --- a/lib/core/storage/secure_storage.dart +++ b/lib/core/storage/secure_storage.dart @@ -52,6 +52,9 @@ class SecureStorage { /// Key for storing username static const String _usernameKey = 'username'; + /// Key for storing email + static const String _emailKey = 'email'; + // ==================== Token Management ==================== /// Save access token securely @@ -126,6 +129,24 @@ class SecureStorage { } } + /// Save email + Future saveEmail(String email) async { + try { + await _storage.write(key: _emailKey, value: email); + } catch (e) { + throw Exception('Failed to save email: $e'); + } + } + + /// Get email + Future getEmail() async { + try { + return await _storage.read(key: _emailKey); + } catch (e) { + throw Exception('Failed to read email: $e'); + } + } + /// Check if user is authenticated (has valid access token) Future isAuthenticated() async { final token = await getAccessToken(); diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart index 436c111..be7d8ba 100644 --- a/lib/features/auth/data/repositories/auth_repository_impl.dart +++ b/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepository { await secureStorage.saveAccessToken(userModel.accessToken); await secureStorage.saveUserId(userModel.userId); await secureStorage.saveUsername(userModel.username); + // Save email (username is the email from login) + await secureStorage.saveEmail(request.username); if (userModel.refreshToken != null) { await secureStorage.saveRefreshToken(userModel.refreshToken!); diff --git a/lib/features/products/presentation/pages/product_detail_page.dart b/lib/features/products/presentation/pages/product_detail_page.dart index b4787c3..5e8163d 100644 --- a/lib/features/products/presentation/pages/product_detail_page.dart +++ b/lib/features/products/presentation/pages/product_detail_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/di/providers.dart'; import '../../../../core/services/print_service.dart'; +import '../../../../core/storage/secure_storage.dart'; import '../../../../core/utils/text_utils.dart'; import '../../../users/domain/entities/user_entity.dart'; import '../../data/models/create_product_warehouse_request.dart'; @@ -55,6 +56,9 @@ class _ProductDetailPageState extends ConsumerState { // Load users from Hive (no API call) await ref.read(usersProvider.notifier).getUsers(); + // Auto-select warehouse user based on stored email + await _autoSelectWarehouseUser(); + await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail( widget.warehouseId, widget.productId, @@ -82,6 +86,43 @@ class _ProductDetailPageState extends ConsumerState { super.dispose(); } + /// Auto-select warehouse user based on stored email from login + Future _autoSelectWarehouseUser() async { + try { + // Get stored email from secure storage + final secureStorage = SecureStorage(); + final storedEmail = await secureStorage.getEmail(); + + if (storedEmail == null || storedEmail.isEmpty) { + return; + } + + print(storedEmail); + + // Get all warehouse users + final warehouseUsers = ref.read(usersListProvider) + .where((user) => user.isWareHouseUser) + .toList(); + + // Find user with matching email + final matchingUsers = warehouseUsers + .where((user) => user.email.toLowerCase() == storedEmail.toLowerCase()) + .toList(); + + final matchingUser = matchingUsers.isNotEmpty ? matchingUsers.first : null; + + // Set selected warehouse user only if a match is found + if (matchingUser != null && mounted) { + setState(() { + _selectedWarehouseUser = matchingUser; + }); + } + } catch (e) { + // Silently fail - user can still manually select + debugPrint('Error auto-selecting warehouse user: $e'); + } + } + Future _onRefresh() async { // await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail( // widget.warehouseId, @@ -476,7 +517,7 @@ class _ProductDetailPageState extends ConsumerState { // Get responsible user name final responsibleName = '${_selectedWarehouseUser!.name} ${_selectedWarehouseUser!.firstName}'; - + final receiverName = '${_selectedEmployee!.name} ${_selectedEmployee!.firstName}'; // Generate barcode data (using product code or product ID) final barcodeData = stage.productCode.isNotEmpty ? stage.productCode @@ -495,6 +536,7 @@ class _ProductDetailPageState extends ConsumerState { issuedKg: finalIssuedKg, issuedPcs: finalIssuedPcs, responsibleName: responsibleName, + receiverName: receiverName, barcodeData: barcodeData, ); } catch (e) {