Added dev tools feature
parent
e44856167f
commit
17c45e1656
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue