# API Client Setup - Complete Implementation ## Overview I have created a robust API client for your Flutter warehouse management app with comprehensive features including: - **Automatic token management** from secure storage - **401 error handling** with automatic token clearing - **Request/response logging** with sensitive data redaction - **Configurable timeouts** (30 seconds) - **Proper error transformation** to custom exceptions - **Support for all HTTP methods** (GET, POST, PUT, DELETE) ## Files Created ### 1. Core Network Files #### `/lib/core/network/api_client.dart` - Main API client implementation using Dio - Automatic Bearer token injection from secure storage - Request/response/error interceptors with comprehensive logging - 401 error handler that clears tokens and triggers logout callback - Methods: `get()`, `post()`, `put()`, `delete()` - Utility methods: `testConnection()`, `isAuthenticated()`, `getAccessToken()`, `clearAuth()` #### `/lib/core/network/api_response.dart` - Generic API response wrapper matching your backend format - Structure: `Value`, `IsSuccess`, `IsFailure`, `Errors`, `ErrorCodes` - Helper methods: `hasData`, `getErrorMessage()`, `getAllErrorsAsString()`, `hasErrorCode()` #### `/lib/core/network/api_client_example.dart` - Comprehensive usage examples for all scenarios - Examples for: Login, GET/POST/PUT/DELETE requests, error handling, etc. #### `/lib/core/network/README.md` - Complete documentation for the API client - Usage guides, best practices, troubleshooting ### 2. Secure Storage #### `/lib/core/storage/secure_storage.dart` - Singleton wrapper for flutter_secure_storage - Token management: `saveAccessToken()`, `getAccessToken()`, `clearTokens()` - User data: `saveUserId()`, `saveUsername()`, etc. - Utility methods: `isAuthenticated()`, `clearAll()`, `containsKey()` - Platform-specific secure options (Android: encrypted shared prefs, iOS: Keychain) ### 3. Constants #### `/lib/core/constants/api_endpoints.dart` - Centralized API endpoint definitions - Authentication: `/auth/login`, `/auth/logout` - Warehouses: `/warehouses` - Products: `/products` with query parameter helpers - Scans: `/api/scans` ### 4. Core Exports #### `/lib/core/core.dart` (Updated) - Added exports for new modules: - `api_endpoints.dart` - `api_response.dart` - `secure_storage.dart` ### 5. Dependencies #### `pubspec.yaml` (Updated) - Added `flutter_secure_storage: ^9.0.0` ## Key Features ### 1. Automatic Token Management The API client automatically injects Bearer tokens into all requests: ```dart // Initialize with secure storage final secureStorage = SecureStorage(); final apiClient = ApiClient(secureStorage); // Token is automatically added to all requests final response = await apiClient.get('/warehouses'); // Request header: Authorization: Bearer ``` ### 2. 401 Error Handling When a 401 Unauthorized error occurs: 1. Error is logged to console 2. All tokens are cleared from secure storage 3. `onUnauthorized` callback is triggered 4. App can navigate to login screen ```dart final apiClient = ApiClient( secureStorage, onUnauthorized: () { // This callback is triggered on 401 errors context.go('/login'); // Navigate to login }, ); ``` ### 3. Comprehensive Logging All requests, responses, and errors are logged with sensitive data redacted: ``` REQUEST[GET] => https://api.example.com/warehouses Headers: {Authorization: ***REDACTED***, Content-Type: application/json} RESPONSE[200] => https://api.example.com/warehouses Data: {...} ERROR[401] => https://api.example.com/products 401 Unauthorized - Clearing tokens and triggering logout ``` ### 4. Error Handling Dio exceptions are transformed into custom app exceptions: ```dart try { final response = await apiClient.get('/products'); } on NetworkException catch (e) { // Timeout, no internet, etc. print('Network error: ${e.message}'); } on ServerException catch (e) { // 4xx, 5xx errors print('Server error: ${e.message}'); print('Error code: ${e.code}'); // e.g., '401', '404', '500' } ``` Specific error codes: - `401`: Unauthorized (automatically handled) - `403`: Forbidden - `404`: Not Found - `422`: Validation Error - `429`: Rate Limited - `500-504`: Server Errors ### 5. API Response Parsing All API responses follow the standard format: ```dart final response = await apiClient.get('/warehouses'); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { final warehouses = apiResponse.value; print('Found ${warehouses.length} warehouses'); } else { print('Error: ${apiResponse.getErrorMessage()}'); } ``` ## Usage Examples ### Initialize API Client ```dart import 'package:minhthu/core/core.dart'; final secureStorage = SecureStorage(); final apiClient = ApiClient( secureStorage, onUnauthorized: () { // Navigate to login on 401 errors context.go('/login'); }, ); ``` ### Login Flow ```dart // 1. Login request final response = await apiClient.post( ApiEndpoints.login, data: { 'username': 'user@example.com', 'password': 'password123', }, ); // 2. Parse response final apiResponse = ApiResponse.fromJson( response.data, (json) => User.fromJson(json), ); // 3. Save tokens (typically done in LoginUseCase) if (apiResponse.isSuccess && apiResponse.value != null) { final user = apiResponse.value!; await secureStorage.saveAccessToken(user.accessToken); await secureStorage.saveRefreshToken(user.refreshToken); await secureStorage.saveUserId(user.userId); } ``` ### Get Warehouses (Authenticated) ```dart // Token is automatically added by the interceptor final response = await apiClient.get(ApiEndpoints.warehouses); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { final warehouses = apiResponse.value!; // Use the data } ``` ### Get Products with Query Parameters ```dart final response = await apiClient.get( ApiEndpoints.products, queryParameters: ApiEndpoints.productQueryParams( warehouseId: 1, type: 'import', ), ); ``` ### Save Scan Data ```dart final response = await apiClient.post( ApiEndpoints.scans, data: { 'barcode': '1234567890', 'field1': 'Value 1', 'field2': 'Value 2', 'field3': 'Value 3', 'field4': 'Value 4', }, ); ``` ### Check Authentication Status ```dart final isAuthenticated = await apiClient.isAuthenticated(); if (!isAuthenticated) { // Navigate to login } ``` ### Logout ```dart await apiClient.clearAuth(); // Clears all tokens ``` ## Integration with Repository Pattern The API client is designed to work with your clean architecture: ```dart class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { final ApiClient apiClient; WarehouseRemoteDataSourceImpl(this.apiClient); @override Future> getWarehouses() async { final response = await apiClient.get(ApiEndpoints.warehouses); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException(apiResponse.getErrorMessage()); } } } ``` ## Configuration ### Timeouts (in `app_constants.dart`) ```dart static const int connectionTimeout = 30000; // 30 seconds static const int receiveTimeout = 30000; // 30 seconds static const int sendTimeout = 30000; // 30 seconds ``` ### Base URL (in `app_constants.dart`) ```dart static const String apiBaseUrl = 'https://api.example.com'; ``` Or update dynamically: ```dart apiClient.updateBaseUrl('https://dev-api.example.com'); ``` ## Security Features 1. **Token Encryption**: Tokens stored using platform-specific secure storage - Android: Encrypted SharedPreferences - iOS: Keychain with `first_unlock` accessibility 2. **Automatic Token Clearing**: 401 errors automatically clear all tokens 3. **Log Sanitization**: Authorization headers redacted in logs ``` Headers: {Authorization: ***REDACTED***} ``` 4. **Singleton Pattern**: SecureStorage uses singleton to prevent multiple instances ## Testing To test the API connection: ```dart final isConnected = await apiClient.testConnection(); if (!isConnected) { print('Cannot connect to API'); } ``` ## Dependency Injection (GetIt) Register with GetIt service locator: ```dart final getIt = GetIt.instance; // Register SecureStorage getIt.registerLazySingleton(() => SecureStorage()); // Register ApiClient getIt.registerLazySingleton( () => ApiClient( getIt(), onUnauthorized: () { // Handle unauthorized }, ), ); ``` ## File Structure ``` lib/ core/ constants/ app_constants.dart # Existing - timeouts and base URL api_endpoints.dart # NEW - API endpoint constants network/ api_client.dart # UPDATED - Full implementation api_response.dart # NEW - API response wrapper api_client_example.dart # NEW - Usage examples README.md # NEW - Documentation storage/ secure_storage.dart # NEW - Secure storage wrapper errors/ exceptions.dart # Existing - Used by API client failures.dart # Existing - Used by repositories core.dart # UPDATED - Added new exports ``` ## Next Steps 1. **Update Existing Repositories**: Update your remote data sources to use the new API client 2. **Configure Base URL**: Set the correct API base URL in `app_constants.dart` 3. **Set Up Navigation**: Implement the `onUnauthorized` callback to navigate to login 4. **Add API Endpoints**: Add any missing endpoints to `api_endpoints.dart` 5. **Test Authentication Flow**: Test login, token injection, and 401 handling ## Testing the Setup Run the example: ```dart import 'package:minhthu/core/network/api_client_example.dart'; void main() async { await runExamples(); } ``` ## Troubleshooting ### Token not being injected - Verify token is saved: `await secureStorage.getAccessToken()` - Check logs for: `REQUEST[...] Headers: {Authorization: ***REDACTED***}` ### 401 errors not clearing tokens - Verify `onUnauthorized` callback is set - Check logs for: `401 Unauthorized - Clearing tokens and triggering logout` ### Connection timeouts - Check network connectivity - Verify base URL is correct - Increase timeout values if needed ## Analysis Results All files pass Flutter analysis with no issues: - ✅ `api_client.dart` - No issues found - ✅ `secure_storage.dart` - No issues found - ✅ `api_response.dart` - No issues found - ✅ `api_endpoints.dart` - No issues found ## Documentation For detailed documentation, see: - `/lib/core/network/README.md` - Complete API client documentation - `/lib/core/network/api_client_example.dart` - Code examples ## Summary The API client is production-ready with: - ✅ Automatic token management from secure storage - ✅ Request interceptor to inject Bearer tokens - ✅ Response interceptor for logging - ✅ Error interceptor to handle 401 errors - ✅ Automatic token clearing on unauthorized access - ✅ Comprehensive error handling - ✅ Request/response logging with sensitive data redaction - ✅ Support for all HTTP methods (GET, POST, PUT, DELETE) - ✅ Configurable timeouts (30 seconds) - ✅ Environment-specific base URLs - ✅ Connection testing - ✅ Clean integration with repository pattern - ✅ Comprehensive documentation and examples - ✅ All files pass static analysis The API client is ready to use and follows Flutter best practices and clean architecture principles!