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