Files
be_happy_public/lib/data/network/api_service.dart
2026-05-29 11:40:55 +03:00

1154 lines
33 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:be_happy/data/exceptions/auth_block_exception.dart';
import 'package:be_happy/data/exceptions/auth_exception.dart';
import 'package:be_happy/data/exceptions/route_history_not_found_exception.dart';
import 'package:be_happy/data/exceptions/scooter_not_found_exception.dart';
import 'package:be_happy/data/exceptions/unauthorized_exception.dart';
import 'package:be_happy/data/exceptions/wrong_zone_exception.dart';
import 'package:be_happy/domain/entities/active_scooter_order.dart';
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:be_happy/domain/service/security_service.dart';
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
import 'package:flutter_client_sse/flutter_client_sse.dart';
import '../../domain/entities/client_subscription.dart';
import '../../domain/entities/point.dart';
import '../../domain/entities/user_profile.dart';
import '../../domain/entities/payment_card.dart';
import '../../domain/entities/scooter_order.dart';
import '../../domain/entities/subscription.dart';
import '../../domain/entities/user_check_flags.dart';
import '../../domain/entities/certificate.dart';
import '../models/scooters_response.dart';
import '../models/tariffs_response.dart';
import '../models/zones_response.dart';
import '../models/subscriptions_response.dart';
import '../models/user_check_response_dto.dart';
class ApiService {
static const String baseUrl = "https://sharing-api.sparkit.by/api/v1";
static const String fileBaseUrl = "https://sharing-api.sparkit.by";
final SecurityService _securityService;
final Dio _dio;
ApiService(this._securityService, this._dio);
Future<Options> _getAuthOptions() async {
final accessToken = await _securityService.getAccessToken();
return Options(headers: {"Authorization": "Bearer $accessToken"});
}
Future<String?> sendPhone(String phone, String model, String systemId) async {
try {
final response = await _dio.post(
"$baseUrl/auth/client/login",
data: {"phone": phone, "model": model, "systemId": systemId},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data["token"];
}
return null;
} catch (e) {
return null;
}
}
Future<Map<String, String>?> verifyCode(String code, String token) async {
try {
final response = await _dio.post(
"$baseUrl/auth/client/verify",
data: {"code": code, "token": token},
);
if (response.statusCode == 200 || response.statusCode == 201) {
return {
"accessToken": response.data["accessToken"],
"refreshToken": response.data["refreshToken"],
};
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final attempts =
int.tryParse(e.response?.data["message"]?.toString() ?? "0") ?? 0;
if (attempts == 0) throw AuthBlockException();
throw AuthException("Ошибка. Неверный код.", attempts);
} else if (e.response?.statusCode == 403) {
throw AuthBlockException();
}
rethrow;
}
}
Future<Map<String, String>?> refresh() async {
final refreshToken = await _securityService.getRefreshToken();
try {
final response = await _dio.get(
"$baseUrl/auth/client/refresh",
options: Options(headers: {"Authorization": "Bearer $refreshToken"}),
);
if (response.statusCode == 200) {
return {
"accessToken": response.data["accessToken"],
"refreshToken": response.data["refreshToken"],
};
}
return null;
} catch (e) {
return null;
}
}
Future<UserProfile?> getProfile() async {
try {
final response = await _dio.get(
"$baseUrl/client/me",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
final profileData = data["profile"];
if (profileData == null) return null;
String? parsedDate;
if (profileData["dob"] != null) {
try {
parsedDate = DateFormat(
'dd.MM.yyyy',
).format(DateTime.parse(profileData["dob"]));
} catch (_) {}
}
final int? avatarId = profileData["avatarId"];
String? avatarUrl;
if (avatarId != null && profileData["avatar"] != null) {
final String? avatarPath = profileData["avatar"]["path"];
if (avatarPath != null && avatarPath.isNotEmpty) {
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString();
}
}
dynamic balanceRaw = profileData["balance"];
int? balance;
if (balanceRaw is int) {
balance = balanceRaw;
} else if (balanceRaw is String) {
balance = int.tryParse(balanceRaw);
}
return UserProfile(
name: profileData["name"] ?? "",
birthDate: parsedDate ?? "",
phone: data["phone"] ?? "",
balance: balance,
email: profileData["email"] ?? "",
avatarId: avatarId,
avatarUrl: avatarUrl,
);
}
return null;
} catch (e) {
return null;
}
}
Future<List<PaymentCard>> getPaymentCards() async {
final response = await _dio.get(
"$baseUrl/client/me",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final List<dynamic> cardsJson = response.data["clientCards"] ?? [];
return Future.wait(
cardsJson.map((cardJson) async {
return PaymentCard(
id: cardJson['id'],
clientId: cardJson['clientId'],
expirationMonth: cardJson['expirationMonth'],
expirationYear: cardJson['expirationYear'],
cardHolder: cardJson['cardHolder'],
cardLastNumber: cardJson['cardLastNumber'],
isMain: cardJson['isMain'],
type: cardJson['cardType'],
);
}),
);
}
return [];
}
Future<UserProfile?> updateProfile(UserProfile profile) async {
const url = "$baseUrl/client/me";
final accessToken = await _securityService.getAccessToken();
final Map<String, dynamic> body = {
if (profile.email != null && profile.email!.isNotEmpty)
"email": profile.email,
if (profile.name != null && profile.name!.isNotEmpty)
"name": profile.name,
if (profile.birthDate.isNotEmpty) "dob": profile.birthDate,
if (profile.avatarId != null) "avatarId": profile.avatarId,
};
if (body.isEmpty) return null;
try {
final response = await _dio.patch(
url,
data: body,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200) {
final data = response.data;
final profileData = data["profile"];
if (profileData == null) return null;
String? parsedDate;
if (profileData["dob"] != null) {
try {
parsedDate = DateFormat(
'dd.MM.yyyy',
).format(DateTime.parse(profileData["dob"]));
} catch (_) {}
}
final int? avatarId = profileData["avatarId"];
String? avatarUrl;
if (avatarId != null && profileData["avatar"] != null) {
final String? avatarPath = profileData["avatar"]["path"];
if (avatarPath != null && avatarPath.isNotEmpty) {
avatarUrl = "$fileBaseUrl/${avatarPath.replaceFirst('/', '')}";
}
}
dynamic balanceRaw = data["balance"];
int? balance;
if (balanceRaw is int) {
balance = balanceRaw;
} else if (balanceRaw is String) {
balance = int.tryParse(balanceRaw);
}
return UserProfile(
name: profileData["name"] ?? "",
birthDate: parsedDate ?? "",
phone: data["phone"] ?? "",
balance: balance,
email: profileData["email"] ?? "",
avatarId: avatarId,
avatarUrl: avatarUrl,
);
}
return null;
} on DioException catch (e) {
print(
"ERROR: updateProfile failed: ${e.response?.statusCode} - ${e.message}",
);
rethrow;
}
}
Future<UserCheckFlags?> checkUser() async {
const url = "$baseUrl/scooterorder/check";
final accessToken = await _securityService.getAccessToken();
try {
final response = await _dio.post(
url,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final dto = UserCheckResponseDto.fromJson(response.data);
return UserCheckFlags(
hasFine: dto.hasFine,
hasUnpaidOrder: dto.hasUnpaidOrder,
hasCard: dto.hasCard,
);
}
return null;
} catch (e) {
print("ERROR: checkUser failed: $e");
return null;
}
}
Future<int?> uploadPhoto(File imageFile) async {
try {
final mimeType = lookupMimeType(imageFile.path) ?? 'image/jpeg';
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
imageFile.path,
filename: basename(imageFile.path),
contentType: MediaType.parse(mimeType),
),
});
final response = await _dio.post(
"$baseUrl/client/upload",
data: formData,
options: await _getAuthOptions(),
);
return response.data['id'];
} catch (e) {
rethrow;
}
}
Future<List<int>> uploadScooterPhotos(List<File> images) async {
try {
final formData = FormData();
for (var file in images) {
final mimeType = lookupMimeType(file.path) ?? 'image/jpeg';
formData.files.add(
MapEntry(
'files',
await MultipartFile.fromFile(
file.path,
filename: basename(file.path),
contentType: MediaType.parse(mimeType),
),
),
);
}
final response = await _dio.post(
"$baseUrl/scooterorder/upload",
data: formData,
options: await _getAuthOptions(),
);
final List<dynamic> list = response.data['data'] ?? [];
return list.map((item) => item['id'] as int).toList();
} catch (e) {
rethrow;
}
}
Future<ScootersResponse?> getScooters({
required List<double> area,
int page = 1,
int pageSize = 500,
}) async {
const path = "$baseUrl/scooter/available";
final queryParams = {
'readAll': 'true',
'area': area.map((coord) => coord.toString()).toList(),
'page': page,
'pageSize': pageSize,
};
final accessToken = await _securityService.getAccessToken();
if (accessToken == null) return null;
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: Options(headers: {"Authorization": "Bearer $accessToken"}),
);
if (response.statusCode == 200) {
return ScootersResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getScooters failed: $e");
return null;
}
}
Future<Scooter?> getScooterById({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooter/$id/client",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Scooter.fromJson(response.data);
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
}
return null;
}
Future<Scooter?> getScooterByTitle({required String title}) async {
final url = "$baseUrl/scooter/$title/code";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200 || response.statusCode == 201) {
return Scooter.fromJson(response.data);
}
if (response.statusCode == 404) {
throw ScooterNotFoundException(message: "Самокат не найден");
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
if (e.response?.statusCode == 404) throw ScooterNotFoundException();
return null;
}
}
Future<ZonesResponse?> getZones({
required List<double> area,
int page = 1,
int pageSize = 500,
}) async {
const path = "$baseUrl/zone/available";
final queryParams = {
'readAll': 'true',
'area': area.map((coord) => coord.toString()).toList(),
'page': page,
'pageSize': pageSize,
};
try {
final response = await _dio.get(
path,
queryParameters: queryParams,
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
return ZonesResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getZones failed: $e");
return null;
}
}
Future<TariffsResponse?> getAvailableTariffs({required int scooterId}) async {
final url = "$baseUrl/scooterplan/$scooterId/available";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
return TariffsResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("ERROR: getAvailableTariffs failed: $e");
return null;
}
}
Future<List<ClientSubscription>> getClientSubscriptions() async {
const url = "$baseUrl/scootersubscription/client";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = response.data;
final List<dynamic> items = responseData['data'] ?? [];
return items.map((item) {
return ClientSubscription.fromJson(item);
}).toList();
}
return [];
} catch (e) {
print("APISERVICE (getClientSubscriptions) Error: $e");
return [];
}
}
Future<SubscriptionsResponse?> getAvailableSubscriptions() async {
const url = "$baseUrl/scootersubscription/available";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200) {
return SubscriptionsResponse.fromJson(response.data);
}
return null;
} catch (e) {
print("APISERVICE (getAvailableSubscriptions) Error: $e");
return null;
}
}
Future<Subscription?> getSubscriptionById({required int id}) async {
final url = "$baseUrl/scootersubscription/$id/client";
try {
final response = await _dio.get(url, options: await _getAuthOptions());
if (response.statusCode == 200 || response.statusCode == 201) {
return Subscription.fromJson(response.data);
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
print("APISERVICE (getSubscriptionById) Error: $e");
return null;
}
}
Future<bool> activateSubscription({required int optionId}) async {
const url = "$baseUrl/scootersubscription/client";
try {
final response = await _dio.post(
url,
data: {"optionId": optionId},
options: await _getAuthOptions(),
);
if (response.statusCode == 201) {
return response.data["success"] == true;
}
return false;
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
print("APISERVICE (activateSubscription) Error: $e");
return false;
}
}
Future<void> addPaymentCard({
required String cardNumber,
required String cardHolder,
required int expirationMonth,
required int expirationYear,
required String cvv,
}) async {
const url = "$baseUrl/client/card";
final cleanCardNumber = cardNumber.replaceAll(' ', '');
try {
final response = await _dio.post(
url,
data: {
"cardNumber": cleanCardNumber,
"cardHolder": cardHolder,
"expirationMonth": expirationMonth,
"expirationYear": expirationYear,
"cvv": cvv,
},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return;
}
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
} on DioException catch (e) {
final data = e.response?.data;
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(data);
throw AuthException(message ?? 'Неверные данные карты', 0);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Пользователь не найден', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<void> setMainPaymentCard(int cardId) async {
final url = "$baseUrl/client/card/$cardId";
try {
await _dio.put(
url,
data: {"isMain": true},
options: await _getAuthOptions(),
);
} on DioException catch (e) {
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(e.response?.data);
throw AuthException(
message ?? 'Ошибка при установке основной карты',
0,
);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Карта не найдена', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<void> removePaymentCard(int cardId) async {
final url = "$baseUrl/client/card/$cardId";
try {
await _dio.delete(url, options: await _getAuthOptions());
} on DioException catch (e) {
final statusCode = e.response?.statusCode;
if (statusCode == 400) {
final message = _parseErrorMessage(e.response?.data);
throw AuthException(message ?? 'Ошибка при удалении карты', 0);
} else if (statusCode == 401) {
throw UnauthorizedException();
} else if (statusCode == 403) {
throw AuthBlockException();
} else if (statusCode == 404) {
throw AuthException('Карта не найдена', 0);
}
throw AuthException('Ошибка сервера: $statusCode', 0);
}
}
Future<ScooterOrder?> bookScooter({
required int scooterId,
required int planId,
int? subscriptionId,
int? cardId,
required bool isBalance,
required bool isInsurance,
}) async {
try {
final response = await _dio.post(
"$baseUrl/scooterorder/booking",
data: {
"scooterId": scooterId,
"planId": planId,
"subscriptionId": subscriptionId,
"cardId": cardId,
"isBalance": isBalance,
"isInsurance": isInsurance,
},
options: await _getAuthOptions(),
);
return ScooterOrder.fromJson(response.data);
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final data = e.response?.data;
if (data is Map && data['message'] is List) {
final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) {
throw WrongZoneException(
message: "Некорректная зона для начала поездки.",
);
}
}
if (data is Map && data['message'] is String) {}
} else {
_handleDioError(e);
}
rethrow;
}
}
Future<ScooterOrder?> startRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/start",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final data = e.response?.data;
if (data is Map && data['message'] is List) {
final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) {
throw WrongZoneException(
message: "Некорректная зона для начала поездки.",
);
}
}
if (data is Map && data['message'] is String) {}
} else {
_handleDioError(e);
}
rethrow;
}
}
Future<ScooterOrder?> cancelRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/cancel",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> pauseRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pause",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> resumeRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/resume",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> finishRide({
required int orderId,
required List<int> filesId,
}) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/finish",
data: {"files": filesId},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final data = e.response?.data;
if (data is Map && data['message'] is List) {
final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) {
throw WrongZoneException(
message: "Некорректная зона для завершения поездки.",
);
}
}
if (data is Map && data['message'] is String) {}
} else {
_handleDioError(e);
}
rethrow;
}
}
Future<void> payRide(int orderId) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pay",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
// return ScooterOrder.fromJson(response.data);
return;
}
} on DioException catch (e) {
_handleDioError(e);
return;
}
}
Future<List<ScooterOrder>> getClientOrders() async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/active",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
if (data is Map<String, dynamic> && data['data'] is List) {
final List ordersList = data['data'];
return ordersList.map((json) => ScooterOrder.fromJson(json)).toList();
}
}
return [];
} on DioException catch (e) {
_handleDioError(e);
return [];
}
}
Future<ActiveScooterOrder?> updateScooterOrderData({
required int orderId,
Map<String, dynamic>? data,
}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$orderId/data",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ActiveScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<ScooterOrder?> getScooterOrderById({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$id/client",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return ScooterOrder.fromJson(response.data);
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<void> payScooterOrderWithPhotos({
required int orderId,
required int? cardId,
required bool isBalance,
}) async {
try {
final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pay",
data: {"cardId": cardId, "isBalance": isBalance},
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return;
}
} on DioException catch (e) {
_handleDioError(e);
}
}
Future<Map<String, dynamic>> getScooterOrderHistory({
int page = 1,
int pageSize = 20,
}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/history",
queryParameters: {"page": page, "pageSize": pageSize},
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return {};
}
}
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
try {
final response = await _dio.get(
"$baseUrl/scooterorder/$id/routehistory",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final String routeString = response.data['route'] ?? '[]';
final List<dynamic> routeList = json.decode(routeString);
return routeList
.map(
(item) => Point(
(item[1] as num).toDouble(),
(item[0] as num).toDouble(),
),
)
.toList();
}
throw RouteHistoryNotFoundException(
message: "История маршрута не найдена",
);
} on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
if (e.response?.statusCode == 404) throw RouteHistoryNotFoundException();
rethrow;
}
}
Future<Map<String, dynamic>> getNews() async {
try {
final response = await _dio.get(
"$baseUrl/news",
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return {};
}
}
Future<Map<String, dynamic>?> getNewsById(int newsId) async {
try {
final response = await _dio.get(
'$baseUrl/news/$newsId',
options: await _getAuthOptions(),
);
return response.data;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Future<Map<String, dynamic>?> cancelNotification(int id) async {
try {
final response = await _dio.put(
"$baseUrl/notification/client/$id/cancel",
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data;
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
Stream<Map<String, dynamic>> getNotificationsStream() {
final url = "$baseUrl/notification/client/stream";
final controller = StreamController<Map<String, dynamic>>();
_securityService.getAccessToken().then((accessToken) {
if (accessToken == null) {
controller.addError(UnauthorizedException());
return;
}
print("[SSE] Subscribing via flutter_client_sse...");
SSEClient.subscribeToSSE(
method: SSERequestType.GET,
url: url,
header: {
"Authorization": "Bearer $accessToken",
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
},
).listen(
(event) {
print("Received line [SSE]: id=${event.id}, event=${event.event}");
print("Data [SSE]: ${event.data}");
if (event.data != null && event.data!.isNotEmpty) {
try {
final Map<String, dynamic> jsonData = jsonDecode(event.data!);
print("✅ [SSE] Parsed JSON: $jsonData");
controller.add(jsonData);
} catch (e) {
print("❌ [SSE] JSON Parse Error: $e | Raw: ${event.data}");
}
}
},
onError: (error) {
print(" [SSE] Library Error: $error");
controller.addError(error);
},
onDone: () {
print(" [SSE] Library Stream Closed");
controller.close();
},
);
});
return controller.stream;
}
Future<List<Map<String, dynamic>>> getNotifications() async {
final url = Uri.parse('$baseUrl/notification/client');
final accessToken = await _securityService.getAccessToken();
if (accessToken == null) {
print("APISERVICE Error: Access token is null.");
throw UnauthorizedException();
}
print("GET NOTIFICATIONS REQUEST:");
print("URL: $url");
final response = await http.get(
url,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer $accessToken",
},
);
print("GET NOTIFICATIONS RESPONSE:");
print("STATUS: ${response.statusCode}");
print("BODY: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
// ✅ Проверяем, является ли ответ массивом или объектом с data[]
if (data is List) {
return data.cast<Map<String, dynamic>>();
} else if (data is Map<String, dynamic>) {
final list = data['data'];
if (list is List) {
return list.cast<Map<String, dynamic>>();
} else {
throw Exception(
'Expected a List under "data" but got ${list.runtimeType}',
);
}
} else {
throw Exception('Expected a List or Map but got ${data.runtimeType}');
}
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 403) {
throw AuthBlockException();
}
throw Exception('Ошибка сервера: ${response.statusCode}');
}
Future<List<Certificate>> getCertificates() async {
try {
final response = await _dio.get(
"$baseUrl/certificate",
options: await _getAuthOptions(),
);
if (response.statusCode == 200) {
final data = response.data;
final dataList = data['data'] as List<dynamic>;
return dataList.map((json) => Certificate.fromJson(json)).toList();
}
return [];
} on DioException catch (e) {
_handleDioError(e);
return [];
}
}
Future<Map<String, dynamic>?> purchaseCertificate({
required int certificateId,
required int cardId,
}) async {
try {
final response = await _dio.post(
"$baseUrl/certificate/$certificateId",
data: {"cardId": cardId},
options: await _getAuthOptions(),
);
if (response.statusCode == 201) {
return response.data;
}
return null;
} on DioException catch (e) {
_handleDioError(e);
return null;
}
}
void _handleDioError(DioException e) {
if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException();
final message = _parseErrorMessage(e.response?.data);
throw AuthException(message ?? 'Ошибка сервера', 0);
}
String? _parseErrorMessage(dynamic data) {
if (data is Map<String, dynamic>) {
final messages = data['message'] as List?;
if (messages != null && messages.isNotEmpty) {
final firstError = messages.first as Map<String, dynamic>?;
return firstError?['message'] as String?;
}
return data['message'] as String?;
}
return null;
}
}