diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index 1a60fb7..cb71ee3 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -1,131 +1,295 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../config/app_config.dart'; +import '../utils/logger.dart'; +import '../utils/result.dart'; +import 'app_exceptions.dart'; +import 'network_connectivity.dart'; class NetworkManager { final Dio mainDio; + final NetworkConnectivity _connectivity = NetworkConnectivity(); NetworkManager() : mainDio = Dio( BaseOptions( - // baseUrl: AppConfig.apiBaseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 10), - contentType: 'application/json', - headers: {'Accept': 'application/json'}, + baseUrl: 'http://10.0.2.2:8081', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, ), ); - Future init() async { - await addInterceptors(); - } - Future addInterceptors() async { final requestInterceptor = InterceptorsWrapper( onRequest: (options, handler) { - // TODO: add more + appLogger.info('🌐 Request: ${options.method} ${options.path}'); + appLogger.info('🌐 Headers: ${options.headers}'); return handler.next(options); }, + onResponse: (response, handler) { + appLogger.info( + '✅ Response: ${response.statusCode} ${response.requestOptions.path}', + ); + + return handler.next(response); + }, + onError: (error, handler) { + appLogger.error( + '❌ Error: ${error.message} - ${error.requestOptions.path}', + ); + appLogger.error('❌ Error type: ${error.type}'); + return handler.next(error); + }, ); mainDio.interceptors.add(requestInterceptor); } - Future makeRequest( + /// Enhanced makeRequest method with comprehensive error handling + Future> makeRequest( String endpoint, T Function(Map) fromJson, { String method = 'GET', - FormData? data, + Object? data, + Map? queryParameters, }) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - - Dio dioInstance = mainDio; - - await addInterceptors(); - - Future makeApiCall(String token, String url) async { - final headers = - token.isEmpty - ? {} - : {'Authorization': 'Bearer $token'}; - switch (method.toUpperCase()) { - case 'POST': - return await dioInstance.post( - url + endpoint, - data: data, - options: Options(headers: headers), - ); - case 'PUT': - return await dioInstance.put( - url + endpoint, - data: data, - options: Options(headers: headers), - ); - case 'PATCH': - return await dioInstance.patch( - url + endpoint, - data: data, - options: Options(headers: headers), - ); - case 'DELETE': - return await dioInstance.delete( - url + endpoint, - data: data, - options: Options(headers: headers), - ); - case 'GET': - return await dioInstance.get( - url + endpoint, - data: data, - options: Options(headers: headers), - ); - default: - return await dioInstance.get( - url + endpoint, - options: Options(headers: headers), - ); - } - } + appLogger.info('🚀 network request: $method $endpoint'); try { + // Check network connectivity first + if (!await _connectivity.hasInternetConnection()) { + appLogger.error('❌ No internet connection available'); + return Result.error( + NetworkConnectionException('No internet connection available'), + ); + } + + SharedPreferences prefs = await SharedPreferences.getInstance(); + Dio dioInstance = mainDio; + + await addInterceptors(); + + Future makeApiCall(String token) async { + final headers = + token.isEmpty + ? {} + : {'Authorization': 'Bearer $token'}; + + final options = Options( + headers: headers, + validateStatus: (status) => status != null && status < 500, + ); + + switch (method.toUpperCase()) { + case 'POST': + return await dioInstance.post( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + ); + case 'PUT': + return await dioInstance.put( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + ); + case 'PATCH': + return await dioInstance.patch( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + ); + case 'DELETE': + return await dioInstance.delete( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + ); + case 'GET': + return await dioInstance.get( + endpoint, + queryParameters: queryParameters, + options: options, + ); + default: + return await dioInstance.get( + endpoint, + queryParameters: queryParameters, + options: options, + ); + } + } + String? bearerToken = prefs.getString('bearer'); // String? url = prefs.getString('url'); - Response res = await makeApiCall(bearerToken ?? '', AppConfig.apiBaseUrl); - switch (res.statusCode) { - case 200: - case 201: - return fromJson(res.data); - case 401: - break; - case 403: - throw Exception('Forbidden: ${res.data['message']}'); - case 404: - throw Exception('Not found: ${res.data['message']}'); - case 423: - throw Exception('Locked: ${res.data['message']}'); - case 500: - throw Exception('Internal server error: ${res.data['message']}'); - default: - throw Exception('Error: ${res.data['message']}'); - } - } on DioException catch (e) { - if (e.response?.statusCode == 401) { - // await api.refreshToken(); - String? bearerToken = prefs.getString('bearer'); - String? url = prefs.getString('url'); - Response res = await makeApiCall(bearerToken ?? '', url ?? ''); - if (res.statusCode == 200 || res.statusCode == 201) { - return fromJson(res.data); + // appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}'); + appLogger.info( + '🔑 Bearer token: ${bearerToken != null ? 'PRESENT' : 'NOT SET'}', + ); + + // if (url == null || url.isEmpty) { + // appLogger.error('❌ Base URL not configured'); + // return Result.error(BadRequestException('Base URL not configured')); + // } + + Response res = await makeApiCall(bearerToken ?? ''); + + // Handle successful responses + if (res.statusCode == 200 || res.statusCode == 201) { + appLogger.info('✅ Request successful: ${res.statusCode}'); + try { + final result = fromJson(res.data); + appLogger.info('✅ Response parsed successfully'); + return Result.ok(result); + } on Exception catch (e) { + appLogger.error('❌ Failed to parse response: $e'); + return Result.error( + FetchDataException( + 'Failed to parse response data', + res.statusCode, + e, + ), + ); } - - throw Exception(e.message ?? 'Error occurred'); } - } catch (e) { - throw Exception(e.toString()); + + // Handle specific HTTP error codes + return _handleHttpError(res); + } on DioException catch (e) { + return _handleDioException(e); + } on Exception catch (e) { + appLogger.error('Unexpected error in makeRequest: $e'); + return Result.error( + FetchDataException('Unexpected error occurred', null, e), + ); + } + } + + /// Handle HTTP error responses + Result _handleHttpError(Response response) { + final statusCode = response.statusCode; + final message = _extractErrorMessage(response.data); + + switch (statusCode) { + case 400: + return Result.error( + BadRequestException(message, statusCode, response.data), + ); + case 401: + return Result.error( + AuthenticationException(message, statusCode, response.data), + ); + case 403: + return Result.error( + ForbiddenException(message, statusCode, response.data), + ); + case 404: + return Result.error( + NotFoundException(message, statusCode, response.data), + ); + case 423: + return Result.error( + ForbiddenException( + 'Resource is locked: $message', + statusCode, + response.data, + ), + ); + case 429: + return Result.error( + RateLimitException(message, statusCode, response.data), + ); + case 500: + case 502: + case 503: + case 504: + return Result.error( + ServerException(message, statusCode, response.data), + ); + default: + return Result.error( + FetchDataException(message, statusCode, response.data), + ); + } + } + + /// Handle Dio exceptions + Result _handleDioException(DioException e) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return Result.error(TimeoutException('Request timed out', e)); + + case DioExceptionType.connectionError: + return Result.error( + NetworkConnectionException('Connection error occurred', e), + ); + + case DioExceptionType.badResponse: + if (e.response?.statusCode == 401) { + // Handle token refresh logic here if needed + // For now, return authentication error + return Result.error( + AuthenticationException( + 'Authentication failed', + e.response?.statusCode, + e, + ), + ); + } + return Result.error( + FetchDataException( + 'Bad response from server', + e.response?.statusCode, + e, + ), + ); + + case DioExceptionType.cancel: + return Result.error( + FetchDataException('Request was cancelled', null, e), + ); + + case DioExceptionType.unknown: + default: + if (e.error is SocketException) { + return Result.error( + NetworkConnectionException('No internet connection', e), + ); + } + return Result.error( + FetchDataException('Network error occurred', null, e), + ); + } + } + + /// Extract error message from response data + String _extractErrorMessage(data) { + if (data == null) { + return 'Unknown error'; } - throw Exception('Unhandled error occurred'); + if (data is Map) { + return data['message'] ?? + data['error'] ?? + data['detail'] ?? + 'Unknown error'; + } + + if (data is String) { + return data; + } + + return 'Unknown error'; } } diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 038d99d..2c008fc 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -2,7 +2,6 @@ import 'dart:convert'; -import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; @@ -19,31 +18,20 @@ class AuthService { required String username, required String password, }) async { - var headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json', - }; - var data = jsonEncode({"password": "Nima.1998", "username": "nakhostin"}); - var dio = Dio( - BaseOptions(baseUrl: 'http://10.0.2.2:8081', headers: headers), - ); - var response = await dio.request( + var data = jsonEncode({"password": password, "username": username}); + + final result = await _networkManager.makeRequest( '/admin/v1/profile/login', - options: Options(method: 'POST'), + method: 'POST', + (json) => LoginResponseModel.fromJson(json), data: data, ); - if (response.statusCode == 200) { + if (result is Ok) { final prefs = await SharedPreferences.getInstance(); - final token = response.data['data']?['access_token']; - prefs.setString('token', token); - if (token != null) { - await prefs.setString('bearer', token); - } - print(json.encode(response.data)); - } else { - print(response.statusMessage); + final token = result.value.data.accessToken; + await prefs.setString('bearer', token); } - return Result.ok(LoginResponseModel.fromJson(response.data)); + return result; } }