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