426 lines
12 KiB
Dart
426 lines
12 KiB
Dart
// 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<String, dynamic> get result => _body?['result'] ?? const {};
|
|
|
|
Map<String, dynamic> get errors => _body?['errors'] ?? const {};
|
|
|
|
String errorMessage =
|
|
'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.';
|
|
|
|
Map? _body;
|
|
|
|
final Map<String, String> _headers = {
|
|
"accept": "*/*",
|
|
"Content-Type": "application/json; charset=UTF-8",
|
|
};
|
|
final String url;
|
|
Map? _requestBody;
|
|
|
|
bool isSuccess = false;
|
|
|
|
RequestService(
|
|
this.url, {
|
|
Map<String, String>? 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<void> _prepare() async {
|
|
if (!_useAuthorization) return;
|
|
await AuthInterceptor.ensureFreshToken();
|
|
_applyAuthHeader();
|
|
}
|
|
|
|
/// در صورت دریافت 401 یکبار refresh کرده و درخواست را retry میکند.
|
|
Future<bool> _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<void> 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<void> 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<void> 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<void> multipartFilesCreateAssismants({
|
|
required List<XFile>? 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<void> 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<void> 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<void> 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<void> 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('<!DOCTYPE html>')) {
|
|
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 ?? 'خطا! لطفا اتصال اینترنت خود را بررسی و مجددا تلاش نمایید.';
|
|
}
|
|
}
|