1144 lines
36 KiB
Dart
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();
|
|
}
|
|
}
|