Added dev tools feature

faiz_dev_tools_imp
faizatflutter 2 weeks ago
parent e44856167f
commit 17c45e1656

@ -1960,12 +1960,56 @@
"refund": "استرداد", "refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:", "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول", "schedule": "جدول",
"doctorSchedule": "جدول الطبيب", "doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية", "weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة", "dayOff": "إجازة",
"notAvailable": "غير متاح", "notAvailable": "غير متاح",
"hide": "إخفاء" "hide": "إخفاء",
"environmentConfig": "إعدادات البيئة",
"apiLogs": "Api سجلات",
"searchByUrl": "البحث حسب عنوان URL أو النقطة النهائية أو الطريقة أو البيئة...",
"total": "المجموع",
"success": "نجح",
"failed": "فشل",
"clearAll": "مسح الكل",
"exportAll": "تصدير الكل",
"showingLogs": "عرض {count} من {total} سجلات",
"noApiLogsYet": "لا توجد سجلات API حتى الآن",
"noLogsMatch": "لا توجد سجلات مطابقة لبحثك",
"apiCallsWillAppear": "ستظهر مكالمات API هنا بمجرد إجرائها",
"clearAllLogs": "مسح جميع السجلات",
"clearLogsConfirm": "هل أنت متأكد أنك تريد مسح جميع سجلات API؟ لا يمكن التراجع عن هذا الإجراء.",
"allLogsCleared": "تم مسح جميع السجلات بنجاح",
"allLogsCopied": "تم نسخ جميع السجلات إلى الحافظة",
"request": "الطلب",
"response": "الاستجابة",
"headers": "الرؤوس",
"copyAsText": "نسخ كنص",
"copiedAsText": "تم النسخ كنص",
"copyAsCurl": "نسخ كـ cURL",
"copiedAsCurl": "تم النسخ كـ cURL",
"copyJson": "نسخ JSON",
"responseJsonCopied": "تم نسخ JSON الاستجابة",
"currentEnvironment": "البيئة الحالية",
"production": "الإنتاج",
"selectEnvironment": "اختر البيئة",
"environmentDetails": "تفاصيل البيئة",
"baseUrl": "عنوان URL الأساسي",
"rcUrl": "عنوان RC URL",
"pharmacyApi": "واجهة برمجة الصيدلية",
"payfortMode": "وضع Payfort",
"sandbox": "بيئة الاختبار",
"applePayMerchant": "معرف تاجر Apple Pay",
"important": "مهم",
"envChangeWarning": "سيؤدي تغيير البيئة إلى إعادة تشغيل التطبيق ومسح جميع البيانات المخزنة مؤقتًا. تأكد من حفظ أي عمل غير محفوظ.",
"resetToDefault": "إعادة تعيين إلى الافتراضي",
"saveAndRestart": "حفظ وإعادة التشغيل",
"resetEnvironment": "إعادة تعيين البيئة",
"resetEnvConfirm": "هل أنت متأكد أنك تريد إعادة التعيين إلى بيئة الإنتاج الافتراضية؟ سيؤدي هذا إلى إعادة تشغيل التطبيق.",
"envResetSuccess": "تم إعادة تعيين البيئة بنجاح",
"envUpdateSuccess": "تم تحديث البيئة بنجاح",
"envUpdateFailed": "فشل تحديث البيئة"
} }

@ -1949,14 +1949,58 @@
"invoiceHistory": "Invoice History", "invoiceHistory": "Invoice History",
"parkingQR": "Parking QR Code", "parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:", "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.", "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
"livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?", "livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
"schedule": "Schedule", "schedule": "Schedule",
"doctorSchedule": "Doctor Schedule", "doctorSchedule": "Doctor Schedule",
"weeklyShift": "Weekly Shift", "weeklyShift": "Weekly Shift",
"dayOff": "Off", "dayOff": "Off",
"notAvailable": "Not Available", "notAvailable": "Not Available",
"hide": "Hide" "hide": "Hide",
"apiLogs": "Api Logs",
"environmentConfig": "Environment Config",
"searchByUrl": "Search by URL, endpoint, method, or environment...",
"total": "Total",
"success": "Success",
"failed": "Failed",
"clearAll": "Clear All",
"exportAll": "Export All",
"showingLogs": "Showing {count} of {total} logs",
"noApiLogsYet": "No API Logs Yet",
"noLogsMatch": "No logs match your search",
"apiCallsWillAppear": "API calls will appear here once made",
"clearAllLogs": "Clear All Logs",
"clearLogsConfirm": "Are you sure you want to clear all API logs? This action cannot be undone.",
"allLogsCleared": "All logs cleared successfully",
"allLogsCopied": "All logs copied to clipboard",
"request": "Request",
"response": "Response",
"headers": "Headers",
"copyAsText": "Copy as Text",
"copiedAsText": "Copied as text",
"copyAsCurl": "Copy as cURL",
"copiedAsCurl": "Copied as cURL",
"copyJson": "Copy JSON",
"responseJsonCopied": "Response JSON copied",
"currentEnvironment": "Current Environment",
"production": "Production",
"selectEnvironment": "Select Environment",
"environmentDetails": "Environment Details",
"baseUrl": "Base URL",
"rcUrl": "RC URL",
"pharmacyApi": "Pharmacy API",
"payfortMode": "Payfort Mode",
"sandbox": "Sandbox",
"applePayMerchant": "Apple Pay Merchant",
"important": "Important",
"envChangeWarning": "Changing the environment will restart the app and clear all cached data. Make sure you have saved any unsaved work.",
"resetToDefault": "Reset to Default",
"saveAndRestart": "Save & Restart",
"resetEnvironment": "Reset Environment",
"resetEnvConfirm": "Are you sure you want to reset to the default production environment? This will restart the app.",
"envResetSuccess": "Environment reset successfully",
"envUpdateSuccess": "Environment updated successfully",
"envUpdateFailed": "Failed to update environment"
} }

@ -1,7 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/dev_tools/api_logger_service.dart';
import 'package:hmg_patient_app_new/features/dev_tools/models/api_log_entry.dart';
import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -28,13 +32,44 @@ class HttpClientManager {
Duration? timeout, Duration? timeout,
int maxRetries = _maxRetries, int maxRetries = _maxRetries,
}) async { }) async {
return _executeWithRetry( final startTime = DateTime.now();
() => _client
.post(uri, headers: headers, body: body) try {
.timeout(timeout ?? _defaultTimeout), final response = await _executeWithRetry(
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(), () => _client
maxRetries: maxRetries, .post(uri, headers: headers, body: body)
); .timeout(timeout ?? _defaultTimeout),
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(),
maxRetries: maxRetries,
);
// Log successful API call
if (ApiConsts.enableDevTools) {
_logApiCall(
method: 'POST',
uri: uri,
headers: headers,
requestBody: body,
response: response,
startTime: startTime,
);
}
return response;
} catch (e) {
// Log failed API call
if (ApiConsts.enableDevTools) {
_logApiCall(
method: 'POST',
uri: uri,
headers: headers,
requestBody: body,
error: e,
startTime: startTime,
);
}
rethrow;
}
} }
/// Execute GET request with retry logic /// Execute GET request with retry logic
@ -44,13 +79,42 @@ class HttpClientManager {
Duration? timeout, Duration? timeout,
int maxRetries = _maxRetries, int maxRetries = _maxRetries,
}) async { }) async {
return _executeWithRetry( final startTime = DateTime.now();
() => _client
.get(uri, headers: headers) try {
.timeout(timeout ?? _defaultTimeout), final response = await _executeWithRetry(
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(), () => _client
maxRetries: maxRetries, .get(uri, headers: headers)
); .timeout(timeout ?? _defaultTimeout),
endpoint: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.toString(),
maxRetries: maxRetries,
);
// Log successful API call
if (ApiConsts.enableDevTools) {
_logApiCall(
method: 'GET',
uri: uri,
headers: headers,
response: response,
startTime: startTime,
);
}
return response;
} catch (e) {
// Log failed API call
if (ApiConsts.enableDevTools) {
_logApiCall(
method: 'GET',
uri: uri,
headers: headers,
error: e,
startTime: startTime,
);
}
rethrow;
}
} }
/// Core retry logic for HTTP requests /// Core retry logic for HTTP requests
@ -212,6 +276,68 @@ class HttpClientManager {
await Future.delayed(delay); await Future.delayed(delay);
} }
/// Log API call to DevTools
void _logApiCall({
required String method,
required Uri uri,
Map<String, String>? headers,
Object? requestBody,
http.Response? response,
Object? error,
required DateTime startTime,
}) {
final duration = DateTime.now().difference(startTime);
final endpoint = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : uri.path;
Map<String, dynamic>? parsedRequestBody;
if (requestBody != null) {
try {
if (requestBody is String) {
parsedRequestBody = json.decode(requestBody);
} else {
parsedRequestBody = {'body': requestBody.toString()};
}
} catch (_) {
parsedRequestBody = {'raw': requestBody.toString()};
}
}
dynamic parsedResponseBody;
int statusCode = 0;
bool isSuccess = false;
if (response != null) {
statusCode = response.statusCode;
isSuccess = statusCode >= 200 && statusCode < 300;
try {
parsedResponseBody = json.decode(response.body);
} catch (_) {
parsedResponseBody = response.body;
}
} else if (error != null) {
parsedResponseBody = error.toString();
statusCode = 0;
isSuccess = false;
}
final logEntry = ApiLogEntry(
id: DateTime.now().millisecondsSinceEpoch.toString(),
timestamp: startTime,
method: method,
url: uri.toString(),
endpoint: endpoint,
environment: ApiLogEntry.envFromUrl(uri.toString()),
headers: headers ?? {},
requestBody: parsedRequestBody,
responseBody: parsedResponseBody,
statusCode: statusCode,
isSuccess: isSuccess,
duration: duration,
);
ApiLoggerService.instance.addLog(logEntry);
}
/// Close the HTTP client /// Close the HTTP client
void dispose() { void dispose() {
_client.close(); _client.close();

@ -5,6 +5,9 @@ import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts { class ApiConsts {
static const maxSmallScreen = 660; static const maxSmallScreen = 660;
// DevTools Configuration
static const bool enableDevTools = true; // Set to false for production builds
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod;
// static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT

@ -0,0 +1,31 @@
import 'package:flutter/foundation.dart';
import 'package:hmg_patient_app_new/features/dev_tools/models/api_log_entry.dart';
/// Singleton service that collects API call logs in-memory.
class ApiLoggerService extends ChangeNotifier {
ApiLoggerService._internal();
static final ApiLoggerService instance = ApiLoggerService._internal();
final List<ApiLogEntry> _logs = [];
List<ApiLogEntry> get logs => List.unmodifiable(_logs.reversed.toList());
void addLog(ApiLogEntry entry) {
_logs.add(entry);
// Keep only last 500 logs to prevent memory issues
if (_logs.length > 500) _logs.removeAt(0);
notifyListeners();
}
void clear() {
_logs.clear();
notifyListeners();
}
int get totalCalls => _logs.length;
int get successCalls => _logs.where((log) => log.isSuccess).length;
int get failedCalls => _logs.where((log) => !log.isSuccess).length;
}

@ -0,0 +1,186 @@
import 'package:amazon_payfort/amazon_payfort.dart';
import 'package:flutter_amazonpaymentservices/environment_type.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
/// Preset environment configuration
class AppEnvironmentPreset {
final String label;
final AppEnvironmentTypeEnum environmentType;
final String baseUrl;
final String rcBaseUrl;
final String hmgPharmacyApiBaseUrl;
final String tamaraUrl;
final String getTamaraInstallmentsUrl;
final String getTamaraPaymentStatus;
final String qlineUrl;
final String chatUrl;
final String serviceUrl;
final FortEnvironment payFortEnvironment;
final EnvironmentType payFortEnvironmentType;
final String applePayMerchantId;
final String symptomsCheckerUsername;
final String symptomsCheckerPassword;
const AppEnvironmentPreset({
required this.label,
required this.environmentType,
required this.baseUrl,
required this.rcBaseUrl,
required this.hmgPharmacyApiBaseUrl,
required this.tamaraUrl,
required this.getTamaraInstallmentsUrl,
required this.getTamaraPaymentStatus,
required this.qlineUrl,
required this.chatUrl,
required this.serviceUrl,
required this.payFortEnvironment,
required this.payFortEnvironmentType,
required this.applePayMerchantId,
required this.symptomsCheckerUsername,
required this.symptomsCheckerPassword,
});
}
/// Predefined environment presets
const List<AppEnvironmentPreset> kEnvironmentPresets = [
AppEnvironmentPreset(
label: 'Production',
environmentType: AppEnvironmentTypeEnum.prod,
baseUrl: 'https://hmgwebservices.com/',
rcBaseUrl: 'https://rc.hmg.com/',
hmgPharmacyApiBaseUrl: 'https://hmgpharmacyapi.hmg.com/',
tamaraUrl: 'https://mdlaboratories.com/tamaralive/Home/Checkout',
getTamaraInstallmentsUrl: 'https://mdlaboratories.com/tamaralive/Home/GetInstallments',
getTamaraPaymentStatus: 'https://mdlaboratories.com/tamaralive/api/OnlineTamara/order_status?orderid=',
qlineUrl: 'https://qline.hmg.com/api/PatientCall/PatientInQueue_Detail',
chatUrl: 'https://chat.hmg.com/geneysChat/Index.aspx?RequestedId=',
serviceUrl: 'https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx',
payFortEnvironment: FortEnvironment.production,
payFortEnvironmentType: EnvironmentType.production,
applePayMerchantId: 'merchant.com.hmgwebservices',
symptomsCheckerUsername: 'mobile_user',
symptomsCheckerPassword: '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92',
),
AppEnvironmentPreset(
label: 'UAT',
environmentType: AppEnvironmentTypeEnum.uat,
baseUrl: 'https://uat.hmgwebservices.com/',
rcBaseUrl: 'https://rc.hmg.com/uat/',
hmgPharmacyApiBaseUrl: 'https://hmgpharmacyapi.hmg.com/',
tamaraUrl: 'https://epharmacy.hmg.com/tamara/Home/Checkout',
getTamaraInstallmentsUrl: 'https://epharmacy.hmg.com/tamara/Home/getinstallments',
getTamaraPaymentStatus: 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid=',
qlineUrl: 'https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail',
chatUrl: 'https://chat.hmg.com/geneysChat/Index.aspx?RequestedId=',
serviceUrl: 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx',
payFortEnvironment: FortEnvironment.test,
payFortEnvironmentType: EnvironmentType.sandbox,
applePayMerchantId: 'merchant.com.hmgwebservices.uat',
symptomsCheckerUsername: 'guest_user',
symptomsCheckerPassword: '123456',
),
AppEnvironmentPreset(
label: 'Dev',
environmentType: AppEnvironmentTypeEnum.dev,
baseUrl: 'https://uat.hmgwebservices.com/',
rcBaseUrl: 'https://rc.hmg.com/uat/',
hmgPharmacyApiBaseUrl: 'https://hmgpharmacyapi.hmg.com/',
tamaraUrl: 'https://epharmacy.hmg.com/tamara/Home/Checkout',
getTamaraInstallmentsUrl: 'https://epharmacy.hmg.com/tamara/Home/getinstallments',
getTamaraPaymentStatus: 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid=',
qlineUrl: 'https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail',
chatUrl: 'https://chat.hmg.com/geneysChat/Index.aspx?RequestedId=',
serviceUrl: 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx',
payFortEnvironment: FortEnvironment.test,
payFortEnvironmentType: EnvironmentType.sandbox,
applePayMerchantId: 'merchant.com.hmgwebservices.uat',
symptomsCheckerUsername: 'guest_user',
symptomsCheckerPassword: '123456',
),
];
// Keys for SharedPreferences
const String envStoredEnvironmentKey = 'dev_tools_stored_environment';
/// Service to manage environment configuration
class EnvironmentConfigService {
static final EnvironmentConfigService instance = EnvironmentConfigService._internal();
EnvironmentConfigService._internal();
final CacheService _cacheService = getIt.get<CacheService>();
/// Apply stored environment or fall back to production defaults
Future<void> applyStoredEnvironment() async {
final storedEnvLabel = _cacheService.getString(key: envStoredEnvironmentKey);
if (storedEnvLabel != null && storedEnvLabel.isNotEmpty) {
try {
final preset = kEnvironmentPresets.firstWhere((p) => p.label == storedEnvLabel);
_applyPresetToApiConsts(preset);
} catch (_) {
// If stored environment not found, use default
_applyDefaultEnvironment();
}
} else {
_applyDefaultEnvironment();
}
}
/// Save and apply preset environment
Future<void> applyPreset(AppEnvironmentPreset preset) async {
await _cacheService.saveString(key: envStoredEnvironmentKey, value: preset.label);
_applyPresetToApiConsts(preset);
}
/// Reset to default production environment
Future<void> resetToDefault() async {
await _cacheService.remove(key: envStoredEnvironmentKey);
_applyDefaultEnvironment();
}
/// Get currently stored environment
String? getCurrentEnvironment() {
return _cacheService.getString(key: envStoredEnvironmentKey);
}
/// Get current preset if matches
AppEnvironmentPreset? getCurrentPreset() {
final storedEnv = getCurrentEnvironment();
if (storedEnv == null) return kEnvironmentPresets[0]; // Default to production
try {
return kEnvironmentPresets.firstWhere((p) => p.label == storedEnv);
} catch (_) {
return null;
}
}
void _applyPresetToApiConsts(AppEnvironmentPreset preset) {
ApiConsts.appEnvironmentType = preset.environmentType;
ApiConsts.baseUrl = preset.baseUrl;
ApiConsts.rcBaseUrl = preset.rcBaseUrl;
ApiConsts.hmgPharmacyApiBaseUrl = preset.hmgPharmacyApiBaseUrl;
ApiConsts.symptomsCheckerApi = '${preset.hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker';
ApiConsts.symptomsCheckerScheduleAppointment = '${ApiConsts.symptomsCheckerApi}/ScheduleAppointment';
ApiConsts.TAMARA_URL = preset.tamaraUrl;
ApiConsts.GET_TAMARA_INSTALLMENTS_URL = preset.getTamaraInstallmentsUrl;
ApiConsts.GET_TAMARA_PAYMENT_STATUS = preset.getTamaraPaymentStatus;
ApiConsts.QLINE_URL = preset.qlineUrl;
ApiConsts.CHAT_URL = preset.chatUrl;
ApiConsts.SERVICE_URL = preset.serviceUrl;
ApiConsts.payFortEnvironment = preset.payFortEnvironment;
ApiConsts.payFortEnvironmentType = preset.payFortEnvironmentType;
ApiConsts.applePayMerchantId = preset.applePayMerchantId;
ApiConsts.symptomsCheckerUsername = preset.symptomsCheckerUsername;
ApiConsts.symptomsCheckerPassword = preset.symptomsCheckerPassword;
}
void _applyDefaultEnvironment() {
// Apply production as default
_applyPresetToApiConsts(kEnvironmentPresets[0]);
}
}

@ -0,0 +1,108 @@
import 'dart:convert';
/// Holds the details of a single API call.
class ApiLogEntry {
final String id;
final DateTime timestamp;
final String method;
final String url;
final String endpoint;
final String environment;
final Map<String, String> headers;
final Map<String, dynamic>? requestBody;
final dynamic responseBody;
final int statusCode;
final bool isSuccess;
final Duration? duration;
ApiLogEntry({
required this.id,
required this.timestamp,
required this.method,
required this.url,
required this.endpoint,
this.environment = '',
required this.headers,
this.requestBody,
this.responseBody,
required this.statusCode,
required this.isSuccess,
this.duration,
});
/// Derives a short environment label from a full URL string.
static String envFromUrl(String url) {
if (url.contains('vidamergeuat')) return 'VidaMerge';
if (url.contains('vidauat')) return 'VidaPlus';
if (url.contains('webservices.hmg.com')) return 'PreProd';
if (url.contains('uat.hmgwebservices')) return 'UAT';
if (url.contains('rc.hmg.com') && url.contains('/test')) return 'RC-Test';
if (url.contains('ms.hmg.com/rc')) return 'MS-RC';
if (url.contains('rc.hmg.com')) return 'RC';
if (url.contains('mdlaboratories.com/exacartapitest')) return 'Pharmacy-Test';
if (url.contains('mdlaboratories.com')) return 'Pharmacy';
if (url.contains('webservices.hmg.com/ClientService')) return 'RSG';
if (url.contains('hmgwebservices.com')) return 'Production';
final uri = Uri.tryParse(url);
return uri?.host.isNotEmpty == true ? uri!.host.split('.').first : 'External';
}
/// Returns a pretty-printed JSON string of the request body.
String get prettyRequestBody {
if (requestBody == null) return '(none)';
try {
return const JsonEncoder.withIndent(' ').convert(requestBody);
} catch (_) {
return requestBody.toString();
}
}
/// Returns a pretty-printed JSON string of the response body.
String get prettyResponseBody {
if (responseBody == null) return '(none)';
try {
if (responseBody is String) {
final decoded = json.decode(responseBody as String);
return const JsonEncoder.withIndent(' ').convert(decoded);
}
return const JsonEncoder.withIndent(' ').convert(responseBody);
} catch (_) {
return responseBody.toString();
}
}
/// Returns formatted text report.
String toFormattedText() {
final buf = StringBuffer();
buf.writeln('═══════════════════════════════════');
buf.writeln('[$method] $url');
buf.writeln('Status : $statusCode | ${isSuccess ? '✅ Success' : '❌ Failed'}');
if (environment.isNotEmpty) buf.writeln('Env : $environment');
buf.writeln('Time : ${timestamp.toIso8601String()}');
if (duration != null) buf.writeln('Duration: ${duration!.inMilliseconds}ms');
buf.writeln('───────────────── HEADERS ─────────────────');
headers.forEach((k, v) => buf.writeln('$k: $v'));
buf.writeln('───────────────── REQUEST ─────────────────');
buf.writeln(prettyRequestBody);
buf.writeln('──────────────── RESPONSE ─────────────────');
buf.writeln(prettyResponseBody);
buf.writeln('═══════════════════════════════════');
return buf.toString();
}
/// Returns a cURL command equivalent.
String toCurl() {
final buf = StringBuffer();
buf.write("curl -X $method \\\n");
buf.write(" '${url.trim()}' \\\n");
headers.forEach((k, v) {
buf.write(" -H '$k: $v' \\\n");
});
if (requestBody != null && method != 'GET') {
final bodyStr = json.encode(requestBody).replaceAll("'", "\\'");
buf.write(" -d '$bodyStr'");
}
return buf.toString();
}
}

@ -1884,4 +1884,54 @@ abstract class LocaleKeys {
static const advanceNumber = 'advanceNumber'; static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly'; static const amountOnly = 'amountOnly';
static const invoiceHistory = 'invoiceHistory'; static const invoiceHistory = 'invoiceHistory';
static const refund = 'refund';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';
static const livecareUpdateInsurance = 'livecareUpdateInsurance';
static const schedule = 'schedule';
static const doctorSchedule = 'doctorSchedule';
static const weeklyShift = 'weeklyShift';
static const dayOff = 'dayOff';
static const hide = 'hide';
static const apiLogs = 'apiLogs';
static const environmentConfig = 'environmentConfig';
static const searchByUrl = 'searchByUrl';
static const clearAll = 'clearAll';
static const exportAll = 'exportAll';
static const showingLogs = 'showingLogs';
static const noApiLogsYet = 'noApiLogsYet';
static const noLogsMatch = 'noLogsMatch';
static const apiCallsWillAppear = 'apiCallsWillAppear';
static const clearAllLogs = 'clearAllLogs';
static const clearLogsConfirm = 'clearLogsConfirm';
static const allLogsCleared = 'allLogsCleared';
static const allLogsCopied = 'allLogsCopied';
static const response = 'response';
static const headers = 'headers';
static const copyAsText = 'copyAsText';
static const copiedAsText = 'copiedAsText';
static const copyAsCurl = 'copyAsCurl';
static const copiedAsCurl = 'copiedAsCurl';
static const copyJson = 'copyJson';
static const responseJsonCopied = 'responseJsonCopied';
static const currentEnvironment = 'currentEnvironment';
static const production = 'production';
static const selectEnvironment = 'selectEnvironment';
static const environmentDetails = 'environmentDetails';
static const baseUrl = 'baseUrl';
static const rcUrl = 'rcUrl';
static const pharmacyApi = 'pharmacyApi';
static const payfortMode = 'payfortMode';
static const sandbox = 'sandbox';
static const applePayMerchant = 'applePayMerchant';
static const important = 'important';
static const envChangeWarning = 'envChangeWarning';
static const resetToDefault = 'resetToDefault';
static const saveAndRestart = 'saveAndRestart';
static const resetEnvironment = 'resetEnvironment';
static const resetEnvConfirm = 'resetEnvConfirm';
static const envResetSuccess = 'envResetSuccess';
static const envUpdateSuccess = 'envUpdateSuccess';
static const envUpdateFailed = 'envUpdateFailed';
} }

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names
abstract class LocaleKeys { abstract class LocaleKeys {
static const english = 'english'; static const english = 'english';
static const arabic = 'arabic'; static const arabic = 'arabic';
static const login = 'login'; static const login = 'login';
@ -1948,5 +1948,44 @@ abstract class LocaleKeys {
static const weeklyShift = 'weeklyShift'; static const weeklyShift = 'weeklyShift';
static const dayOff = 'dayOff'; static const dayOff = 'dayOff';
static const hide = 'hide'; static const hide = 'hide';
static const environmentConfig = 'environmentConfig';
static const apiLogs = 'apiLogs';
static const searchByUrl = 'searchByUrl';
static const clearAll = 'clearAll';
static const exportAll = 'exportAll';
static const showingLogs = 'showingLogs';
static const noApiLogsYet = 'noApiLogsYet';
static const noLogsMatch = 'noLogsMatch';
static const apiCallsWillAppear = 'apiCallsWillAppear';
static const clearAllLogs = 'clearAllLogs';
static const clearLogsConfirm = 'clearLogsConfirm';
static const allLogsCleared = 'allLogsCleared';
static const allLogsCopied = 'allLogsCopied';
static const response = 'response';
static const headers = 'headers';
static const copyAsText = 'copyAsText';
static const copiedAsText = 'copiedAsText';
static const copyAsCurl = 'copyAsCurl';
static const copiedAsCurl = 'copiedAsCurl';
static const copyJson = 'copyJson';
static const responseJsonCopied = 'responseJsonCopied';
static const currentEnvironment = 'currentEnvironment';
static const production = 'production';
static const selectEnvironment = 'selectEnvironment';
static const environmentDetails = 'environmentDetails';
static const baseUrl = 'baseUrl';
static const rcUrl = 'rcUrl';
static const pharmacyApi = 'pharmacyApi';
static const payfortMode = 'payfortMode';
static const sandbox = 'sandbox';
static const applePayMerchant = 'applePayMerchant';
static const important = 'important';
static const envChangeWarning = 'envChangeWarning';
static const resetToDefault = 'resetToDefault';
static const saveAndRestart = 'saveAndRestart';
static const resetEnvironment = 'resetEnvironment';
static const resetEnvConfirm = 'resetEnvConfirm';
static const envResetSuccess = 'envResetSuccess';
static const envUpdateSuccess = 'envUpdateSuccess';
static const envUpdateFailed = 'envUpdateFailed';
} }

@ -7,6 +7,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart';
@ -16,6 +17,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_
import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart';
import 'package:hmg_patient_app_new/features/dev_tools/environment_config_service.dart';
import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_view_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
@ -128,6 +130,11 @@ Future<void> callInitializations() async {
// Initialize App Lifecycle Service to monitor background/foreground transitions // Initialize App Lifecycle Service to monitor background/foreground transitions
getIt.get<AppLifecycleService>().initialize(); getIt.get<AppLifecycleService>().initialize();
// Load DevTools environment configuration
if (ApiConsts.enableDevTools) {
await EnvironmentConfigService.instance.applyStoredEnvironment();
}
// Restore persisted dark-mode preference before the first frame. // Restore persisted dark-mode preference before the first frame.
getIt.get<ProfileSettingsViewModel>().loadDarkMode(); getIt.get<ProfileSettingsViewModel>().loadDarkMode();

@ -0,0 +1,565 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/dev_tools/api_logger_service.dart';
import 'package:hmg_patient_app_new/features/dev_tools/models/api_log_entry.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
class ApiLogScreen extends StatefulWidget {
const ApiLogScreen({super.key});
@override
State<ApiLogScreen> createState() => _ApiLogScreenState();
}
class _ApiLogScreenState extends State<ApiLogScreen> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
List<ApiLogEntry> _filterLogs(List<ApiLogEntry> logs) {
if (_searchQuery.isEmpty) return logs;
return logs.where((log) {
final query = _searchQuery.toLowerCase();
return log.url.toLowerCase().contains(query) ||
log.endpoint.toLowerCase().contains(query) ||
log.method.toLowerCase().contains(query) ||
log.environment.toLowerCase().contains(query);
}).toList();
}
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.apiLogs.tr(context: context),
isClose: true,
child: ListenableBuilder(
listenable: ApiLoggerService.instance,
builder: (context, _) {
final allLogs = ApiLoggerService.instance.logs;
final filteredLogs = _filterLogs(allLogs);
final totalCalls = ApiLoggerService.instance.totalCalls;
final successCalls = ApiLoggerService.instance.successCalls;
final failedCalls = ApiLoggerService.instance.failedCalls;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search Bar
Container(
margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 16.h),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
decoration: InputDecoration(
hintText: LocaleKeys.searchByUrl.tr(context: context),
border: InputBorder.none,
icon: Icon(Icons.search, color: AppColors.greyInfoTextColor),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: AppColors.greyInfoTextColor),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
),
),
),
// Statistics Bar
Container(
margin: EdgeInsets.only(left: 24.w, right: 24.w, bottom: 16.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(LocaleKeys.total.tr(context: context), totalCalls.toString(), AppColors.primaryRedColor),
Container(width: 1, height: 40.h, color: AppColors.greyInfoTextColor),
_buildStatItem(LocaleKeys.success.tr(context: context), successCalls.toString(), AppColors.successColor),
Container(width: 1, height: 40.h, color: AppColors.greyInfoTextColor),
_buildStatItem(LocaleKeys.failed.tr(context: context), failedCalls.toString(), AppColors.errorColor),
],
),
),
// Action Buttons
Padding(
padding: EdgeInsets.only(left: 24.w, right: 24.w, bottom: 16.h),
child: Row(
children: [
Expanded(
child: _buildActionButton(
LocaleKeys.clearAll.tr(context: context),
Icons.delete_outline,
AppColors.errorColor,
() => _showClearConfirmation(),
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildActionButton(
LocaleKeys.exportAll.tr(context: context),
Icons.file_download_outlined,
AppColors.infoColor,
filteredLogs.isEmpty ? null : () => _exportAllLogs(filteredLogs),
),
),
],
),
),
// Results Count
if (filteredLogs.isNotEmpty)
Padding(
padding: EdgeInsets.only(left: 24.w, right: 24.w, bottom: 12.h),
child: Text(
LocaleKeys.showingLogs
.tr(context: context)
.replaceAll('{count}', filteredLogs.length.toString())
.replaceAll('{total}', totalCalls.toString()),
style: TextStyle(fontSize: 14.f, color: AppColors.greyInfoTextColor),
),
),
// Logs List
if (filteredLogs.isEmpty)
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 60.h),
Icon(Icons.api_outlined, size: 80.w, color: AppColors.greyInfoTextColor),
SizedBox(height: 16.h),
Text(
_searchQuery.isEmpty ? LocaleKeys.noApiLogsYet.tr(context: context) : LocaleKeys.noLogsMatch.tr(context: context),
style: TextStyle(fontSize: 16.f, color: AppColors.greyInfoTextColor),
),
if (_searchQuery.isEmpty) ...[
SizedBox(height: 8.h),
Text(
LocaleKeys.apiCallsWillAppear.tr(context: context),
style: TextStyle(fontSize: 14.f, color: AppColors.greyInfoTextColor),
),
],
],
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.w, right: 24.w, bottom: 24.h),
itemCount: filteredLogs.length,
itemBuilder: (context, index) {
return _ApiLogTile(log: filteredLogs[index]);
},
),
],
);
},
),
);
}
Widget _buildStatItem(String label, String value, Color color) {
return Column(
children: [
Text(value, style: TextStyle(fontSize: 20.f, fontWeight: FontWeight.bold, color: color)),
SizedBox(height: 4.h),
Text(label, style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor)),
],
);
}
Widget _buildActionButton(String label, IconData icon, Color color, VoidCallback? onTap) {
final isEnabled = onTap != null;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.r),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 16.w),
decoration: BoxDecoration(
color: isEnabled ? color.withValues(alpha: 0.1) : AppColors.greyInfoTextColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isEnabled ? color.withValues(alpha: 0.3) : AppColors.greyInfoTextColor.withValues(alpha: 0.3),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: isEnabled ? color : AppColors.greyInfoTextColor, size: 20.w),
SizedBox(width: 8.w),
Text(label, style: TextStyle(fontSize: 14.f, color: isEnabled ? color : AppColors.greyInfoTextColor, fontWeight: FontWeight.bold)),
],
),
),
);
}
void _showClearConfirmation() {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.clearAllLogs.tr(context: context),
child: Utils.getWarningWidget(
loadingText: LocaleKeys.clearLogsConfirm.tr(context: context),
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () {
ApiLoggerService.instance.clear();
Navigator.pop(context);
Utils.showToast(LocaleKeys.allLogsCleared.tr(context: context));
},
),
callBackFunc: () {},
isFullScreen: false,
);
}
void _exportAllLogs(List<ApiLogEntry> logs) {
final buffer = StringBuffer();
buffer.writeln('═════════════════════════════════════════════');
buffer.writeln('API LOGS EXPORT');
buffer.writeln('Generated: ${DateTime.now()}');
buffer.writeln('Total Logs: ${logs.length}');
buffer.writeln('═════════════════════════════════════════════\n');
for (var log in logs) {
buffer.writeln(log.toFormattedText());
buffer.writeln('\n');
}
Clipboard.setData(ClipboardData(text: buffer.toString()));
Utils.showToast(LocaleKeys.allLogsCopied.tr(context: context));
}
}
class _ApiLogTile extends StatefulWidget {
final ApiLogEntry log;
const _ApiLogTile({required this.log});
@override
State<_ApiLogTile> createState() => _ApiLogTileState();
}
class _ApiLogTileState extends State<_ApiLogTile> {
bool _isExpanded = false;
int _selectedTabIndex = 0;
@override
Widget build(BuildContext context) {
final log = widget.log;
final statusColor = log.isSuccess ? AppColors.successColor : AppColors.errorColor;
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
borderRadius: BorderRadius.circular(16.r),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Method Badge
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: _getMethodColor(log.method).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
log.method,
style: TextStyle(fontSize: 12.f, color: _getMethodColor(log.method), fontWeight: FontWeight.bold),
),
),
SizedBox(width: 8.w),
// Status Code
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Text(
log.statusCode.toString(),
style: TextStyle(fontSize: 12.f, color: statusColor, fontWeight: FontWeight.bold),
),
),
const Spacer(),
// Duration
if (log.duration != null)
Text('${log.duration!.inMilliseconds}ms', style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor)),
SizedBox(width: 8.w),
Icon(
_isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
color: AppColors.greyInfoTextColor,
),
],
),
SizedBox(height: 8.h),
// URL
Text(
log.url,
style: TextStyle(fontSize: 14.f),
maxLines: _isExpanded ? null : 2,
overflow: _isExpanded ? null : TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
// Environment & Timestamp
Row(
children: [
if (log.environment.isNotEmpty) ...[
Icon(Icons.label_outline, size: 14.w, color: AppColors.greyInfoTextColor),
SizedBox(width: 4.w),
Text(log.environment, style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor)),
SizedBox(width: 12.w),
],
Icon(Icons.access_time, size: 14.w, color: AppColors.greyInfoTextColor),
SizedBox(width: 4.w),
Text(
DateFormat('HH:mm:ss').format(log.timestamp),
style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor),
),
],
),
],
),
),
),
if (_isExpanded) ...[
Divider(height: 1, color: AppColors.greyInfoTextColor.withValues(alpha: 0.2)),
// Tabs
Container(
decoration: BoxDecoration(
color: AppColors.greyInfoTextColor.withValues(alpha: 0.05),
),
child: Row(
children: [
_buildTab(LocaleKeys.request.tr(context: context), 0),
_buildTab(LocaleKeys.response.tr(context: context), 1),
_buildTab(LocaleKeys.headers.tr(context: context), 2),
],
),
),
// Tab Content
Container(
padding: EdgeInsets.all(16.w),
constraints: BoxConstraints(maxHeight: 300.h),
child: SingleChildScrollView(
child: _buildTabContent(),
),
),
// Action Buttons
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: AppColors.greyInfoTextColor.withValues(alpha: 0.05),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16.r),
bottomRight: Radius.circular(16.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionIcon(
Icons.content_copy,
LocaleKeys.copyAsText.tr(context: context),
() {
Clipboard.setData(ClipboardData(text: log.toFormattedText()));
Utils.showToast(LocaleKeys.copiedAsText.tr(context: context));
},
),
_buildActionIcon(
Icons.code,
LocaleKeys.copyAsCurl.tr(context: context),
() {
Clipboard.setData(ClipboardData(text: log.toCurl()));
Utils.showToast(LocaleKeys.copiedAsCurl.tr(context: context));
},
),
_buildActionIcon(
Icons.data_object,
LocaleKeys.copyJson.tr(context: context),
() {
Clipboard.setData(ClipboardData(text: log.prettyResponseBody));
Utils.showToast(LocaleKeys.responseJsonCopied.tr(context: context));
},
),
],
),
),
],
],
),
);
}
Widget _buildTab(String title, int index) {
final isSelected = _selectedTabIndex == index;
return Expanded(
child: InkWell(
onTap: () {
setState(() {
_selectedTabIndex = index;
});
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelected ? AppColors.primaryRedColor : Colors.transparent,
width: 2,
),
),
),
child: Center(
child: Text(
title,
style: TextStyle(
fontSize: 14.f,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? AppColors.primaryRedColor : AppColors.greyInfoTextColor,
),
),
),
),
),
);
}
Widget _buildTabContent() {
switch (_selectedTabIndex) {
case 0:
return SelectableText(
widget.log.prettyRequestBody,
style: TextStyle(fontSize: 12.f, fontFamily: 'Courier', color: AppColors.blackBgColor),
);
case 1:
return SelectableText(
widget.log.prettyResponseBody,
style: TextStyle(fontSize: 12.f, fontFamily: 'Courier', color: AppColors.blackBgColor),
);
case 2:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.log.headers.entries.map((entry) {
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${entry.key}: ', style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.bold, color: AppColors.blackBgColor)),
Expanded(
child: SelectableText(
entry.value,
style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor),
),
),
],
),
);
}).toList(),
);
default:
return const SizedBox.shrink();
}
}
Widget _buildActionIcon(IconData icon, String label, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8.r),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
child: Column(
children: [
Icon(icon, size: 20.w, color: AppColors.primaryRedColor),
SizedBox(height: 4.h),
Text(label, style: TextStyle(fontSize: 10.f, color: AppColors.greyInfoTextColor)),
],
),
),
);
}
Color _getMethodColor(String method) {
switch (method.toUpperCase()) {
case 'GET':
return Colors.blue;
case 'POST':
return Colors.green;
case 'PUT':
return Colors.orange;
case 'DELETE':
return Colors.red;
case 'PATCH':
return Colors.purple;
default:
return AppColors.greyInfoTextColor;
}
}
}

@ -0,0 +1,336 @@
import 'package:amazon_payfort/amazon_payfort.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/dev_tools/environment_config_service.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
class EnvironmentConfigSheet extends StatefulWidget {
const EnvironmentConfigSheet({super.key});
@override
State<EnvironmentConfigSheet> createState() => _EnvironmentConfigSheetState();
}
class _EnvironmentConfigSheetState extends State<EnvironmentConfigSheet> {
AppEnvironmentPreset? _selectedPreset;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCurrentEnvironment();
}
Future<void> _loadCurrentEnvironment() async {
final currentPreset = EnvironmentConfigService.instance.getCurrentPreset();
setState(() {
_selectedPreset = currentPreset;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.scaffoldBgColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24.r),
topRight: Radius.circular(24.r),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
Container(
margin: EdgeInsets.only(top: 12.h, bottom: 8.h),
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: AppColors.greyInfoTextColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2.r),
),
),
// Title
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h),
child: Row(
children: [
Expanded(
child: Text(
LocaleKeys.environmentConfig.tr(context: context),
style: TextStyle(fontSize: 20.f, fontWeight: FontWeight.bold),
),
),
IconButton(
icon: Icon(Icons.close, color: AppColors.blackBgColor),
onPressed: () => Navigator.pop(context, false),
),
],
),
),
Divider(height: 1, color: AppColors.greyInfoTextColor.withValues(alpha: 0.2)),
if (_isLoading)
Padding(
padding: EdgeInsets.all(40.h),
child: const CircularProgressIndicator(),
)
else
SingleChildScrollView(
padding: EdgeInsets.all(24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Current Environment Info
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: AppColors.infoColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: AppColors.infoColor.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(Icons.info_outline, color: AppColors.infoColor, size: 24.w),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
LocaleKeys.currentEnvironment.tr(context: context),
style: TextStyle(fontSize: 14.f, fontWeight: FontWeight.bold, color: AppColors.infoColor),
),
SizedBox(height: 4.h),
Text(
_selectedPreset?.label ?? LocaleKeys.production.tr(context: context),
style: TextStyle(fontSize: 12.f, color: AppColors.blackBgColor),
),
],
),
),
],
),
),
SizedBox(height: 24.h),
Text(LocaleKeys.selectEnvironment.tr(context: context), style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.bold)),
SizedBox(height: 12.h),
// Environment Chips
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: kEnvironmentPresets.map((preset) {
final isSelected = _selectedPreset?.label == preset.label;
return InkWell(
onTap: () {
setState(() {
_selectedPreset = preset;
});
},
borderRadius: BorderRadius.circular(20.r),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor : AppColors.whiteColor,
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyInfoTextColor.withValues(alpha: 0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isSelected)
Icon(
Icons.check_circle,
size: 18.w,
color: Colors.white,
),
if (isSelected) SizedBox(width: 6.w),
Text(
preset.label,
style: TextStyle(
fontSize: 14.f,
color: isSelected ? Colors.white : AppColors.blackBgColor,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}).toList(),
),
SizedBox(height: 24.h),
// Environment Details
if (_selectedPreset != null) ...[
Text(LocaleKeys.environmentDetails.tr(context: context), style: TextStyle(fontSize: 16.f, fontWeight: FontWeight.bold)),
SizedBox(height: 12.h),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: AppColors.greyInfoTextColor.withValues(alpha: 0.2),
),
),
child: Column(
children: [
_buildDetailRow(LocaleKeys.baseUrl.tr(context: context), _selectedPreset!.baseUrl),
Divider(height: 20.h),
_buildDetailRow(LocaleKeys.rcUrl.tr(context: context), _selectedPreset!.rcBaseUrl),
Divider(height: 20.h),
_buildDetailRow(LocaleKeys.pharmacyApi.tr(context: context), _selectedPreset!.hmgPharmacyApiBaseUrl),
Divider(height: 20.h),
_buildDetailRow(
LocaleKeys.payfortMode.tr(context: context),
_selectedPreset!.payFortEnvironment == FortEnvironment.production
? LocaleKeys.production.tr(context: context)
: LocaleKeys.sandbox.tr(context: context)),
Divider(height: 20.h),
_buildDetailRow(LocaleKeys.applePayMerchant.tr(context: context), _selectedPreset!.applePayMerchantId),
],
),
),
],
SizedBox(height: 24.h),
// Warning
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: AppColors.warningColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: AppColors.warningColor.withValues(alpha: 0.3),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.warning_amber_rounded, color: AppColors.warningColor, size: 24.w),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(LocaleKeys.important.tr(context: context),
style: TextStyle(fontSize: 14.f, fontWeight: FontWeight.bold, color: AppColors.warningColor)),
SizedBox(height: 4.h),
Text(
LocaleKeys.envChangeWarning.tr(context: context),
style: TextStyle(fontSize: 12.f, color: AppColors.blackBgColor),
),
],
),
),
],
),
),
SizedBox(height: 24.h),
// Action Buttons
Row(
children: [
Expanded(
child: CustomButton(
height: 50.h,
text: LocaleKeys.resetToDefault.tr(context: context),
icon: AppAssets.cancel,
iconColor: AppColors.errorColor,
iconSize: 20.w,
textColor: AppColors.errorColor,
borderColor: AppColors.errorColor.withValues(alpha: 0.3),
backgroundColor: AppColors.errorColor.withValues(alpha: 0.1),
onPressed: () => _showResetConfirmation(),
),
),
SizedBox(width: 12.w),
Expanded(
child: CustomButton(
height: 50.h,
text: LocaleKeys.saveAndRestart.tr(context: context),
icon: AppAssets.confirm,
iconSize: 20.w,
onPressed: _selectedPreset != null ? _saveAndRestart : null,
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100.w,
child: Text(label, style: TextStyle(fontSize: 12.f, color: AppColors.greyInfoTextColor)),
),
Expanded(
child: Text(value, style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.bold), maxLines: 3),
),
],
);
}
void _showResetConfirmation() {
showCommonBottomSheetWithoutHeight(
context,
title: LocaleKeys.resetEnvironment.tr(context: context),
child: Utils.getWarningWidget(
loadingText: LocaleKeys.resetEnvConfirm.tr(context: context),
isShowActionButtons: true,
onCancelTap: () => Navigator.pop(context),
onConfirmTap: () async {
Navigator.pop(context); // Close confirmation dialog
await EnvironmentConfigService.instance.resetToDefault();
Navigator.pop(context, true); // Close config sheet and trigger restart
Utils.showToast(LocaleKeys.envResetSuccess.tr(context: context));
},
),
callBackFunc: () {},
isFullScreen: false,
);
}
Future<void> _saveAndRestart() async {
if (_selectedPreset == null) return;
try {
await EnvironmentConfigService.instance.applyPreset(_selectedPreset!);
Navigator.pop(context, true); // Return true to indicate app should restart
Utils.showToast(LocaleKeys.envUpdateSuccess.tr(context: context));
} catch (e) {
Utils.showToast('${LocaleKeys.envUpdateFailed.tr(context: context)}: $e');
}
}
}

@ -22,11 +22,14 @@ import 'package:hmg_patient_app_new/features/medical_file/models/family_file_res
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart';
import 'package:hmg_patient_app_new/presentation/dev_tools/api_log_screen.dart';
import 'package:hmg_patient_app_new/presentation/dev_tools/environment_config_sheet.dart';
import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/accessibility_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/accessibility_page.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/profile_picture_widget.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/profile_picture_widget.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_email_widget.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_email_widget.dart';
import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_emergency_contact_widget.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_emergency_contact_widget.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/app_language_change.dart'; import 'package:hmg_patient_app_new/widgets/app_language_change.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
@ -310,6 +313,26 @@ class ProfileSettingsState extends State<ProfileSettings> {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Column( child: Column(
children: [ children: [
// DevTools Menu Items (only shown if enabled)
if (ApiConsts.enableDevTools) ...[
actionItem(AppAssets.document, LocaleKeys.apiLogs.tr(context: context), () {
Navigator.of(context).push(CustomPageRoute(page: const ApiLogScreen()));
}),
1.divider,
actionItem(AppAssets.ic_filters, LocaleKeys.environmentConfig.tr(context: context), () async {
final shouldRestart = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const EnvironmentConfigSheet(),
);
if (shouldRestart == true && mounted) {
// Restart app by navigating to splash screen
Navigator.pushNamedAndRemoveUntil(context, AppRoutes.initialRoute, (route) => false);
}
}),
1.divider,
],
actionItem(AppAssets.call_fill, LocaleKeys.contactUs.tr(context: context), () { actionItem(AppAssets.call_fill, LocaleKeys.contactUs.tr(context: context), () {
launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); launchUrl(Uri.parse("tel://" + "+966 92 006 6666"));
}, trailingLabel: "92 006 6666", isEnglishOnly: true), }, trailingLabel: "92 006 6666", isEnglishOnly: true),

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
@ -15,14 +16,12 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.light, brightness: Brightness.light,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: { builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[400], hintColor: Colors.grey[400],
disabledColor: Colors.grey[300], disabledColor: Colors.grey[300],
textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey), textSelectionTheme: const TextSelectionThemeData(
cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey),
canvasColor: Colors.white, canvasColor: Colors.white,
highlightColor: Colors.grey[100]!.withOpacity(0.4), highlightColor: Colors.grey[100]!.withOpacity(0.4),
splashColor: Colors.transparent, splashColor: Colors.transparent,
@ -51,20 +50,17 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.dark, brightness: Brightness.dark,
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: { builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
), ),
hintColor: Colors.grey[600], hintColor: Colors.grey[600],
disabledColor: Colors.grey[700], disabledColor: Colors.grey[700],
textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey), textSelectionTheme: const TextSelectionThemeData(
cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey),
canvasColor: AppColors.dark.scaffoldBgColor, canvasColor: AppColors.dark.scaffoldBgColor,
scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor,
highlightColor: Colors.grey[800]!.withOpacity(0.4), highlightColor: Colors.grey[800]!.withOpacity(0.4),
splashColor: Colors.transparent, splashColor: Colors.transparent,
bottomSheetTheme: BottomSheetThemeData( bottomSheetTheme: BottomSheetThemeData(backgroundColor: Colors.black.withOpacity(0)),
backgroundColor: Colors.black.withOpacity(0)),
floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
color: AppColors.dark.scaffoldBgColor, color: AppColors.dark.scaffoldBgColor,

@ -71,8 +71,8 @@ dependencies:
firebase_analytics: ^11.5.1 firebase_analytics: ^11.5.1
firebase_crashlytics: ^4.3.8 firebase_crashlytics: ^4.3.8
jiffy: ^6.4.3 jiffy: ^6.4.3
# hijri_gregorian_calendar: # hijri_gregorian_calendar:
# path: /Users/aamir/StudioProjects/hijri_gregorian_calendar # path: /Users/aamir/StudioProjects/hijri_gregorian_calendar
sms_otp_auto_verify: ^2.2.0 sms_otp_auto_verify: ^2.2.0
web: any web: any
flutter_staggered_animations: ^1.1.1 flutter_staggered_animations: ^1.1.1
@ -89,7 +89,7 @@ dependencies:
path: package/flutter_swiper_view path: package/flutter_swiper_view
flutter_callkit_incoming: 3.0.0 flutter_callkit_incoming: 3.0.0
in_app_update: ^4.2.5 in_app_update: ^4.2.5
# device_preview: ^1.3.1 # device_preview: ^1.3.1
image_picker_android: ^0.8.13+16 image_picker_android: ^0.8.13+16
image_picker_platform_interface: ^2.10.0 image_picker_platform_interface: ^2.10.0

Loading…
Cancel
Save