# API Client - Network Module

A robust API client for the Flutter warehouse management app, built on top of Dio with comprehensive error handling, authentication management, and request/response logging.

## Features

- **Automatic Token Management**: Automatically injects Bearer tokens from secure storage
- **401 Error Handling**: Automatically clears tokens and triggers logout on unauthorized access
- **Request/Response Logging**: Comprehensive logging for debugging with sensitive data redaction
- **Error Transformation**: Converts Dio exceptions to custom app exceptions
- **Timeout Configuration**: Configurable connection, receive, and send timeouts (30 seconds)
- **Secure Storage Integration**: Uses flutter_secure_storage for token management
- **Environment Support**: Easy base URL switching for different environments

## Files

- `api_client.dart` - Main API client implementation
- `api_response.dart` - Generic API response wrapper matching backend format
- `api_client_example.dart` - Comprehensive usage examples
- `README.md` - This documentation

## Installation

The API client requires the following dependencies (already added to `pubspec.yaml`):

```yaml
dependencies:
  dio: ^5.3.2
  flutter_secure_storage: ^9.0.0
```

## Quick Start

### 1. Initialize API Client

```dart
import 'package:minhthu/core/core.dart';

// Create secure storage instance
final secureStorage = SecureStorage();

// Create API client with unauthorized callback
final apiClient = ApiClient(
  secureStorage,
  onUnauthorized: () {
    // Navigate to login screen
    context.go('/login');
  },
);
```

### 2. Make API Requests

#### 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@example.com',
    'password': 'password123',
  },
);
```

#### PUT Request

```dart
final response = await apiClient.put(
  '/products/123',
  data: {'name': 'Updated Name'},
);
```

#### DELETE Request

```dart
final response = await apiClient.delete('/products/123');
```

## API Response Format

All API responses follow this standard format from the backend:

```dart
{
  "Value": {...},           // The actual data
  "IsSuccess": true,        // Success flag
  "IsFailure": false,       // Failure flag
  "Errors": [],            // List of error messages
  "ErrorCodes": []         // List of error codes
}
```

Use the `ApiResponse` class to parse responses:

```dart
final apiResponse = ApiResponse.fromJson(
  response.data,
  (json) => User.fromJson(json), // Parse the Value field
);

if (apiResponse.isSuccess && apiResponse.value != null) {
  final user = apiResponse.value;
  print('Success: ${user.username}');
} else {
  print('Error: ${apiResponse.getErrorMessage()}');
}
```

## Authentication Flow

### Login

```dart
// 1. Login via API
final response = await apiClient.post('/auth/login', data: credentials);

// 2. Parse response
final apiResponse = ApiResponse.fromJson(response.data, (json) => User.fromJson(json));

// 3. Save tokens (done by LoginUseCase)
if (apiResponse.isSuccess) {
  final user = apiResponse.value!;
  await secureStorage.saveAccessToken(user.accessToken);
  await secureStorage.saveRefreshToken(user.refreshToken);
}

// 4. Subsequent requests automatically include Bearer token
```

### Automatic Token Injection

The API client automatically adds the Bearer token to all requests:

```dart
// You just make the request
final response = await apiClient.get('/warehouses');

// The interceptor automatically adds:
// Authorization: Bearer <token>
```

### 401 Error Handling

When a 401 Unauthorized error occurs:

1. Error is logged
2. All tokens are cleared from secure storage
3. `onUnauthorized` callback is triggered
4. App can navigate to login screen

```dart
// This is handled automatically - no manual intervention needed
// Just provide the callback when creating the client:
final apiClient = ApiClient(
  secureStorage,
  onUnauthorized: () {
    // This will be called on 401 errors
    context.go('/login');
  },
);
```

## Error Handling

The API client transforms Dio exceptions into custom app exceptions:

```dart
try {
  final response = await apiClient.get('/products');
} on NetworkException catch (e) {
  // Handle network errors (timeout, no internet, etc.)
  print('Network error: ${e.message}');
} on ServerException catch (e) {
  // Handle server errors (4xx, 5xx)
  print('Server error: ${e.message}');
  if (e.code == '401') {
    // Unauthorized - already handled by interceptor
  }
} catch (e) {
  // Handle unknown errors
  print('Unknown error: $e');
}
```

### Error Types

- `NetworkException`: Connection timeouts, no internet, certificate errors
- `ServerException`: HTTP errors (400-599) with specific error codes
  - 401: Unauthorized (automatically handled)
  - 403: Forbidden
  - 404: Not Found
  - 422: Validation Error
  - 429: Rate Limited
  - 500+: Server Errors

## Logging

The API client provides comprehensive logging for debugging:

### Request Logging
```
REQUEST[GET] => https://api.example.com/warehouses
Headers: {Authorization: ***REDACTED***, Content-Type: application/json}
Query Params: {limit: 10}
Body: {...}
```

### Response Logging
```
RESPONSE[200] => https://api.example.com/warehouses
Data: {...}
```

### Error Logging
```
ERROR[401] => https://api.example.com/warehouses
Error Data: {Errors: [Unauthorized access], ErrorCodes: [AUTH_001]}
```

### Security

All sensitive headers (Authorization, api-key, token) are automatically redacted in logs:

```dart
// Logged as:
Headers: {Authorization: ***REDACTED***, Content-Type: application/json}
```

## Configuration

### Timeout Settings

Configure timeouts in `lib/core/constants/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

Configure base URL in `lib/core/constants/app_constants.dart`:

```dart
static const String apiBaseUrl = 'https://api.example.com';
```

Or update dynamically:

```dart
// For different environments
apiClient.updateBaseUrl('https://dev-api.example.com');  // Development
apiClient.updateBaseUrl('https://staging-api.example.com'); // Staging
apiClient.updateBaseUrl('https://api.example.com'); // Production
```

## API Endpoints

Define endpoints in `lib/core/constants/api_endpoints.dart`:

```dart
class ApiEndpoints {
  static const String login = '/auth/login';
  static const String warehouses = '/warehouses';
  static const String products = '/products';

  // Dynamic endpoints
  static String productById(int id) => '/products/$id';

  // Query parameters helper
  static Map<String, dynamic> productQueryParams({
    required int warehouseId,
    required String type,
  }) {
    return {
      'warehouseId': warehouseId,
      'type': type,
    };
  }
}
```

## Utility Methods

### Test Connection

```dart
final isConnected = await apiClient.testConnection();
if (!isConnected) {
  print('Cannot connect to API');
}
```

### Check Authentication

```dart
final isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
  // Navigate to login
}
```

### Get Current Token

```dart
final token = await apiClient.getAccessToken();
if (token != null) {
  print('Token exists');
}
```

### Clear Authentication

```dart
// Logout - clears all tokens
await apiClient.clearAuth();
```

## Integration with Repository Pattern

The API client is designed to work with the repository pattern:

```dart
// Remote Data Source
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());
    }
  }
}
```

## Dependency Injection

Register the API client with GetIt:

```dart
final getIt = GetIt.instance;

// Register SecureStorage
getIt.registerLazySingleton<SecureStorage>(() => SecureStorage());

// Register ApiClient
getIt.registerLazySingleton<ApiClient>(
  () => ApiClient(
    getIt<SecureStorage>(),
    onUnauthorized: () {
      // Handle unauthorized access
    },
  ),
);
```

## Best Practices

1. **Always use ApiResponse**: Parse all responses using the `ApiResponse` wrapper
2. **Handle errors gracefully**: Catch specific exception types for better error handling
3. **Use endpoints constants**: Define all endpoints in `api_endpoints.dart`
4. **Don't expose Dio**: Use the provided methods (get, post, put, delete) instead of accessing `dio` directly
5. **Test connection**: Use `testConnection()` before critical operations
6. **Log appropriately**: The client logs automatically, but you can add app-level logs too

## Testing

Mock the API client in tests:

```dart
class MockApiClient extends Mock implements ApiClient {}

void main() {
  late MockApiClient mockApiClient;

  setUp(() {
    mockApiClient = MockApiClient();
  });

  test('should get warehouses', () async {
    // Arrange
    when(mockApiClient.get(any))
      .thenAnswer((_) async => Response(
        data: {'Value': [], 'IsSuccess': true},
        statusCode: 200,
        requestOptions: RequestOptions(path: '/warehouses'),
      ));

    // Act & Assert
    final response = await mockApiClient.get('/warehouses');
    expect(response.statusCode, 200);
  });
}
```

## Troubleshooting

### Token not being added to requests
- Ensure token is saved in secure storage
- Check if token is expired
- Verify `getAccessToken()` returns a value

### 401 errors not triggering logout
- Verify `onUnauthorized` callback is set
- Check error interceptor logs
- Ensure secure storage is properly initialized

### Connection timeouts
- Check network connectivity
- Verify base URL is correct
- Increase timeout values if needed

### Logging not appearing
- Use Flutter DevTools or console
- Check log level settings
- Ensure developer.log is not filtered

## Examples

See `api_client_example.dart` for comprehensive usage examples including:
- Login flow
- GET/POST/PUT/DELETE requests
- Error handling
- Custom options
- Request cancellation
- Environment switching

## Support

For issues or questions:
1. Check this documentation
2. Review `api_client_example.dart`
3. Check Flutter DevTools logs
4. Review backend API documentation
