taxgilde/lib/controller/api_repository.dart
2026-04-11 10:21:31 +05:30

1144 lines
36 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/controller/api_consts.dart';
import 'package:taxglide/model/chat_model.dart';
import 'package:taxglide/model/chat_profile_model.dart';
import 'package:taxglide/model/city_model.dart';
import 'package:taxglide/model/count_model.dart';
import 'package:taxglide/model/country_model.dart';
import 'package:taxglide/model/dash_board_model.dart';
import 'package:taxglide/model/detail_model.dart';
import 'package:taxglide/model/employeeprofile_model.dart';
import 'package:taxglide/model/login_model.dart';
import 'package:taxglide/model/notification_model.dart';
import 'package:taxglide/model/notificationcount_model.dart';
import 'package:taxglide/model/profile_get_model.dart';
import 'package:taxglide/model/serivce_list_model.dart';
import 'package:taxglide/model/service_list_history_model.dart';
import 'package:taxglide/model/signup_model.dart';
import 'package:taxglide/model/staff_model.dart';
import 'package:taxglide/model/terms_model.dart';
import 'package:taxglide/router/consts_routers.dart';
class ApiRepository {
final LocalStore _localStore = LocalStore();
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
// 🔹 Common headers without token (for login/signup)
Map<String, String> get _baseHeaders => {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// 🔹 Authorized headers with token
Future<Map<String, String>> _authorizedHeaders() async {
final token = await _localStore.getToken();
return {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/// 🔑 Get fresh FCM token on-demand (not from storage)
Future<String?> _getFreshFcmToken() async {
try {
debugPrint('🔄 Generating fresh FCM token...');
final token = await _messaging.getToken();
debugPrint('✅ FCM Token generated: $token');
return token;
} catch (e) {
debugPrint('❌ Error getting FCM token: $e');
return null;
}
}
Future<Map<String, String?>> _regenerateFcmToken() async {
try {
// 📌 Print old token BEFORE deleting
final oldToken = await _messaging.getToken();
debugPrint('📋 OLD FCM Token (before delete): $oldToken');
debugPrint('🔄 Deleting old FCM token...');
// Delete the current token
await _messaging.deleteToken();
debugPrint('🗑️ Old FCM token deleted');
// Wait for Firebase to process the deletion
await Future.delayed(const Duration(milliseconds: 800));
// Request permission again (important for iOS)
await _messaging.requestPermission();
// Generate new tokens
debugPrint('🔄 Requesting new tokens...');
final newToken = await _messaging.getToken();
final apnsToken = await _messaging.getAPNSToken();
debugPrint('✅ NEW FCM Token: $newToken');
debugPrint('✅ NEW APNS Token: $apnsToken');
// Compare tokens
if (oldToken == newToken) {
debugPrint('⚠️ WARNING: Old and New tokens are SAME!');
} else {
debugPrint('✅ SUCCESS: New token is DIFFERENT from old token');
}
return {'fcm_token': newToken, 'apns_token': apnsToken};
} catch (e) {
debugPrint('❌ Error regenerating tokens: $e');
return {'fcm_token': null, 'apns_token': null};
}
}
// 🔐 Centralized token expiry handler
Future<void> _handleUnauthorized() async {
debugPrint('🔐 Token expired - Auto logout');
await _localStore.clearAll();
Get.offAllNamed(ConstRouters.login);
Get.snackbar(
'Session Expired',
'Please login again',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
// 🔹 Wrapper to handle token expiry for all authenticated requests
Future<http.Response> _makeAuthenticatedRequest(
Future<http.Response> Function() request,
) async {
try {
final response = await request();
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
return response;
} catch (e) {
rethrow;
}
}
// 🔹 LOGIN API - sends OTP
Future<Map<String, dynamic>> loginUser(LoginModel model) async {
try {
final params = model.toJsonMobile();
final uri = Uri.parse(ConstsApi.login).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 VERIFY OTP (LOGIN) - ✅ Regenerates FCM token
Future<Map<String, dynamic>> verifyOtp(LoginModel model) async {
try {
final params = model.toJsonOtp();
// ✅ Regenerate FCM and APNS tokens on every login
final tokens = await _regenerateFcmToken();
final fcmToken = tokens['fcm_token'];
final apnsToken = tokens['apns_token'];
if (fcmToken != null && fcmToken.isNotEmpty) {
params['fcm_token'] = fcmToken;
}
if (apnsToken != null && apnsToken.isNotEmpty) {
params['apns_token'] = apnsToken;
}
if (params['fcm_token'] == null) {
debugPrint('⚠️ Failed to generate FCM token, trying fallback...');
final fallbackToken = await _getFreshFcmToken();
if (fallbackToken != null && fallbackToken.isNotEmpty) {
params['fcm_token'] = fallbackToken;
}
}
final uri = Uri.parse(ConstsApi.verifyOtp).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
await _localStore.saveLoginData(data);
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 EMPLOYEE LOGIN
Future<Map<String, dynamic>> employeeLogin(String mobile) async {
try {
final uri = Uri.parse(
ConstsApi.employeelogin,
).replace(queryParameters: {'mobile': mobile});
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 SIGNUP API
Future<Map<String, dynamic>> signupUser(SignupModel model) async {
try {
final params = model.toJson();
final uri = Uri.parse(ConstsApi.signup).replace(
queryParameters: params.map(
(key, value) => MapEntry(key, value.toString()),
),
);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 VERIFY OTP (SIGNUP) - ✅ Regenerates FCM token
Future<Map<String, dynamic>> verifySignupOtp(
String mobile,
String otp,
) async {
try {
// ✅ Regenerate FCM and APNS tokens on signup
final tokens = await _regenerateFcmToken();
final fcmToken = tokens['fcm_token'];
final apnsToken = tokens['apns_token'];
final params = {'mobile': mobile, 'otp': otp};
if (fcmToken != null && fcmToken.isNotEmpty) {
params['fcm_token'] = fcmToken;
}
if (apnsToken != null && apnsToken.isNotEmpty) {
params['apns_token'] = apnsToken;
}
if (params['fcm_token'] == null) {
debugPrint('⚠️ Failed to generate FCM token, trying fallback...');
final fallbackToken = await _getFreshFcmToken();
if (fallbackToken != null && fallbackToken.isNotEmpty) {
params['fcm_token'] = fallbackToken;
}
}
final uri = Uri.parse(
ConstsApi.verifyOtp,
).replace(queryParameters: params);
final response = await http.post(uri, headers: _baseHeaders);
final data = jsonDecode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
await _localStore.saveLoginData(data);
return {'success': true, 'data': data};
} else {
return {'success': false, 'error': _extractErrorMessage(data)};
}
} catch (e) {
return {
'success': false,
'error': 'Connection error. Please check your internet.',
};
}
}
// 🔹 FETCH TERMS & CONDITIONS
Future<TermsModel> fetchTermsAndConditions() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.terms),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return TermsModel.fromJson(data);
} else {
throw Exception('Failed to fetch terms and conditions');
}
}
// 🔹 FETCH DASHBOARD
Future<DashBoardModel> fetchDashboard() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.dashboard),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return DashBoardModel.fromJson(data);
} else {
throw Exception('Failed to fetch dashboard');
}
}
// 🔹 FETCH POLICY
Future<TermsModel> fetchpolicy() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.policy),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return TermsModel.fromJson(data);
} else {
throw Exception('Failed to fetch Policy');
}
}
// 🔹 FETCH PROFILE
Future<ProfileGetModel> fetchProfile() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.getprofile),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ProfileGetModel.fromJson(data);
} else {
throw Exception('Failed to fetch Profile');
}
}
// 🔹 FETCH EMPLOYEE PROFILE
Future<EmployeeProfileModel> fetchEmployeeProfile() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.getprofile),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return EmployeeProfileModel.fromJson(data);
} else {
throw Exception('Failed to fetch Employee Profile');
}
}
// 🔹 FETCH SERVICE HISTORY
Future<ServiceListHistoryModel> fetchServiceHistory(
String type, {
String? fromDate,
String? toDate,
}) async {
final uri = Uri.parse(ConstsApi.serivcehistory).replace(
queryParameters: {
'type': type,
if (fromDate != null) 'from_date': fromDate,
if (toDate != null) 'to_date': toDate,
},
);
final response = await _makeAuthenticatedRequest(() async {
return await http.get(uri, headers: await _authorizedHeaders());
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ServiceListHistoryModel.fromJson(data);
} else {
throw Exception('Failed to fetch Service History');
}
}
// 🔹 FETCH COUNT
Future<CountModel> fetchCount(String type) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.count}?chat_id=$type"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return CountModel.fromJson(data);
} else {
throw Exception('Failed to fetch count');
}
}
// 🔹 FETCH NOTIFICATION COUNT
Future<NotificationCountModel> fetchNotificationCount() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.notificationcount),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return NotificationCountModel.fromJson(data);
} else {
throw Exception('Failed to fetch notification count');
}
}
// 🔹 FETCH SERVICE DETAIL
Future<DetailModel> fetchServiceDetail(int id) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.serivcedetails}?service_request_id=$id"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return DetailModel.fromJson(data);
} else {
throw Exception('Failed to fetch Service Detail');
}
}
// 🔹 FETCH CHAT DOCUMENTS
Future<ChatProfileModel> fetchChatDocuments(int id) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.chatdocument}?chat_id=$id"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return ChatProfileModel.fromJson(data);
} else {
throw Exception('Failed to fetch Chat Documents');
}
}
// 🔹 FETCH SERVICE LIST
Future<List<ServiceListModel>> fetchServiceList() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.serivcelist),
headers: await _authorizedHeaders(),
);
});
debugPrint("📡 Status Code: ${response.statusCode}");
debugPrint("📡 Response: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => ServiceListModel.fromJson(e)).toList();
}
throw Exception('Failed to fetch service list');
}
// 🔹 FETCH STAFF LIST
Future<List<StaffModel>> fetchStaffList() async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(ConstsApi.stafflist),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => StaffModel.fromJson(e)).toList();
} else {
throw Exception('Failed to fetch staff list');
}
}
// 🔹 FETCH NOTIFICATION LIST
Future<List<NotificationModel>> fetchNotificationList({
required int page,
}) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.notificationList}?page=$page"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final List<dynamic> list = data['data'] ?? [];
return list.map((e) => NotificationModel.fromJson(e)).toList();
} else {
throw Exception('Failed to fetch notifications');
}
}
// 🔹 FETCH CHAT MESSAGES
Future<List<MessageModel>> fetchChatMessages({
required int chatId,
required int page,
}) async {
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse("${ConstsApi.chatList}/$chatId/messages?page=$page"),
headers: await _authorizedHeaders(),
);
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final messages = data["messages"]["data"] ?? [];
return List<MessageModel>.from(
messages.map((e) => MessageModel.fromJson(e)),
);
} else {
throw Exception("Failed to fetch chat messages → ${response.statusCode}");
}
}
// 🔹 FETCH COUNTRY AND STATES
Future<CountryModel> fetchCountryAndStates(String url) async {
try {
debugPrint('🌐 Fetching Country & States from: $url');
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(url),
headers: await _authorizedHeaders(),
);
});
debugPrint('📡 API Status: ${response.statusCode}');
debugPrint('📡 API Body: ${response.body}');
if (response.statusCode == 200) {
final jsonBody = jsonDecode(response.body);
final model = CountryModel.fromJson(jsonBody);
debugPrint(
'✅ Countries: ${model.countriesData?.length ?? 0}, States: ${model.statesData?.length ?? 0}',
);
return model;
} else {
final error = jsonDecode(response.body);
throw Exception(error['message'] ?? 'Failed to fetch data');
}
} catch (e) {
debugPrint('❌ fetchCountryAndStates Error: $e');
throw Exception('Error fetching country & state data: $e');
}
}
// 🔹 FETCH CITY BY STATE
Future<CityModel> fetchCityByState(String url, int stateId) async {
try {
final fullUrl = "$url?state_id=$stateId";
debugPrint('🌐 Fetching Cities (Districts) from: $fullUrl');
final response = await _makeAuthenticatedRequest(() async {
return await http.get(
Uri.parse(fullUrl),
headers: await _authorizedHeaders(),
);
});
debugPrint('📡 City API Status: ${response.statusCode}');
debugPrint('📡 City API Body: ${response.body}');
if (response.statusCode == 200) {
final jsonBody = jsonDecode(response.body);
final model = CityModel.fromJson(jsonBody);
debugPrint('✅ Districts fetched: ${model.districtsData?.length ?? 0}');
return model;
} else {
final error = jsonDecode(response.body);
throw Exception(error['message'] ?? 'Failed to fetch city data');
}
} catch (e) {
debugPrint('❌ fetchCityByState Error: $e');
throw Exception('Error fetching city data: $e');
}
}
// 🔹 KYC FORM UPDATE
Future<void> kycFormUpdate({
File? logo,
File? panFile,
File? gstFile,
File? incorporationFile,
required int countryId,
required int stateId,
required int cityId,
required String companyPincode,
required String companyAddress,
required String panNumber,
required String gstNumber,
required String tanNumber,
required String cinNumber,
required String yearOfIncorporation,
required String address,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.kycupdate);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
'country_id': countryId.toString(),
'state_id': stateId.toString(),
'district_id': cityId.toString(),
'company_pincode': companyPincode,
'company_address': companyAddress,
'pan_number': panNumber,
'gst_number': gstNumber,
'tan_number': tanNumber,
'cin': cinNumber,
'year_of_incorporation': yearOfIncorporation,
'address': address,
});
await Future.wait([
_addFileIfExists(request, logo, 'logo'),
_addFileIfExists(request, panFile, 'pan_file'),
_addFileIfExists(request, gstFile, 'gst_file'),
_addFileIfExists(request, incorporationFile, 'incorporation_file'),
]);
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ KYC update success');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ KYC update error: $e');
throw {'error': 'Failed to update KYC: ${e.toString()}'};
}
}
// 🔹 SEND CHAT MESSAGE
Future<void> sendChatMessage({
required int chatId,
required String messages,
int? tagId,
List<File>? files,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.sendChatMessage);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
})
..fields.addAll({'chat_id': chatId.toString(), 'message': messages});
if (tagId != null && tagId > 0) {
request.fields['tag_id'] = tagId.toString();
}
if (files != null && files.isNotEmpty) {
for (int i = 0; i < files.length; i++) {
request.files.add(
await http.MultipartFile.fromPath(
'file[$i]',
files[i].path,
filename: files[i].path.split('/').last,
),
);
}
}
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 200) {
debugPrint("✅ Chat message sent successfully");
return;
}
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} catch (e) {
debugPrint('❌ Chat send error: $e');
if (e is Map<String, dynamic>) rethrow;
throw {'error': 'Failed to send message: ${e.toString()}'};
}
}
// 🔹 UPDATE SERVICE REQUEST
Future<void> updateServiceRequest({
required int serviceId,
required String message,
List<File>? documents,
}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.serivcerequest);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
'service_id': serviceId.toString(),
'message': message,
});
if (documents != null && documents.isNotEmpty) {
for (int i = 0; i < documents.length; i++) {
final file = documents[i];
final fileName = file.path.split('/').last;
request.files.add(
await http.MultipartFile.fromPath(
'documents[$i]',
file.path,
filename: fileName,
),
);
}
}
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
// Handle token expiry
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ Service request updated successfully');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ Service request update error: $e');
throw {'error': 'Failed to update service request: ${e.toString()}'};
}
}
Future<void> proformaaccept({required int proformaId}) async {
try {
final token = await _localStore.getToken();
final uri = Uri.parse(ConstsApi.proformaaccept);
var request = http.MultipartRequest('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Connection': 'keep-alive',
})
..fields.addAll({
'id': proformaId.toString(), // ✅ FIXED
});
final streamedResponse = await request.send().timeout(
const Duration(seconds: 60),
onTimeout: () {
throw TimeoutException('Request timed out after 60 seconds');
},
);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 401 || response.statusCode == 403) {
await _handleUnauthorized();
throw Exception('Unauthorized - Token expired');
}
if (response.statusCode == 200) {
debugPrint('✅ Proforma accepted successfully');
return;
}
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 422) {
throw _formatValidationErrors(decoded);
} else {
final message =
decoded?['message'] ?? decoded?['error'] ?? 'Something went wrong';
throw {'error': message.toString()};
}
} on SocketException {
throw {'error': 'No internet connection. Please check your network.'};
} on TimeoutException {
throw {'error': 'Request timed out. Please try again.'};
} on FormatException {
throw {'error': 'Invalid server response. Please try again.'};
} catch (e) {
if (e is Map<String, dynamic>) rethrow;
debugPrint('❌ Proforma accept error: $e');
throw {'error': 'Failed to accept proforma: ${e.toString()}'};
}
}
// 🔹 LOGOUT - ✅ Deletes FCM token
Future<void> logout() async {
try {
final token = await _localStore.getToken();
if (token == null || token.isEmpty) {
debugPrint('⚠️ No token found — clearing data and deleting FCM token');
await _messaging.deleteToken();
await _localStore.clearAll(keepFcm: false);
return;
}
final uri = Uri.parse(ConstsApi.logout);
debugPrint('📡 Logging out from: $uri');
final request = http.Request('POST', uri)
..headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/json',
'Connection': 'keep-alive',
});
final streamedResponse = await request.send().timeout(
const Duration(seconds: 30),
onTimeout: () =>
throw TimeoutException('Logout request timed out after 30 seconds'),
);
final response = await http.Response.fromStream(streamedResponse);
debugPrint('📡 Logout Status Code: ${response.statusCode}');
debugPrint('📦 Logout Body: ${response.body}');
final decoded = _safeJsonDecode(response.body);
if (response.statusCode == 200) {
final message = decoded?['message'] ?? 'Logout successful';
debugPrint('$message');
} else if (response.statusCode == 401) {
debugPrint('⚠️ Token expired or unauthorized. Clearing storage.');
} else {
final message = decoded?['message'] ?? 'Logout failed';
debugPrint('⚠️ $message');
}
} on SocketException {
debugPrint('❌ No internet connection');
} on TimeoutException {
debugPrint('❌ Request timed out');
} catch (e, stack) {
debugPrint('❌ Logout error: $e');
debugPrint('🧩 Stack trace: $stack');
} finally {
// ✅ ALWAYS delete FCM token and clear data on logout
try {
debugPrint('🗑️ Deleting FCM token on logout...');
await _messaging.deleteToken();
debugPrint('✅ FCM token deleted successfully');
} catch (e) {
debugPrint('⚠️ Error deleting FCM token: $e');
}
await _localStore.clearAll(keepFcm: false);
debugPrint('✅ Local storage cleared');
}
}
// Helper methods remain the same...
Map<String, dynamic>? _safeJsonDecode(String body) {
try {
return jsonDecode(body) as Map<String, dynamic>?;
} catch (e) {
debugPrint('⚠️ JSON decode error: $e');
return null;
}
}
String _extractErrorMessage(
dynamic data, [
String defaultMsg = 'Operation failed',
]) {
if (data is! Map) return defaultMsg;
if (data['status'] == 'error') {
if (data['errors'] is Map) {
final errors = data['errors'] as Map;
final firstErrorKey = errors.keys.first;
final errorList = errors[firstErrorKey];
if (errorList is List && errorList.isNotEmpty) {
return errorList.first.toString();
} else if (errorList is String) {
return errorList;
}
} else if (data['errors'] is List &&
(data['errors'] as List).isNotEmpty) {
final firstError = (data['errors'] as List).first;
if (firstError is String) {
return firstError;
} else if (firstError is Map && firstError['message'] != null) {
return firstError['message'];
}
}
}
return data['message'] ??
data['error'] ??
data['msg'] ??
data['detail'] ??
defaultMsg;
}
// Additional helper methods (fetchProfile, fetchServiceHistory, etc.) remain unchanged
// ... (include all other methods from your original code)
}
// 🔹 HELPER: Add file if exists
Future<void> _addFileIfExists(
http.MultipartRequest request,
File? file,
String fieldName,
) async {
if (file == null) return;
try {
if (!file.existsSync()) {
debugPrint('⚠️ File not found: ${file.path}');
return;
}
request.files.add(
await http.MultipartFile.fromPath(
fieldName,
file.path,
filename: basename(file.path),
),
);
} catch (e) {
debugPrint('⚠️ Error adding file $fieldName: $e');
}
}
// // 🔹 HELPER: Safe JSON decode
// Map<String, dynamic>? _safeJsonDecode(String body) {
// try {
// return jsonDecode(body) as Map<String, dynamic>?;
// } catch (e) {
// debugPrint('⚠️ JSON decode error: $e');
// return null;
// }
// }
// // 🔹 HELPER: Extract error message from API response
// String _extractErrorMessage(
// dynamic data, [
// String defaultMsg = 'Operation failed',
// ]) {
// if (data is! Map) return defaultMsg;
// // Check if status is error
// if (data['status'] == 'error') {
// // Handle errors object with field-specific errors
// if (data['errors'] is Map) {
// final errors = data['errors'] as Map;
// final firstErrorKey = errors.keys.first;
// final errorList = errors[firstErrorKey];
// if (errorList is List && errorList.isNotEmpty) {
// return errorList.first.toString();
// } else if (errorList is String) {
// return errorList;
// }
// }
// // Handle errors as List
// else if (data['errors'] is List && (data['errors'] as List).isNotEmpty) {
// final firstError = (data['errors'] as List).first;
// if (firstError is String) {
// return firstError;
// } else if (firstError is Map && firstError['message'] != null) {
// return firstError['message'];
// }
// }
// }
// // Fallback to other common error formats
// return data['message'] ??
// data['error'] ??
// data['msg'] ??
// data['detail'] ??
// defaultMsg;
// }
// 🔹 HELPER: Format validation errors
Map<String, String> _formatValidationErrors(Map<String, dynamic>? decoded) {
if (decoded == null) return {'error': 'Validation failed'};
final rawErrors =
(decoded['errors'] ?? decoded['error'] ?? {}) as Map<String, dynamic>;
final Map<String, String> formattedErrors = {};
rawErrors.forEach((key, value) {
if (value is List && value.isNotEmpty) {
formattedErrors[key] = value.first.toString();
} else if (value is String) {
formattedErrors[key] = value;
} else {
formattedErrors[key] = 'Invalid input';
}
});
return formattedErrors.isNotEmpty
? formattedErrors
: {'error': 'Validation failed'};
}
// ⚡ Optimized HTTP Client for better performance
class OptimizedHttpClient {
static final http.Client _client = http.Client();
static http.Client get client => _client;
static void dispose() {
_client.close();
}
}