fill
This commit is contained in:
452
API_CLIENT_SETUP.md
Normal file
452
API_CLIENT_SETUP.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# 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!
|
||||
Reference in New Issue
Block a user