// lib/services/network/request.dart // ignore_for_file: empty_catches import 'dart:convert'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:didvan/services/auth/auth_interceptor.dart'; import 'package:didvan/services/auth/token_storage.dart'; import 'package:didvan/services/storage/storage.dart'; // ignore: depend_on_referenced_packages import 'package:http/http.dart' as http; // ignore: depend_on_referenced_packages import 'package:http_parser/http_parser.dart' as parser; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:permission_handler/permission_handler.dart'; class RequestService { /// Access token جاری. توسط [TokenStorage] sync می‌شود. /// برای حفظ سازگاری با کدهای قدیمی، setter آن `TokenStorage` را نیز /// نمی‌نویسد (آن کار توسط KeycloakAuthService انجام می‌شود). static String? token; int? statusCode; final bool _useAuthorization; bool _didRetry401 = false; dynamic data(String s) { return _body?[s] ?? const {}; } Map get result => _body?['result'] ?? const {}; Map get errors => _body?['errors'] ?? const {}; String errorMessage = 'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.'; Map? _body; final Map _headers = { "accept": "*/*", "Content-Type": "application/json; charset=UTF-8", }; final String url; Map? _requestBody; bool isSuccess = false; RequestService( this.url, { Map? requestHeaders, body, bool useAutherization = true, }) : _useAuthorization = useAutherization { if (body != null) _requestBody = body; if (requestHeaders != null) _headers.addAll(requestHeaders); _applyAuthHeader(); } /// Bearer access token جاری را روی هدرها اعمال می‌کند (در صورت نیاز). void _applyAuthHeader() { if (!_useAuthorization) { _headers.remove('Authorization'); return; } // اولویت با TokenStorage (به‌روزترین مقدار). در نبود آن از token استاتیک // قدیمی استفاده می‌کنیم تا سازگاری حفظ شود. final current = TokenStorage.accessToken ?? token; if (current != null && current.isNotEmpty) { _headers['Authorization'] = 'Bearer $current'; } else { _headers.remove('Authorization'); } } /// قبل از هر فراخوانی شبکه، تازه بودن توکن را تضمین می‌کند. Future _prepare() async { if (!_useAuthorization) return; await AuthInterceptor.ensureFreshToken(); _applyAuthHeader(); } /// در صورت دریافت 401 یک‌بار refresh کرده و درخواست را retry می‌کند. Future _maybeRetryOn401() async { if (!_useAuthorization) return false; if (_didRetry401) return false; if (statusCode != 401) return false; _didRetry401 = true; final ok = await AuthInterceptor.handleUnauthorized(); if (ok) { _applyAuthHeader(); } return ok; } String? get _generateBody => _requestBody == null ? null : jsonEncode(_requestBody); Future httpGet() async { log('req is: $url', name: 'RequestService'); await _prepare(); try { final response = await http .get( Uri.parse(url), headers: _headers, ) .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); _handleResponse(response); if (await _maybeRetryOn401()) { final retry = await http .get(Uri.parse(url), headers: _headers) .timeout(const Duration(seconds: 30)); _handleResponse(retry); } } catch (e) { _handleError(null); } } Future post() async { await _prepare(); try { final response = await http .post( Uri.parse(url), body: _generateBody, headers: _headers, ) .timeout( const Duration(seconds: 30), ) .catchError( (e) { throw e; }, ); _handleResponse(response); if (await _maybeRetryOn401()) { final retry = await http .post(Uri.parse(url), body: _generateBody, headers: _headers) .timeout(const Duration(seconds: 30)); _handleResponse(retry); } } catch (e) { _handleError(null); } } Future delete() async { await _prepare(); try { final response = await http .delete( Uri.parse(url), body: _generateBody, headers: _headers, ) .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); _handleResponse(response); if (await _maybeRetryOn401()) { final retry = await http .delete(Uri.parse(url), body: _generateBody, headers: _headers) .timeout(const Duration(seconds: 30)); _handleResponse(retry); } } catch (e) { _handleError(null); } } Future multipartFilesCreateAssismants({ required List? files, required XFile? image, required String method, }) async { try { final request = http.MultipartRequest(method, Uri.parse(url)); _headers.update('Content-Type', (_) => 'multipart/form-data'); request.headers.addAll(_headers); if (_requestBody != null) { _requestBody!.forEach((key, value) { request.fields.addAll({key.toString(): value.toString()}); }); } if (files != null) { for (var file in files) { final length = await file.length(); final mimeType = lookupMimeType(file.name) ?? 'application/octet-stream'; // Get content type request.files.add( http.MultipartFile( 'files', file.readAsBytes().asStream(), length, filename: file.name, contentType: parser.MediaType.parse(mimeType), // Use the content type ), ); } } if (image != null) { final length = await image.length(); final mimeType = lookupMimeType(image.name) ?? 'application/octet-stream'; // Get content type request.files.add( http.MultipartFile( 'image', image.readAsBytes().asStream(), length, filename: image.name, contentType: parser.MediaType.parse(mimeType), // Use the content type ), ); } final streamedResponse = await request .send() .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); final response = await http.Response.fromStream(streamedResponse); _handleResponse(response); } catch (e) { _handleError(null); } } Future multipart({ required dynamic file, required String method, required String fileName, required String fieldName, required String mediaFormat, required String mediaExtension, }) async { try { final request = http.MultipartRequest(method, Uri.parse(url)); _headers.update('Content-Type', (_) => 'multipart/form-data'); request.headers.addAll(_headers); final length = await file.length(); if (_requestBody != null) { _requestBody!.forEach((key, value) { request.fields.addAll({key.toString(): value.toString()}); }); } request.files.add( http.MultipartFile( fieldName, file.readAsBytes().asStream(), length, filename: '$fileName.$mediaExtension', contentType: parser.MediaType(mediaFormat, mediaExtension), ), ); final streamedResponse = await request .send() .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); final response = await http.Response.fromStream(streamedResponse); _handleResponse(response); } catch (e) { _handleError(null); } } Future multipartBytes({ required Uint8List file, required String method, required String fileName, required String fieldName, required String mediaFormat, required String mediaExtension, }) async { try { final request = http.MultipartRequest(method, Uri.parse(url)); _headers.update('Content-Type', (_) => 'multipart/form-data'); request.headers.addAll(_headers); if (_requestBody != null) { _requestBody!.forEach((key, value) { request.fields.addAll({key.toString(): value.toString()}); }); } request.files.add( http.MultipartFile.fromBytes( fieldName, file, filename: '$fileName.$mediaExtension', contentType: parser.MediaType(mediaFormat, mediaExtension), ), ); final streamedResponse = await request .send() .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); final response = await http.Response.fromStream(streamedResponse); _handleResponse(response); } catch (e) { _handleError(null); } } Future put() async { await _prepare(); try { final response = await http .put( Uri.parse(url), body: _generateBody, headers: _headers, ) .timeout( const Duration(seconds: 30), ) .catchError( (e) => throw e, ); _handleResponse(response); if (await _maybeRetryOn401()) { final retry = await http .put(Uri.parse(url), body: _generateBody, headers: _headers) .timeout(const Duration(seconds: 30)); _handleResponse(retry); } } catch (e) { _handleError(null); } } Future download(String fileName, String subDirectory) async { await Permission.storage.request(); final response = await http.get(Uri.parse(url)); StorageService.createFile( bytes: response.bodyBytes, subDirectory: subDirectory, name: fileName, ); } void _handleResponse(http.Response? response) { statusCode = response?.statusCode; if (response != null) { if (kDebugMode) { print('Response from [$url]: ${response.body}'); } } if (_handleError(response)) { if (response!.body.isNotEmpty) { _body = json.decode(response.body); } } errorMessage = _errorMessageGenerator(response); } bool _handleError(http.Response? response) { if (response == null) { log( 'Connecion failed or request timedout! ("$url")', name: 'Request fail error', ); isSuccess = false; return false; } if (response.statusCode != 200) { dynamic data; if (response.body.isEmpty) { data = 'No results!'; } else if (response.body.contains('')) { data = response.body; } else { data = jsonDecode(response.body); } log( 'Request to [$url] failed with status code ${response.statusCode} & result : \n $data', name: 'Request fail error', ); isSuccess = false; return false; } isSuccess = true; return true; } String _errorMessageGenerator(http.Response? response) { String? error; if (response != null) { if (_body != null) { if (result.isNotEmpty) { error = result['msg']; } } } return error ?? 'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.'; } }