Added quote expiry-aware flows with auto-refresh

This commit is contained in:
Arseni
2025-12-29 18:38:21 +03:00
parent 4aeb06fd31
commit f3ad4c2d4f
14 changed files with 610 additions and 64 deletions

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
class QuoteStatusActions extends StatelessWidget {
final bool isLoading;
final bool canRefresh;
final bool autoRefreshEnabled;
final ValueChanged<bool>? onToggleAutoRefresh;
final VoidCallback? onRefresh;
final String autoRefreshLabel;
final String refreshLabel;
final AppDimensions dimensions;
final TextTheme theme;
const QuoteStatusActions({
super.key,
required this.isLoading,
required this.canRefresh,
required this.autoRefreshEnabled,
required this.onToggleAutoRefresh,
required this.onRefresh,
required this.autoRefreshLabel,
required this.refreshLabel,
required this.dimensions,
required this.theme,
});
@override
Widget build(BuildContext context) => Row(
children: [
Expanded(
child: SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
value: autoRefreshEnabled,
title: Text(autoRefreshLabel, style: theme.bodyMedium),
onChanged: onToggleAutoRefresh,
),
),
TextButton.icon(
onPressed: canRefresh ? onRefresh : null,
icon: isLoading
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
label: Text(refreshLabel),
),
],
);
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
class QuoteStatusMessage extends StatelessWidget {
final String statusText;
final String? errorText;
final bool showDetails;
final VoidCallback onToggleDetails;
final AppDimensions dimensions;
final TextTheme theme;
final String showLabel;
final String hideLabel;
const QuoteStatusMessage({
super.key,
required this.statusText,
required this.errorText,
required this.showDetails,
required this.onToggleDetails,
required this.dimensions,
required this.theme,
required this.showLabel,
required this.hideLabel,
});
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(statusText, style: theme.bodyMedium),
if (errorText != null) ...[
SizedBox(height: dimensions.paddingSmall),
TextButton(
onPressed: onToggleDetails,
child: Text(showDetails ? hideLabel : showLabel),
),
if (showDetails) ...[
SizedBox(height: dimensions.paddingSmall),
Text(
errorText!,
style: theme.bodySmall,
),
],
],
],
);
}

View File

@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/pages/payment_methods/payment_page/quote/actions.dart';
import 'package:pweb/pages/payment_methods/payment_page/quote/message.dart';
import 'package:pweb/utils/dimensions.dart';
class QuoteStatus extends StatefulWidget {
const QuoteStatus({super.key});
@override
State<QuoteStatus> createState() => _QuoteStatusState();
}
class _QuoteStatusState extends State<QuoteStatus> {
bool _showDetails = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
final loc = AppLocalizations.of(context)!;
return Consumer<QuotationProvider>(
builder: (context, provider, _) {
final statusText = _statusText(provider, loc);
final canRefresh = provider.canRequestQuote && !provider.isLoading;
final refreshLabel = provider.isLoading ? loc.quoteUpdating : loc.retry;
final error = provider.error;
final backgroundColor = theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3);
return Container(
width: double.infinity,
padding: EdgeInsets.all(dimensions.paddingMedium),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuoteStatusMessage(
statusText: statusText,
errorText: error?.toString(),
showDetails: _showDetails,
onToggleDetails: () => setState(() => _showDetails = !_showDetails),
dimensions: dimensions,
theme: theme.textTheme,
showLabel: loc.showDetails,
hideLabel: loc.hideDetails,
),
SizedBox(height: dimensions.paddingSmall),
QuoteStatusActions(
isLoading: provider.isLoading,
canRefresh: canRefresh,
autoRefreshEnabled: provider.autoRefreshEnabled,
onToggleAutoRefresh: provider.canRequestQuote
? (value) => context.read<QuotationProvider>().setAutoRefresh(value)
: null,
onRefresh: canRefresh ? () => context.read<QuotationProvider>().refreshNow() : null,
autoRefreshLabel: loc.quoteAutoRefresh,
refreshLabel: refreshLabel,
dimensions: dimensions,
theme: theme.textTheme,
),
],
),
);
},
);
}
String _statusText(QuotationProvider provider, AppLocalizations loc) {
if (provider.error != null) {
return loc.quoteErrorGeneric;
}
if (!provider.canRequestQuote) {
return loc.quoteUnavailable;
}
if (provider.isLoading) {
return loc.quoteUpdating;
}
if (provider.hasLiveQuote) {
final remaining = provider.timeToExpire;
if (remaining != null) {
return loc.quoteExpiresIn(_formatDuration(remaining));
}
}
if (provider.hasQuoteForCurrentIntent) {
return loc.quoteExpired;
}
return loc.quoteUnavailable;
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
if (duration.inHours > 0) {
final hours = duration.inHours;
final mins = minutes.remainder(60).toString().padLeft(2, '0');
return '$hours:$mins:$seconds';
}
return '$minutes:$seconds';
}
}