This commit is contained in:
2025-11-02 20:40:11 +07:00
parent efcc6306b0
commit 2495330bf5
11 changed files with 101 additions and 2249 deletions

View File

@@ -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 <token>
```
### 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<List<Warehouse>> 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>(() => SecureStorage());
// Register ApiClient
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(
getIt<SecureStorage>(),
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!

View File

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

View File

@@ -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": <data>,
"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.

View File

@@ -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<Failure, Success>
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<Failure, Success>
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.

View File

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

View File

@@ -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<bool> isAuthenticated() async {
final token = await getAccessToken();
return token != null && token.isNotEmpty;
}
// Save tokens (during login)
Future<void> saveAccessToken(String token);
Future<void> saveRefreshToken(String token);
// Clear tokens (during logout)
Future<void> 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<String?> _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<bool> 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.

View File

@@ -38,11 +38,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -19,6 +19,7 @@ class PrintService {
required double issuedKg, required double issuedKg,
required int issuedPcs, required int issuedPcs,
String? responsibleName, String? responsibleName,
String? receiverName,
String? barcodeData, String? barcodeData,
}) async { }) async {
// Load Vietnamese-compatible fonts using PdfGoogleFonts // Load Vietnamese-compatible fonts using PdfGoogleFonts
@@ -323,6 +324,35 @@ class PrintService {
pw.SizedBox(height: 12), 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 // Barcode section
if (barcodeData != null && barcodeData.isNotEmpty) if (barcodeData != null && barcodeData.isNotEmpty)
pw.Center( pw.Center(

View File

@@ -52,6 +52,9 @@ class SecureStorage {
/// Key for storing username /// Key for storing username
static const String _usernameKey = 'username'; static const String _usernameKey = 'username';
/// Key for storing email
static const String _emailKey = 'email';
// ==================== Token Management ==================== // ==================== Token Management ====================
/// Save access token securely /// Save access token securely
@@ -126,6 +129,24 @@ class SecureStorage {
} }
} }
/// Save email
Future<void> saveEmail(String email) async {
try {
await _storage.write(key: _emailKey, value: email);
} catch (e) {
throw Exception('Failed to save email: $e');
}
}
/// Get email
Future<String?> 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) /// Check if user is authenticated (has valid access token)
Future<bool> isAuthenticated() async { Future<bool> isAuthenticated() async {
final token = await getAccessToken(); final token = await getAccessToken();

View File

@@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepository {
await secureStorage.saveAccessToken(userModel.accessToken); await secureStorage.saveAccessToken(userModel.accessToken);
await secureStorage.saveUserId(userModel.userId); await secureStorage.saveUserId(userModel.userId);
await secureStorage.saveUsername(userModel.username); await secureStorage.saveUsername(userModel.username);
// Save email (username is the email from login)
await secureStorage.saveEmail(request.username);
if (userModel.refreshToken != null) { if (userModel.refreshToken != null) {
await secureStorage.saveRefreshToken(userModel.refreshToken!); await secureStorage.saveRefreshToken(userModel.refreshToken!);

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/di/providers.dart'; import '../../../../core/di/providers.dart';
import '../../../../core/services/print_service.dart'; import '../../../../core/services/print_service.dart';
import '../../../../core/storage/secure_storage.dart';
import '../../../../core/utils/text_utils.dart'; import '../../../../core/utils/text_utils.dart';
import '../../../users/domain/entities/user_entity.dart'; import '../../../users/domain/entities/user_entity.dart';
import '../../data/models/create_product_warehouse_request.dart'; import '../../data/models/create_product_warehouse_request.dart';
@@ -55,6 +56,9 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
// Load users from Hive (no API call) // Load users from Hive (no API call)
await ref.read(usersProvider.notifier).getUsers(); await ref.read(usersProvider.notifier).getUsers();
// Auto-select warehouse user based on stored email
await _autoSelectWarehouseUser();
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail( await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
widget.warehouseId, widget.warehouseId,
widget.productId, widget.productId,
@@ -82,6 +86,43 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
super.dispose(); super.dispose();
} }
/// Auto-select warehouse user based on stored email from login
Future<void> _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<void> _onRefresh() async { Future<void> _onRefresh() async {
// await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail( // await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
// widget.warehouseId, // widget.warehouseId,
@@ -476,7 +517,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
// Get responsible user name // Get responsible user name
final responsibleName = '${_selectedWarehouseUser!.name} ${_selectedWarehouseUser!.firstName}'; final responsibleName = '${_selectedWarehouseUser!.name} ${_selectedWarehouseUser!.firstName}';
final receiverName = '${_selectedEmployee!.name} ${_selectedEmployee!.firstName}';
// Generate barcode data (using product code or product ID) // Generate barcode data (using product code or product ID)
final barcodeData = stage.productCode.isNotEmpty final barcodeData = stage.productCode.isNotEmpty
? stage.productCode ? stage.productCode
@@ -495,6 +536,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
issuedKg: finalIssuedKg, issuedKg: finalIssuedKg,
issuedPcs: finalIssuedPcs, issuedPcs: finalIssuedPcs,
responsibleName: responsibleName, responsibleName: responsibleName,
receiverName: receiverName,
barcodeData: barcodeData, barcodeData: barcodeData,
); );
} catch (e) { } catch (e) {