From 9587cd883560fd8558da468425b58e65b1c6fe03 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Fri, 31 Oct 2025 18:09:34 -0600 Subject: [PATCH 1/7] feat: improve countdown timer with order_expires_at and dynamic scaling in pending orders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use exact timestamps from order_expires_at tag for precise calculations - Add dynamic day/hour scaling: 14d 20h 06m (>24h) or HH:MM:SS (≤24h) - Refactor: Create shared DynamicCountdownWidget to remove duplication - Apply consistent behavior across TakeOrderScreen and TradeDetailScreen --- lib/data/models/nostr_event.dart | 1 + lib/data/models/order.dart | 4 +- .../order/screens/take_order_screen.dart | 58 ++++--------- .../trades/screens/trade_detail_screen.dart | 41 +++------ .../widgets/dynamic_countdown_widget.dart | 87 +++++++++++++++++++ 5 files changed, 116 insertions(+), 75 deletions(-) create mode 100644 lib/shared/widgets/dynamic_countdown_widget.dart diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index ffa8e57d..0a742874 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -39,6 +39,7 @@ extension NostrEventExtensions on NostrEvent { String? timeAgoWithLocale(String? locale) => _timeAgo(_getTagValue('expiration'), locale); DateTime get expirationDate => _getTimeStamp(_getTagValue('expiration')!); + String? get orderExpiresAt => _getTagValue('order_expires_at'); String? get platform => _getTagValue('y'); String get type => _getTagValue('z')!; diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index d0a75304..0c33f95c 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -192,7 +192,9 @@ class Order implements Payload { paymentMethod: event.paymentMethods.join(','), premium: event.premium as int, createdAt: event.createdAt as int, - expiresAt: event.expiration as int?, + expiresAt: event.orderExpiresAt != null + ? int.parse(event.orderExpiresAt!) + : null, ); } diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 8ae04279..a259e3ba 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -1,4 +1,3 @@ -import 'package:circular_countdown/circular_countdown.dart'; import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -18,8 +17,8 @@ import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; -import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/providers/time_provider.dart'; +import 'package:mostro_mobile/shared/widgets/dynamic_countdown_widget.dart'; import 'package:mostro_mobile/generated/l10n.dart'; class TakeOrderScreen extends ConsumerStatefulWidget { @@ -88,7 +87,7 @@ class _TakeOrderScreenState extends ConsumerState { _buildCreatorReputation(order), const SizedBox(height: 24), _CountdownWidget( - expirationDate: order.expirationDate, + order: order, ), const SizedBox(height: 36), _buildActionButtons(context, ref, order), @@ -443,10 +442,10 @@ class _TakeOrderScreenState extends ConsumerState { /// Widget that displays a real-time countdown timer for pending orders class _CountdownWidget extends ConsumerWidget { - final DateTime expirationDate; + final NostrEvent order; const _CountdownWidget({ - required this.expirationDate, + required this.order, }); @override @@ -456,7 +455,7 @@ class _CountdownWidget extends ConsumerWidget { return timeAsync.when( data: (currentTime) { - return _buildCountDownTime(context, ref, expirationDate); + return _buildCountDownTime(context, ref, order); }, loading: () => const CircularProgressIndicator(), error: (error, stack) => const SizedBox.shrink(), @@ -464,47 +463,20 @@ class _CountdownWidget extends ConsumerWidget { } Widget _buildCountDownTime( - BuildContext context, WidgetRef ref, DateTime expiration) { - Duration countdown = Duration(hours: 0); - final now = DateTime.now(); - - // Handle edge case: expiration in the past - if (expiration.isBefore(now.subtract(const Duration(hours: 1)))) { - // If expiration is more than 1 hour in the past, likely invalid - return const SizedBox.shrink(); - } - - if (expiration.isAfter(now)) { - countdown = expiration.difference(now); - } - - // Get dynamic expiration hours from Mostro instance - final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; - final maxOrderHours = - mostroInstance?.expirationHours ?? 24; // fallback to 24 hours - - // Validate expiration hours - if (maxOrderHours <= 0 || maxOrderHours > 168) { - // Max 1 week + BuildContext context, WidgetRef ref, NostrEvent order) { + // Use exact timestamps from order_expires_at + if (order.orderExpiresAt == null) { + // No valid expiration timestamp available return const SizedBox.shrink(); } - final hoursLeft = countdown.inHours.clamp(0, maxOrderHours); - final minutesLeft = countdown.inMinutes % 60; - final secondsLeft = countdown.inSeconds % 60; - - final formattedTime = - '${hoursLeft.toString().padLeft(2, '0')}:${minutesLeft.toString().padLeft(2, '0')}:${secondsLeft.toString().padLeft(2, '0')}'; + final expiresAtTimestamp = int.parse(order.orderExpiresAt!); + final expiration = DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp * 1000); + final createdAt = order.createdAt!; - return Column( - children: [ - CircularCountdown( - countdownTotal: maxOrderHours, - countdownRemaining: hoursLeft, - ), - const SizedBox(height: 16), - Text(S.of(context)!.timeLeftLabel(formattedTime)), - ], + return DynamicCountdownWidget( + expiration: expiration, + createdAt: createdAt, ); } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 668f1fb8..5a393358 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -23,6 +23,7 @@ import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/shared/providers/time_provider.dart'; +import 'package:mostro_mobile/shared/widgets/dynamic_countdown_widget.dart'; import 'package:mostro_mobile/features/disputes/providers/dispute_providers.dart'; import 'package:mostro_mobile/generated/l10n.dart'; @@ -978,19 +979,14 @@ class _CountdownWidget extends ConsumerWidget { // Show countdown ONLY for these 3 specific statuses if (status == Status.pending) { - // Pending orders: use expirationHours - final expHours = - mostroInstance?.expirationHours ?? 24; // 24 hours fallback - final countdownDuration = Duration(hours: expHours); - - // Handle edge case: invalid timestamp - if (expiresAtTimestamp != null && expiresAtTimestamp <= 0) { - expiresAtTimestamp = null; + // Pending orders: use exact timestamps from order_expires_at + if (expiresAtTimestamp == null || expiresAtTimestamp <= 0) { + // No valid expiration timestamp available + return null; } - final expiration = expiresAtTimestamp != null - ? DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp) - : now.add(countdownDuration); + final expiration = DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp); + final createdAt = DateTime.fromMillisecondsSinceEpoch((tradeState.order?.createdAt ?? 0) * 1000); // Handle edge case: expiration in the past if (expiration.isBefore(now.subtract(const Duration(hours: 1)))) { @@ -998,26 +994,9 @@ class _CountdownWidget extends ConsumerWidget { return null; } - final Duration difference = expiration.isAfter(now) - ? expiration.difference(now) - : const Duration(); - - final hoursLeft = difference.inHours.clamp(0, expHours); - final minutesLeft = difference.inMinutes % 60; - final secondsLeft = difference.inSeconds % 60; - - final formattedTime = - '${hoursLeft.toString().padLeft(2, '0')}:${minutesLeft.toString().padLeft(2, '0')}:${secondsLeft.toString().padLeft(2, '0')}'; - - return Column( - children: [ - CircularCountdown( - countdownTotal: expHours, - countdownRemaining: hoursLeft, - ), - const SizedBox(height: 16), - Text(S.of(context)!.timeLeftLabel(formattedTime)), - ], + return DynamicCountdownWidget( + expiration: expiration, + createdAt: createdAt, ); } else if (status == Status.waitingBuyerInvoice || status == Status.waitingPayment) { diff --git a/lib/shared/widgets/dynamic_countdown_widget.dart b/lib/shared/widgets/dynamic_countdown_widget.dart new file mode 100644 index 00000000..fe53259d --- /dev/null +++ b/lib/shared/widgets/dynamic_countdown_widget.dart @@ -0,0 +1,87 @@ +import 'package:circular_countdown/circular_countdown.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; + +/// Shared countdown widget for orders in PENDING status only +/// +/// Displays a dynamic circular countdown timer that automatically scales between day/hour modes: +/// - >24 hours remaining: Day scale showing "14d 20h 06m" format +/// - ≤24 hours remaining: Hour scale showing "HH:MM:SS" format +/// +/// Uses exact timestamps from order_expires_at tag for precise calculations. +/// +/// Note: Orders in waiting status (waitingBuyerInvoice, waitingPayment) use +/// a different countdown system based on expirationSeconds + message timestamps. +class DynamicCountdownWidget extends ConsumerWidget { + final DateTime expiration; + final DateTime createdAt; + + const DynamicCountdownWidget({ + super.key, + required this.expiration, + required this.createdAt, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final now = DateTime.now(); + + // Handle edge case: expiration in the past + if (expiration.isBefore(now.subtract(const Duration(hours: 1)))) { + // If expiration is more than 1 hour in the past, likely invalid + return const SizedBox.shrink(); + } + + // Calculate total duration and remaining time + final totalDuration = expiration.difference(createdAt); + final Duration remainingTime = expiration.isAfter(now) + ? expiration.difference(now) + : const Duration(); + + // Determine if we should use day scale (>24 hours remaining) or hour scale (≤24 hours) + final remainingHours = remainingTime.inHours; + final useDayScale = remainingHours > 24; + + if (useDayScale) { + // DAY SCALE: Show days and hours + final totalDays = (totalDuration.inHours / 24).round(); + final daysLeft = (remainingTime.inHours / 24).floor(); + final hoursLeftInDay = remainingTime.inHours % 24; + + final minutesLeftInHour = remainingTime.inMinutes % 60; + final formattedTime = '${daysLeft}d ${hoursLeftInDay}h ${minutesLeftInHour.toString().padLeft(2, '0')}m'; + + return Column( + children: [ + CircularCountdown( + countdownTotal: totalDays, + countdownRemaining: daysLeft, + ), + const SizedBox(height: 16), + Text(S.of(context)!.timeLeftLabel(formattedTime)), + ], + ); + } else { + // HOUR SCALE: Show hours, minutes, seconds (≤24 hours remaining) + final totalHours = (totalDuration.inMinutes / 60).round(); + final hoursLeft = remainingTime.inHours.clamp(0, totalHours); + final minutesLeft = remainingTime.inMinutes % 60; + final secondsLeft = remainingTime.inSeconds % 60; + + final formattedTime = + '${hoursLeft.toString().padLeft(2, '0')}:${minutesLeft.toString().padLeft(2, '0')}:${secondsLeft.toString().padLeft(2, '0')}'; + + return Column( + children: [ + CircularCountdown( + countdownTotal: totalHours, + countdownRemaining: hoursLeft, + ), + const SizedBox(height: 16), + Text(S.of(context)!.timeLeftLabel(formattedTime)), + ], + ); + } + } +} \ No newline at end of file From fef2f11ff3f9898fa7543ad22929e3672e2f49a8 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:00:16 -0600 Subject: [PATCH 2/7] fix: use int.tryParse for order_expires_at parsing and update documentation --- CLAUDE.md | 12 ++++ .../TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md | 71 ++++++++++++++++++- lib/data/models/order.dart | 4 +- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f2e0a075..af08c6f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -245,6 +245,18 @@ When orders are canceled, Mostro sends `Action.canceled` gift wrap: - **Implementation**: Custom `timeAgoWithLocale()` method in NostrEvent extension - **Usage**: Automatically uses app's current locale for "hace X horas" vs "hours ago" +### Dynamic Countdown Timer System +- **DynamicCountdownWidget**: Intelligent countdown widget for pending orders with automatic day/hour scaling +- **Implementation**: Located in `lib/shared/widgets/dynamic_countdown_widget.dart` +- **Data Source**: Uses exact `order_expires_at` timestamps from Mostro protocol for precision +- **Dual Display Modes**: + - **Day Scale** (>24h remaining): Shows "14d 20h 06m" format with day-based circular progress + - **Hour Scale** (≤24h remaining): Shows "HH:MM:SS" format with hour-based circular progress +- **Automatic Transition**: Switches at exactly 24:00:00 remaining time +- **Localization**: Uses `S.of(context)!.timeLeftLabel()` for internationalized display +- **Scope**: Only for pending status orders; waiting orders use separate countdown system +- **Integration**: Shared across TakeOrderScreen and TradeDetailScreen for consistency + ## Relay Synchronization System ### Overview diff --git a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md index aff433be..f0ff0322 100755 --- a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md +++ b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md @@ -306,7 +306,76 @@ final countdownTimeProvider = StreamProvider((ref) { - **Resource efficiency**: Single timer supports multiple subscribers - **Memory leak prevention**: Proper disposal handling -### Countdown UI Integration +### Dynamic Countdown Timer System + +The application now uses a unified `DynamicCountdownWidget` for all pending order countdown timers, providing intelligent scaling and precise timestamp calculations. + +#### **DynamicCountdownWidget Architecture** + +```dart +// lib/shared/widgets/dynamic_countdown_widget.dart +class DynamicCountdownWidget extends ConsumerWidget { + final DateTime expiration; + final DateTime createdAt; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final remainingTime = expiration.isAfter(now) ? expiration.difference(now) : Duration.zero; + final useDayScale = remainingTime.inHours > 24; + + if (useDayScale) { + // Day scale: "14d 20h 06m" format for >24 hours + final daysLeft = (remainingTime.inHours / 24).floor(); + final hoursLeftInDay = remainingTime.inHours % 24; + final minutesLeftInHour = remainingTime.inMinutes % 60; + return CircularCountdown(countdownTotal: totalDays, countdownRemaining: daysLeft); + } else { + // Hour scale: "HH:MM:SS" format for ≤24 hours + final hoursLeft = remainingTime.inHours.clamp(0, totalHours); + final minutesLeft = remainingTime.inMinutes % 60; + final secondsLeft = remainingTime.inSeconds % 60; + return CircularCountdown(countdownTotal: totalHours, countdownRemaining: hoursLeft); + } + } +} +``` + +#### **Key Features** + +1. **Automatic Scaling**: Switches between day/hour formats based on remaining time +2. **Exact Timestamps**: Uses `order_expires_at` tag for precise calculations +3. **Dynamic Display**: + - **>24 hours**: Day scale with "14d 20h 06m" format + - **≤24 hours**: Hour scale with "HH:MM:SS" format +4. **Intelligent Rounding**: Circle divisions use intelligent rounding (28.2h → 28h, 23.7h → 24h) +5. **Shared Component**: Eliminates 96 lines of duplicated countdown code + +#### **Integration Points** + +**TakeOrderScreen Usage**: +```dart +// lib/features/order/screens/take_order_screen.dart - Lines 477-480 +return DynamicCountdownWidget( + expiration: DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp * 1000), + createdAt: order.createdAt!, +); +``` + +**TradeDetailScreen Usage**: +```dart +// lib/features/trades/screens/trade_detail_screen.dart - Lines 87-93 +_CountdownWidget( + orderId: orderId, + tradeState: tradeState, + expiresAtTimestamp: orderPayload.expiresAt != null ? orderPayload.expiresAt! * 1000 : null, +), +``` + +#### **Scope and Limitations** + +- **Pending Orders Only**: DynamicCountdownWidget is specifically designed for orders in `Status.pending` +- **Waiting Orders Use Different System**: Orders in `Status.waitingBuyerInvoice` and `Status.waitingPayment` use separate countdown logic based on `expirationSeconds` + message timestamps +- **Data Source**: Uses `order_expires_at` Nostr tag for exact expiration timestamps rather than calculated values #### **Real-time Countdown Widget** diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 0c33f95c..1bbabf61 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -192,8 +192,8 @@ class Order implements Payload { paymentMethod: event.paymentMethods.join(','), premium: event.premium as int, createdAt: event.createdAt as int, - expiresAt: event.orderExpiresAt != null - ? int.parse(event.orderExpiresAt!) + expiresAt: event.orderExpiresAt != null + ? int.tryParse(event.orderExpiresAt!) : null, ); } From 18ab5f3c0813f75fafe1be65a2d017ac927ac7f1 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:43:50 -0600 Subject: [PATCH 3/7] fix: safe parsing for order countdown timestamps --- lib/features/order/screens/take_order_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index a259e3ba..f0ef15bf 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -470,8 +470,11 @@ class _CountdownWidget extends ConsumerWidget { return const SizedBox.shrink(); } - final expiresAtTimestamp = int.parse(order.orderExpiresAt!); - final expiration = DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp * 1000); + final expiresAtSeconds = int.tryParse(order.orderExpiresAt!); + if (expiresAtSeconds == null || expiresAtSeconds <= 0) { + return const SizedBox.shrink(); + } + final expiration = DateTime.fromMillisecondsSinceEpoch(expiresAtSeconds * 1000); final createdAt = order.createdAt!; return DynamicCountdownWidget( From b56449af086143946855544a9fde7a599edcc241 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:06:45 -0600 Subject: [PATCH 4/7] fix: prevent countdown crashes with zero totals from short orders --- lib/shared/widgets/dynamic_countdown_widget.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/shared/widgets/dynamic_countdown_widget.dart b/lib/shared/widgets/dynamic_countdown_widget.dart index fe53259d..dc8b91d3 100644 --- a/lib/shared/widgets/dynamic_countdown_widget.dart +++ b/lib/shared/widgets/dynamic_countdown_widget.dart @@ -35,6 +35,12 @@ class DynamicCountdownWidget extends ConsumerWidget { // Calculate total duration and remaining time final totalDuration = expiration.difference(createdAt); + + // Early return if expiration is at or before creation time + if (expiration.isAtSameMomentAs(createdAt) || expiration.isBefore(createdAt)) { + return const SizedBox.shrink(); + } + final Duration remainingTime = expiration.isAfter(now) ? expiration.difference(now) : const Duration(); @@ -45,8 +51,8 @@ class DynamicCountdownWidget extends ConsumerWidget { if (useDayScale) { // DAY SCALE: Show days and hours - final totalDays = (totalDuration.inHours / 24).round(); - final daysLeft = (remainingTime.inHours / 24).floor(); + final totalDays = ((totalDuration.inSeconds + 86399) ~/ 86400).clamp(1, double.infinity).toInt(); + final daysLeft = ((remainingTime.inHours / 24).floor()).clamp(0, totalDays); final hoursLeftInDay = remainingTime.inHours % 24; final minutesLeftInHour = remainingTime.inMinutes % 60; @@ -64,7 +70,7 @@ class DynamicCountdownWidget extends ConsumerWidget { ); } else { // HOUR SCALE: Show hours, minutes, seconds (≤24 hours remaining) - final totalHours = (totalDuration.inMinutes / 60).round(); + final totalHours = ((totalDuration.inSeconds + 3599) ~/ 3600).clamp(1, double.infinity).toInt(); final hoursLeft = remainingTime.inHours.clamp(0, totalHours); final minutesLeft = remainingTime.inMinutes % 60; final secondsLeft = remainingTime.inSeconds % 60; From 71bf5facc8771ab641ad7d07e538eef72363b4f8 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:22:38 -0600 Subject: [PATCH 5/7] update documentation --- CLAUDE.md | 4 ++-- .../TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index af08c6f9..831889af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,14 +125,14 @@ Automatic cleanup system that prevents sessions from becoming orphaned when Most - **Purpose**: Prevents orphan sessions when Mostro doesn't respond within 10 seconds - **Cleanup**: Deletes session, shows localized notification, navigates to order book - **Cancellation**: Timer automatically cancelled when any response received from Mostro -- **Implementation**: `AbstractMostroNotifier.startSessionTimeoutCleanup()` in `abstract_mostro_notifier.dart:286-305` +- **Implementation**: `AbstractMostroNotifier.startSessionTimeoutCleanup()` method in `abstract_mostro_notifier.dart` **Order Creation Protection**: - **Activation**: Started automatically when users create orders (`AddOrderNotifier.submitOrder`) - **Purpose**: Prevents orphan sessions when Mostro doesn't respond to new order creation within 10 seconds - **Cleanup**: Deletes temporary session, shows localized notification, navigates to order book - **Cancellation**: Timer automatically cancelled when any response received from Mostro -- **Implementation**: `AbstractMostroNotifier.startSessionTimeoutCleanupForRequestId()` in `abstract_mostro_notifier.dart` +- **Implementation**: `AbstractMostroNotifier.startSessionTimeoutCleanupForRequestId()` method in `abstract_mostro_notifier.dart` #### **Localized User Feedback** ``` diff --git a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md index f0ff0322..2042512c 100755 --- a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md +++ b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md @@ -354,7 +354,7 @@ class DynamicCountdownWidget extends ConsumerWidget { **TakeOrderScreen Usage**: ```dart -// lib/features/order/screens/take_order_screen.dart - Lines 477-480 +// lib/features/order/screens/take_order_screen.dart - _buildCountDownTime method return DynamicCountdownWidget( expiration: DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp * 1000), createdAt: order.createdAt!, @@ -363,7 +363,7 @@ return DynamicCountdownWidget( **TradeDetailScreen Usage**: ```dart -// lib/features/trades/screens/trade_detail_screen.dart - Lines 87-93 +// lib/features/trades/screens/trade_detail_screen.dart - trade details widget tree _CountdownWidget( orderId: orderId, tradeState: tradeState, @@ -380,7 +380,7 @@ _CountdownWidget( #### **Real-time Countdown Widget** ```dart -// lib/features/trades/screens/trade_detail_screen.dart - Lines 862-1062 +// lib/features/trades/screens/trade_detail_screen.dart - _CountdownWidget class class _CountdownWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { @@ -413,7 +413,7 @@ class _CountdownWidget extends ConsumerWidget { The countdown displays different behaviors based on order status: ```dart -// Lines 916-1023: Countdown logic by order status +// _buildCountDownTime method: Countdown logic by order status Widget? _buildCountDownTime(BuildContext context, WidgetRef ref, OrderState tradeState, List messages, int? expiresAtTimestamp) { @@ -475,7 +475,7 @@ Widget? _buildCountDownTime(BuildContext context, WidgetRef ref, #### **Message State Detection** ```dart -// Lines 1025-1050: Find the message that caused the current state +// _findMessageForState method: Find the message that caused the current state MostroMessage? _findMessageForState(List messages, Status status) { // Sort messages by timestamp (most recent first) final sortedMessages = List.from(messages) @@ -797,7 +797,7 @@ The system automatically starts cleanup timers for both order creation and order When users take orders, a cleanup timer is automatically started to prevent sessions from becoming orphaned if Mostro doesn't respond: ```dart -// lib/features/order/notfiers/abstract_mostro_notifier.dart:286-305 +// lib/features/order/notfiers/abstract_mostro_notifier.dart - startSessionTimeoutCleanup method static void startSessionTimeoutCleanup(String orderId, Ref ref) { // Cancel existing timer if any _sessionTimeouts[orderId]?.cancel(); From 845393d594c9fc60dc6e352f0bc6910c8af815f7 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:51:33 -0600 Subject: [PATCH 6/7] coderabbit suggestion --- lib/features/order/screens/take_order_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index f0ef15bf..08ff50c4 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -475,7 +475,10 @@ class _CountdownWidget extends ConsumerWidget { return const SizedBox.shrink(); } final expiration = DateTime.fromMillisecondsSinceEpoch(expiresAtSeconds * 1000); - final createdAt = order.createdAt!; + final createdAt = order.createdAt; + if (createdAt == null) { + return const SizedBox.shrink(); + } return DynamicCountdownWidget( expiration: expiration, From 5f4ccb6915e1b55bd6b4c5ddf48ecbf8f8256ff2 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:39:28 -0600 Subject: [PATCH 7/7] use expires_at --- docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md | 4 ++-- lib/data/models/nostr_event.dart | 2 +- lib/data/models/order.dart | 4 ++-- lib/features/order/screens/take_order_screen.dart | 6 +++--- lib/features/trades/screens/trade_detail_screen.dart | 2 +- lib/shared/widgets/dynamic_countdown_widget.dart | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md index 2042512c..127ed433 100755 --- a/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md +++ b/docs/architecture/TIMEOUT_DETECTION_AND_SESSION_CLEANUP.md @@ -343,7 +343,7 @@ class DynamicCountdownWidget extends ConsumerWidget { #### **Key Features** 1. **Automatic Scaling**: Switches between day/hour formats based on remaining time -2. **Exact Timestamps**: Uses `order_expires_at` tag for precise calculations +2. **Exact Timestamps**: Uses `expires_at` tag for precise calculations 3. **Dynamic Display**: - **>24 hours**: Day scale with "14d 20h 06m" format - **≤24 hours**: Hour scale with "HH:MM:SS" format @@ -375,7 +375,7 @@ _CountdownWidget( - **Pending Orders Only**: DynamicCountdownWidget is specifically designed for orders in `Status.pending` - **Waiting Orders Use Different System**: Orders in `Status.waitingBuyerInvoice` and `Status.waitingPayment` use separate countdown logic based on `expirationSeconds` + message timestamps -- **Data Source**: Uses `order_expires_at` Nostr tag for exact expiration timestamps rather than calculated values +- **Data Source**: Uses `expires_at` Nostr tag for exact expiration timestamps rather than calculated values #### **Real-time Countdown Widget** diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index 0a742874..e39d5190 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -39,7 +39,7 @@ extension NostrEventExtensions on NostrEvent { String? timeAgoWithLocale(String? locale) => _timeAgo(_getTagValue('expiration'), locale); DateTime get expirationDate => _getTimeStamp(_getTagValue('expiration')!); - String? get orderExpiresAt => _getTagValue('order_expires_at'); + String? get expiresAt => _getTagValue('expires_at'); String? get platform => _getTagValue('y'); String get type => _getTagValue('z')!; diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 1bbabf61..8e086a52 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -192,8 +192,8 @@ class Order implements Payload { paymentMethod: event.paymentMethods.join(','), premium: event.premium as int, createdAt: event.createdAt as int, - expiresAt: event.orderExpiresAt != null - ? int.tryParse(event.orderExpiresAt!) + expiresAt: event.expiresAt != null + ? int.tryParse(event.expiresAt!) : null, ); } diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 08ff50c4..065bd6ce 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -464,13 +464,13 @@ class _CountdownWidget extends ConsumerWidget { Widget _buildCountDownTime( BuildContext context, WidgetRef ref, NostrEvent order) { - // Use exact timestamps from order_expires_at - if (order.orderExpiresAt == null) { + // Use exact timestamps from expires_at + if (order.expiresAt == null) { // No valid expiration timestamp available return const SizedBox.shrink(); } - final expiresAtSeconds = int.tryParse(order.orderExpiresAt!); + final expiresAtSeconds = int.tryParse(order.expiresAt.toString()); if (expiresAtSeconds == null || expiresAtSeconds <= 0) { return const SizedBox.shrink(); } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 5a393358..67394bd2 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -979,7 +979,7 @@ class _CountdownWidget extends ConsumerWidget { // Show countdown ONLY for these 3 specific statuses if (status == Status.pending) { - // Pending orders: use exact timestamps from order_expires_at + // Pending orders: use exact timestamps from expires_at if (expiresAtTimestamp == null || expiresAtTimestamp <= 0) { // No valid expiration timestamp available return null; diff --git a/lib/shared/widgets/dynamic_countdown_widget.dart b/lib/shared/widgets/dynamic_countdown_widget.dart index dc8b91d3..4d762af6 100644 --- a/lib/shared/widgets/dynamic_countdown_widget.dart +++ b/lib/shared/widgets/dynamic_countdown_widget.dart @@ -9,7 +9,7 @@ import 'package:mostro_mobile/generated/l10n.dart'; /// - >24 hours remaining: Day scale showing "14d 20h 06m" format /// - ≤24 hours remaining: Hour scale showing "HH:MM:SS" format /// -/// Uses exact timestamps from order_expires_at tag for precise calculations. +/// Uses exact timestamps from expires_at tag for precise calculations. /// /// Note: Orders in waiting status (waitingBuyerInvoice, waitingPayment) use /// a different countdown system based on expirationSeconds + message timestamps.