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 get _baseHeaders => { 'Content-Type': 'application/json', 'Accept': 'application/json', }; // 🔹 Authorized headers with token Future> _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 _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> _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 _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 _makeAuthenticatedRequest( Future 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> 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> 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> 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> 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> 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 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 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 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 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 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 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 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 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 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 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> 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 list = data['data'] ?? []; return list.map((e) => ServiceListModel.fromJson(e)).toList(); } throw Exception('Failed to fetch service list'); } // 🔹 FETCH STAFF LIST Future> 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 list = data['data'] ?? []; return list.map((e) => StaffModel.fromJson(e)).toList(); } else { throw Exception('Failed to fetch staff list'); } } // 🔹 FETCH NOTIFICATION LIST Future> 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 list = data['data'] ?? []; return list.map((e) => NotificationModel.fromJson(e)).toList(); } else { throw Exception('Failed to fetch notifications'); } } // 🔹 FETCH CHAT MESSAGES Future> 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.from( messages.map((e) => MessageModel.fromJson(e)), ); } else { throw Exception("Failed to fetch chat messages → ${response.statusCode}"); } } // 🔹 FETCH COUNTRY AND STATES Future 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 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 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) rethrow; debugPrint('❌ KYC update error: $e'); throw {'error': 'Failed to update KYC: ${e.toString()}'}; } } // 🔹 SEND CHAT MESSAGE Future sendChatMessage({ required int chatId, required String messages, int? tagId, List? 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) rethrow; throw {'error': 'Failed to send message: ${e.toString()}'}; } } // 🔹 UPDATE SERVICE REQUEST Future updateServiceRequest({ required int serviceId, required String message, List? 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) rethrow; debugPrint('❌ Service request update error: $e'); throw {'error': 'Failed to update service request: ${e.toString()}'}; } } Future 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) rethrow; debugPrint('❌ Proforma accept error: $e'); throw {'error': 'Failed to accept proforma: ${e.toString()}'}; } } // 🔹 LOGOUT - ✅ Deletes FCM token Future 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? _safeJsonDecode(String body) { try { return jsonDecode(body) as Map?; } 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 _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? _safeJsonDecode(String body) { // try { // return jsonDecode(body) as Map?; // } 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 _formatValidationErrors(Map? decoded) { if (decoded == null) return {'error': 'Validation failed'}; final rawErrors = (decoded['errors'] ?? decoded['error'] ?? {}) as Map; final Map 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(); } }