fix
This commit is contained in:
@@ -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!
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
426
ROUTER_SETUP.md
426
ROUTER_SETUP.md
@@ -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.
|
||||
@@ -38,11 +38,11 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class PrintService {
|
||||
required double issuedKg,
|
||||
required int issuedPcs,
|
||||
String? responsibleName,
|
||||
String? receiverName,
|
||||
String? barcodeData,
|
||||
}) async {
|
||||
// Load Vietnamese-compatible fonts using PdfGoogleFonts
|
||||
@@ -323,6 +324,35 @@ class PrintService {
|
||||
|
||||
pw.SizedBox(height: 12),
|
||||
|
||||
|
||||
pw.Container(
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.black, width: 0.5),
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Row(
|
||||
children: [
|
||||
pw.Text(
|
||||
'Nhân viên tiếp nhận: ',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
color: PdfColors.grey700,
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
receiverName ?? '-',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 12),
|
||||
|
||||
// Barcode section
|
||||
if (barcodeData != null && barcodeData.isNotEmpty)
|
||||
pw.Center(
|
||||
|
||||
@@ -52,6 +52,9 @@ class SecureStorage {
|
||||
/// Key for storing username
|
||||
static const String _usernameKey = 'username';
|
||||
|
||||
/// Key for storing email
|
||||
static const String _emailKey = 'email';
|
||||
|
||||
// ==================== Token Management ====================
|
||||
|
||||
/// Save access token securely
|
||||
@@ -126,6 +129,24 @@ class SecureStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Save email
|
||||
Future<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)
|
||||
Future<bool> isAuthenticated() async {
|
||||
final token = await getAccessToken();
|
||||
|
||||
@@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
await secureStorage.saveAccessToken(userModel.accessToken);
|
||||
await secureStorage.saveUserId(userModel.userId);
|
||||
await secureStorage.saveUsername(userModel.username);
|
||||
// Save email (username is the email from login)
|
||||
await secureStorage.saveEmail(request.username);
|
||||
|
||||
if (userModel.refreshToken != null) {
|
||||
await secureStorage.saveRefreshToken(userModel.refreshToken!);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../core/di/providers.dart';
|
||||
import '../../../../core/services/print_service.dart';
|
||||
import '../../../../core/storage/secure_storage.dart';
|
||||
import '../../../../core/utils/text_utils.dart';
|
||||
import '../../../users/domain/entities/user_entity.dart';
|
||||
import '../../data/models/create_product_warehouse_request.dart';
|
||||
@@ -55,6 +56,9 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
// Load users from Hive (no API call)
|
||||
await ref.read(usersProvider.notifier).getUsers();
|
||||
|
||||
// Auto-select warehouse user based on stored email
|
||||
await _autoSelectWarehouseUser();
|
||||
|
||||
await ref.read(productDetailProvider(_providerKey).notifier).loadProductDetail(
|
||||
widget.warehouseId,
|
||||
widget.productId,
|
||||
@@ -82,6 +86,43 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
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 {
|
||||
// await ref.read(productDetailProvider(_providerKey).notifier).refreshProductDetail(
|
||||
// widget.warehouseId,
|
||||
@@ -476,7 +517,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
|
||||
// Get responsible user name
|
||||
final responsibleName = '${_selectedWarehouseUser!.name} ${_selectedWarehouseUser!.firstName}';
|
||||
|
||||
final receiverName = '${_selectedEmployee!.name} ${_selectedEmployee!.firstName}';
|
||||
// Generate barcode data (using product code or product ID)
|
||||
final barcodeData = stage.productCode.isNotEmpty
|
||||
? stage.productCode
|
||||
@@ -495,6 +536,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
||||
issuedKg: finalIssuedKg,
|
||||
issuedPcs: finalIssuedPcs,
|
||||
responsibleName: responsibleName,
|
||||
receiverName: receiverName,
|
||||
barcodeData: barcodeData,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user