From 44f7ae0216e264007a1de59425dca32f1df8cbc3 Mon Sep 17 00:00:00 2001 From: Kemalau <2024302111194@whu.edu.cn> Date: Mon, 16 Mar 2026 18:40:37 +0000 Subject: [PATCH] fix(notifications): clarify timeout cancellation message --- .../utils/notification_data_extractor.dart | 11 +- .../utils/notification_message_mapper.dart | 4 +- lib/l10n/intl_en.arb | 1 + lib/l10n/intl_es.arb | 1 + lib/l10n/intl_fr.arb | 1 + lib/l10n/intl_it.arb | 1 + .../notification_message_mapper_test.dart | 104 ++++++++++++++++++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 test/features/notifications/notification_message_mapper_test.dart diff --git a/lib/features/notifications/utils/notification_data_extractor.dart b/lib/features/notifications/utils/notification_data_extractor.dart index 34278eee8..250334b34 100644 --- a/lib/features/notifications/utils/notification_data_extractor.dart +++ b/lib/features/notifications/utils/notification_data_extractor.dart @@ -148,8 +148,15 @@ class NotificationDataExtractor { break; case Action.canceled: - // Canceled orders don't generate persistent notifications - return null; + final order = event.getPayload(); + final currentSession = session; + + // Maker-side expirations arrive as canceled events for a pending order with no peer. + // Preserve that context so the notification copy can explain the timeout clearly. + if (order?.status == Status.pending && currentSession?.peer == null) { + values['cancellation_reason'] = 'peer_timeout'; + } + break; case Action.cooperativeCancelInitiatedByYou: // No additional values needed diff --git a/lib/features/notifications/utils/notification_message_mapper.dart b/lib/features/notifications/utils/notification_message_mapper.dart index 26e5f22ef..c72a01e13 100644 --- a/lib/features/notifications/utils/notification_message_mapper.dart +++ b/lib/features/notifications/utils/notification_message_mapper.dart @@ -314,6 +314,8 @@ class NotificationMessageMapper { return s.notification_order_canceled_title; case 'notification_order_canceled_message': return s.notification_order_canceled_message; + case 'notification_order_canceled_peer_timeout_message': + return s.notification_order_canceled_peer_timeout_message; case 'notification_cooperative_cancel_initiated_by_you_title': return s.notification_cooperative_cancel_initiated_by_you_title; case 'notification_cooperative_cancel_initiated_by_you_message': @@ -360,4 +362,4 @@ class NotificationMessageMapper { return _resolveLocalizationKey(localizations, messageKey); } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 8e4307d21..430c538de 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1130,6 +1130,7 @@ "notification_dispute_started_message": "A dispute has been initiated", "notification_order_canceled_title": "Order canceled", "notification_order_canceled_message": "The order has been canceled", + "notification_order_canceled_peer_timeout_message": "The counterparty did not respond in time. The order has been canceled.", "notification_cooperative_cancel_initiated_by_you_title": "Cancellation requested", "notification_cooperative_cancel_initiated_by_you_message": "You requested to cancel the order, waiting for peer confirmation", "notification_cooperative_cancel_initiated_by_peer_title": "Cancellation request", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 3183296a7..6d24510af 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1044,6 +1044,7 @@ "notification_dispute_started_message": "Se ha iniciado una disputa", "notification_order_canceled_title": "Orden cancelada", "notification_order_canceled_message": "La orden ha sido cancelada", + "notification_order_canceled_peer_timeout_message": "La contraparte no respondió a tiempo. La orden ha sido cancelada.", "notification_cooperative_cancel_initiated_by_you_title": "Cancelación solicitada", "notification_cooperative_cancel_initiated_by_you_message": "Solicitaste cancelar la orden, esperando confirmación del par", "notification_cooperative_cancel_initiated_by_peer_title": "Solicitud de cancelación", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 6886da7a8..804466c2e 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1130,6 +1130,7 @@ "notification_dispute_started_message": "Un différend a été initié", "notification_order_canceled_title": "Commande annulée", "notification_order_canceled_message": "La commande a été annulée", + "notification_order_canceled_peer_timeout_message": "La contrepartie n'a pas répondu à temps. La commande a été annulée.", "notification_cooperative_cancel_initiated_by_you_title": "Annulation demandée", "notification_cooperative_cancel_initiated_by_you_message": "Vous avez demandé d'annuler la commande, en attente de confirmation du pair", "notification_cooperative_cancel_initiated_by_peer_title": "Demande d'annulation", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index b4d24ff83..4698fee60 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1103,6 +1103,7 @@ "notification_dispute_started_message": "È stata avviata una disputa", "notification_order_canceled_title": "Ordine annullato", "notification_order_canceled_message": "L'ordine è stato annullato", + "notification_order_canceled_peer_timeout_message": "La controparte non ha risposto in tempo. L'ordine è stato annullato.", "notification_cooperative_cancel_initiated_by_you_title": "Cancellazione richiesta", "notification_cooperative_cancel_initiated_by_you_message": "Hai richiesto di annullare l'ordine, in attesa della conferma del peer", "notification_cooperative_cancel_initiated_by_peer_title": "Richiesta di cancellazione", diff --git a/test/features/notifications/notification_message_mapper_test.dart b/test/features/notifications/notification_message_mapper_test.dart new file mode 100644 index 000000000..f6432934a --- /dev/null +++ b/test/features/notifications/notification_message_mapper_test.dart @@ -0,0 +1,104 @@ +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro_mobile/data/models.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/enums/role.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/features/notifications/utils/notification_data_extractor.dart'; +import 'package:mostro_mobile/features/notifications/utils/notification_message_mapper.dart'; + +void main() { + group('NotificationMessageMapper', () { + test('returns timeout-specific message key for expired counterparty cancellations', () { + final key = NotificationMessageMapper.getMessageKeyWithContext( + Action.canceled, + const {'cancellation_reason': 'peer_timeout'}, + ); + + expect(key, 'notification_order_canceled_peer_timeout_message'); + }); + + test('keeps generic cancellation copy for other canceled orders', () { + final key = NotificationMessageMapper.getMessageKeyWithContext( + Action.canceled, + const {}, + ); + + expect(key, 'notification_order_canceled_message'); + }); + }); + + group('NotificationDataExtractor', () { + test('tags maker-side timeout cancellations with peer_timeout reason', () async { + final message = MostroMessage( + id: 'order-123', + action: Action.canceled, + payload: const Order( + id: 'order-123', + kind: OrderType.sell, + status: Status.pending, + fiatCode: 'USD', + fiatAmount: 100, + paymentMethod: 'Cash', + ), + ); + + final session = Session( + masterKey: NostrKeyPairs(private: '1' * 64, public: '2' * 64), + tradeKey: NostrKeyPairs(private: '3' * 64, public: '4' * 64), + keyIndex: 0, + fullPrivacy: false, + startTime: DateTime.utc(2026, 1, 1), + orderId: 'order-123', + role: Role.seller, + ); + + final notification = await NotificationDataExtractor.extractFromMostroMessage( + message, + null, + session: session, + ); + + expect(notification, isNotNull); + expect(notification!.action, Action.canceled); + expect(notification.values['cancellation_reason'], 'peer_timeout'); + }); + + test('does not tag cancellations after a peer was established', () async { + final message = MostroMessage( + id: 'order-123', + action: Action.canceled, + payload: const Order( + id: 'order-123', + kind: OrderType.sell, + status: Status.pending, + fiatCode: 'USD', + fiatAmount: 100, + paymentMethod: 'Cash', + buyerTradePubkey: '5' * 64, + ), + ); + + final session = Session( + masterKey: NostrKeyPairs(private: '1' * 64, public: '2' * 64), + tradeKey: NostrKeyPairs(private: '3' * 64, public: '4' * 64), + keyIndex: 0, + fullPrivacy: false, + startTime: DateTime.utc(2026, 1, 1), + orderId: 'order-123', + role: Role.seller, + peer: Peer(publicKey: '5' * 64), + ); + + final notification = await NotificationDataExtractor.extractFromMostroMessage( + message, + null, + session: session, + ); + + expect(notification, isNotNull); + expect(notification!.values.containsKey('cancellation_reason'), isFalse); + }); + }); +}