import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tatlock_ui/core/auth/auth_provider.dart'; import 'package:tatlock_ui/core/config/app_config.dart'; import 'package:tatlock_ui/core/error/app_exception.dart'; /// Adds authentication token to requests. /// /// Skipped entirely when [AppConfig.requiresAuth] is false (LAN development). class AuthInterceptor extends Interceptor { AuthInterceptor(this._ref); final Ref _ref; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // Skip auth for LAN development if (!AppConfig.requiresAuth) { handler.next(options); return; } final authState = _ref.read(authProvider); authState.whenData((auth) { if (auth.isAuthenticated && auth.accessToken != null) { options.headers['Authorization'] = 'Bearer ${auth.accessToken}'; } }); handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) { // Skip auth error handling for LAN development if (!AppConfig.requiresAuth) { handler.next(err); return; } if (err.response?.statusCode == 401) { // Token expired - trigger re-authentication _ref.read(authProvider.notifier).signOut(); } handler.next(err); } } /// Logs API requests and responses to the console. /// /// All requests are logged with method, URL, query params, and body. /// Responses include status code. Errors include full details. class LoggingInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final buffer = StringBuffer() ..writeln('┌── API Request ──────────────────────────────────────') ..writeln('│ ${options.method} ${options.path}'); if (options.queryParameters.isNotEmpty) { buffer.writeln('│ Query: ${options.queryParameters}'); } if (options.data != null) { buffer.writeln('│ Body: ${options.data}'); } buffer.writeln('└─────────────────────────────────────────────────────'); final message = buffer.toString(); developer.log(message, name: 'API'); debugPrint(message); handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { final message = '✓ ${response.statusCode} ${response.requestOptions.method} ${response.requestOptions.path}'; developer.log(message, name: 'API'); debugPrint(message); handler.next(response); } @override void onError(DioException err, ErrorInterceptorHandler handler) { final buffer = StringBuffer() ..writeln('┌── API Error ────────────────────────────────────────') ..writeln('│ ${err.requestOptions.method} ${err.requestOptions.path}') ..writeln('│ Status: ${err.response?.statusCode ?? 'NETWORK ERROR'}') ..writeln('│ Message: ${err.message}'); if (err.response?.data != null) { buffer.writeln('│ Response: ${err.response?.data}'); } buffer.writeln('└─────────────────────────────────────────────────────'); final message = buffer.toString(); developer.log(message, name: 'API', error: err); debugPrint(message); handler.next(err); } } /// Converts Dio errors to AppException types. class ErrorInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { final exception = _mapToAppException(err); handler.reject( DioException( requestOptions: err.requestOptions, response: err.response, type: err.type, error: exception, ), ); } AppException _mapToAppException(DioException err) { switch (err.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return NetworkException( message: 'Connection timed out', cause: err, ); case DioExceptionType.connectionError: return NetworkException( message: 'Unable to connect to server', cause: err, ); case DioExceptionType.badResponse: final statusCode = err.response?.statusCode ?? 0; final data = err.response?.data; String message = 'Request failed'; String? code; if (data is Map) { final error = data['error']; if (error is Map) { message = error['message'] as String? ?? message; code = error['code'] as String?; } else if (error is String) { message = error; } } return ApiException( message: message, statusCode: statusCode, code: code, cause: err, ); case DioExceptionType.cancel: return const NetworkException(message: 'Request cancelled'); case DioExceptionType.badCertificate: return const NetworkException(message: 'Invalid SSL certificate'); case DioExceptionType.unknown: return NetworkException( message: err.message ?? 'Unknown error', cause: err, ); } } }