diff --git a/lib/data/repositories/dispute_repository.dart b/lib/data/repositories/dispute_repository.dart index 9c186b01..0ac53b7d 100644 --- a/lib/data/repositories/dispute_repository.dart +++ b/lib/data/repositories/dispute_repository.dart @@ -1,10 +1,66 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/dispute.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -/// Stub repository for disputes - UI only implementation +/// Repository for managing dispute creation class DisputeRepository { - static final DisputeRepository _instance = DisputeRepository._internal(); - factory DisputeRepository() => _instance; - DisputeRepository._internal(); + final NostrService _nostrService; + final String _mostroPubkey; + final Ref _ref; + final Logger _logger = Logger(); + + DisputeRepository(this._nostrService, this._mostroPubkey, this._ref); + + /// Create a new dispute for an order + Future createDispute(String orderId) async { + try { + _logger.d('Creating dispute for order: $orderId'); + + // Get user's session for the order to get the trade key + final sessions = _ref.read(sessionNotifierProvider); + final session = sessions.firstWhereOrNull( + (s) => s.orderId == orderId, + ); + + if (session == null) { + _logger + .e('No session found for order: $orderId, cannot create dispute'); + return false; + } + + // Validate trade key is present + if (session.tradeKey.private.isEmpty) { + _logger.e('Trade key is empty for order: $orderId, cannot create dispute'); + return false; + } + + // Create dispute message using Gift Wrap protocol (NIP-59) + final disputeMessage = MostroMessage( + action: Action.dispute, + id: orderId, + ); + + // Wrap message using Gift Wrap protocol (NIP-59) + final event = await disputeMessage.wrap( + tradeKey: session.tradeKey, + recipientPubKey: _mostroPubkey, + ); + + // Send the wrapped event to Mostro + await _nostrService.publishEvent(event); + + _logger.d('Successfully sent dispute creation for order: $orderId'); + return true; + } catch (e) { + _logger.e('Failed to create dispute: $e'); + return false; + } + } Future> getUserDisputes() async { // Mock implementation for UI testing diff --git a/lib/features/disputes/providers/dispute_providers.dart b/lib/features/disputes/providers/dispute_providers.dart index 2ab4a577..0412a77a 100644 --- a/lib/features/disputes/providers/dispute_providers.dart +++ b/lib/features/disputes/providers/dispute_providers.dart @@ -1,8 +1,20 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/dispute.dart'; import 'package:mostro_mobile/data/models/dispute_chat.dart'; +import 'package:mostro_mobile/data/repositories/dispute_repository.dart'; import 'package:mostro_mobile/features/disputes/notifiers/dispute_chat_notifier.dart'; import 'package:mostro_mobile/features/disputes/data/dispute_mock_data.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; + +/// Provider for the dispute repository +final disputeRepositoryProvider = Provider.autoDispose((ref) { + final nostrService = ref.watch(nostrServiceProvider); + final settings = ref.watch(settingsProvider); + final mostroPubkey = settings.mostroPublicKey; + + return DisputeRepository(nostrService, mostroPubkey, ref); +}); /// Provider for dispute details - uses mock data when enabled final disputeDetailsProvider = FutureProvider.family((ref, disputeId) async { diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 88bb4e34..0cd4a33a 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/features/disputes/providers/dispute_providers.dart'; import 'package:mostro_mobile/generated/l10n.dart'; class TradeDetailScreen extends ConsumerWidget { @@ -711,7 +712,36 @@ class TradeDetailScreen extends ConsumerWidget { // Only proceed with dispute if user confirmed if (result == true) { - ref.read(orderNotifierProvider(orderId).notifier).disputeOrder(); + try { + // Create dispute using the repository + final repository = ref.read(disputeRepositoryProvider); + final success = await repository.createDispute(orderId); + + if (success && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context)!.disputeCreatedSuccessfully), + backgroundColor: AppTheme.mostroGreen, + ), + ); + } else if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context)!.disputeCreationFailed), + backgroundColor: AppTheme.red1, + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context)!.disputeCreationErrorWithMessage(e.toString())), + backgroundColor: AppTheme.red1, + ), + ); + } + } } }, style: ElevatedButton.styleFrom( diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 099763bd..3c148ca0 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -213,7 +213,7 @@ class MostroMessageDetail extends ConsumerWidget { .of(context)! .disputeInitiatedByPeer(orderPayload!.id!, payload!.disputeId); case actions.Action.adminTookDispute: - return S.of(context)!.adminTookDisputeUsers('{admin token}'); + return S.of(context)!.adminTookDisputeUsers; case actions.Action.adminCanceled: return S.of(context)!.adminCanceledUsers(orderPayload!.id ?? ''); case actions.Action.adminSettled: diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index e18f1951..57dfc170 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -106,10 +106,34 @@ "cooperativeCancelInitiatedByYou": "You have initiated the cancellation of the order ID: {id}. Your counterparty must agree. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.", "cooperativeCancelInitiatedByPeer": "Your counterparty wants to cancel order ID: {id}. If you agree, please send me cancel-order-message. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.", "cooperativeCancelAccepted": "Order {id} has been successfully canceled!", - "disputeInitiatedByYou": "You have initiated a dispute for order Id: {id}. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. Your dispute token is: {user_token}.", - "disputeInitiatedByPeer": "Your counterparty has initiated a dispute for order Id: {id}. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. Your dispute token is: {user_token}.", + "disputeInitiatedByYou": "You have initiated a dispute for order ID: {id}. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. Your dispute ID is: {dispute_id}.", + "@disputeInitiatedByYou": { + "placeholders": { + "id": { + "type": "String", + "example": "abc123" + }, + "dispute_id": { + "type": "String", + "example": "dispute-456" + } + } + }, + "disputeInitiatedByPeer": "Your counterparty has initiated a dispute for order ID: {id}. A solver will be assigned soon. Once assigned, I will share their npub with you, and only they will be able to assist you. Your dispute ID is: {dispute_id}.", + "@disputeInitiatedByPeer": { + "placeholders": { + "id": { + "type": "String", + "example": "abc123" + }, + "dispute_id": { + "type": "String", + "example": "dispute-456" + } + } + }, "adminTookDisputeAdmin": "Here are the details of the dispute order you have taken: {details}. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed.", - "adminTookDisputeUsers": "The solver {admin_npub} will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token.", + "adminTookDisputeUsers": "A dispute resolver has been assigned to handle your dispute. They will contact you through the app.", "adminCanceledAdmin": "You have canceled the order ID: {id}.", "adminCanceledUsers": "Admin has canceled the order ID: {id}.", "adminSettledAdmin": "You have completed the order ID: {id}.", @@ -267,6 +291,16 @@ "orderIdCopiedMessage": "Order ID copied to clipboard", "disputeTradeDialogTitle": "Start Dispute", "disputeTradeDialogContent": "You are about to start a dispute with your counterparty. Do you want to continue?", + "disputeCreatedSuccessfully": "Dispute created successfully", + "disputeCreationFailed": "Failed to create dispute", + "disputeCreationErrorWithMessage": "Error creating dispute: {error}", + "@disputeCreationErrorWithMessage": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "typeToAdd": "Type to add...", "noneSelected": "None selected", "fiatCurrencies": "Fiat currencies", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 03b12403..a7063673 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -106,10 +106,10 @@ "cooperativeCancelInitiatedByYou": "Has iniciado la cancelación de la orden ID: {id}. Tu contraparte debe estar de acuerdo. Si no responden, puedes abrir una disputa. Ten en cuenta que ningún administrador te contactará sobre esta cancelación a menos que abras una disputa primero.", "cooperativeCancelInitiatedByPeer": "Tu contraparte quiere cancelar la orden ID: {id}. Si estás de acuerdo, por favor envíame cancel-order-message. Ten en cuenta que ningún administrador te contactará sobre esta cancelación a menos que abras una disputa primero.", "cooperativeCancelAccepted": "¡La orden {id} ha sido cancelada exitosamente!", - "disputeInitiatedByYou": "Has iniciado una disputa para la orden Id: {id}. Un resolutor será asignado pronto. Una vez asignado, compartiré su npub contigo, y solo ellos podrán ayudarte. Tu token de disputa es: {user_token}.", - "disputeInitiatedByPeer": "Tu contraparte ha iniciado una disputa para la orden Id: {id}. Un resolutor será asignado pronto. Una vez asignado, compartiré su npub contigo, y solo ellos podrán ayudarte. Tu token de disputa es: {user_token}.", + "disputeInitiatedByYou": "Has iniciado una disputa para la orden Id: {id}. Un resolutor será asignado pronto. Una vez asignado, compartiré su npub contigo, y solo ellos podrán ayudarte. Tu ID de disputa es: {dispute_id}.", + "disputeInitiatedByPeer": "Tu contraparte ha iniciado una disputa para la orden Id: {id}. Un resolutor será asignado pronto. Una vez asignado, compartiré su npub contigo, y solo ellos podrán ayudarte. Tu ID de disputa es: {dispute_id}.", "adminTookDisputeAdmin": "Aquí están los detalles de la orden en disputa que has tomado: {details}. Necesitas determinar qué usuario es correcto y decidir si cancelar o completar la orden. Ten en cuenta que tu decisión será final y no se puede revertir.", - "adminTookDisputeUsers": "El resolutor {admin_npub} manejará tu disputa. Puedes contactarlos directamente, pero si te contactan primero, asegúrate de pedirles tu token de disputa.", + "adminTookDisputeUsers": "Se ha asignado un resolutor de disputas para manejar tu caso. Te contactarán a través de la aplicación.", "adminCanceledAdmin": "Has cancelado la orden ID: {id}.", "adminCanceledUsers": "El administrador ha cancelado la orden ID: {id}.", "adminSettledAdmin": "Has completado la orden ID: {id}.", @@ -608,6 +608,16 @@ "orderIdCopiedMessage": "ID de orden copiado al portapapeles", "disputeTradeDialogTitle": "Iniciar Disputa", "disputeTradeDialogContent": "Estás a punto de iniciar una disputa con tu contraparte. ¿Deseas continuar?", + "disputeCreatedSuccessfully": "Disputa creada exitosamente", + "disputeCreationFailed": "Falló la creación de la disputa", + "disputeCreationErrorWithMessage": "Error al crear la disputa: {error}", + "@disputeCreationErrorWithMessage": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "language": "Idioma", "systemDefault": "Predeterminado", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 3ff6f76c..e9af4301 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -106,10 +106,34 @@ "cooperativeCancelInitiatedByYou": "Hai iniziato l'annullamento dell'ordine ID: {id}. La tua controparte deve anche concordare l'annullamento. Se non risponde, puoi aprire una disputa. Nota che nessun amministratore ti contatterà MAI riguardo questo annullamento a meno che tu non apra prima una disputa.", "cooperativeCancelInitiatedByPeer": "La tua controparte vuole annullare l'ordine ID: {id}. Nota che nessun amministratore ti contatterà MAI riguardo questo annullamento a meno che tu non apra prima una disputa. Se concordi su tale annullamento, premi: Annulla Ordine.", "cooperativeCancelAccepted": "L'ordine {id} è stato annullato con successo!", - "disputeInitiatedByYou": "Hai iniziato una disputa per l'ordine ID: {id}. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, riceverai il suo npub e solo questo account potrà assisterti. Devi contattare l'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa. Il token di questa disputa è: {user_token}.", - "disputeInitiatedByPeer": "La tua controparte ha iniziato una disputa per l'ordine ID: {id}. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, ti condividerò il loro npub e solo loro potranno assisterti. Devi contattare l'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa. Il token di questa disputa è: {user_token}.", + "disputeInitiatedByYou": "Hai iniziato una disputa per l'ordine ID: {id}. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, riceverai il suo npub e solo questo account potrà assisterti. Devi contattare l'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti l'ID per la tua disputa. L'ID di questa disputa è: {dispute_id}.", + "disputeInitiatedByPeer": "La tua controparte ha iniziato una disputa per l'ordine ID: {id}. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, ti condividerò il loro npub e solo loro potranno assisterti. Devi contattare l'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti l'ID per la tua disputa. L'ID di questa disputa è: {dispute_id}.", + "@disputeInitiatedByPeer": { + "placeholders": { + "id": { + "type": "String", + "description": "ID dell'ordine" + }, + "dispute_id": { + "type": "String", + "description": "ID della disputa" + } + } + }, + "@disputeInitiatedByYou": { + "placeholders": { + "id": { + "type": "String", + "description": "ID dell'ordine" + }, + "dispute_id": { + "type": "String", + "description": "ID della disputa" + } + } + }, "adminTookDisputeAdmin": "Ecco i dettagli dell'ordine della disputa che hai preso: {details}. Devi determinare quale utente ha ragione e decidere se annullare o completare l'ordine. Nota che la tua decisione sarà finale e non può essere annullata.", - "adminTookDisputeUsers": "L'amministratore {admin_npub} gestirà la tua disputa. Devi contattare l'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa..", + "adminTookDisputeUsers": "È stato assegnato un risolutore di controversie per gestire il tuo caso. Ti contatteranno attraverso l'applicazione.", "adminCanceledAdmin": "Hai annullato l'ordine ID: {id}!", "adminCanceledUsers": "L'amministratore ha annullato l'ordine ID: {id}!", "adminSettledAdmin": "Hai completato l'ordine ID: {id}!", @@ -649,6 +673,16 @@ "orderIdCopiedMessage": "ID ordine copiato negli appunti", "disputeTradeDialogTitle": "Inizia Disputa", "disputeTradeDialogContent": "Stai per iniziare una disputa con la tua controparte. Vuoi continuare?", + "disputeCreatedSuccessfully": "Disputa creata con successo", + "disputeCreationFailed": "Creazione della disputa fallita", + "disputeCreationErrorWithMessage": "Errore nella creazione della disputa: {error}", + "@disputeCreationErrorWithMessage": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "language": "Lingua", "systemDefault": "Predefinito di sistema", diff --git a/lib/services/dispute_service.dart b/lib/services/dispute_service.dart index f4eb7e20..91a6ff38 100644 --- a/lib/services/dispute_service.dart +++ b/lib/services/dispute_service.dart @@ -1,5 +1,5 @@ +import 'dart:math'; import 'package:mostro_mobile/data/models/dispute.dart'; -import 'package:mostro_mobile/data/repositories/dispute_repository.dart'; /// Stub service for disputes - UI only implementation class DisputeService { @@ -7,18 +7,49 @@ class DisputeService { factory DisputeService() => _instance; DisputeService._internal(); - final DisputeRepository _disputeRepository = DisputeRepository(); - Future> getUserDisputes() async { - return await _disputeRepository.getUserDisputes(); + // Mock implementation for UI testing + await Future.delayed(const Duration(milliseconds: 500)); + + return [ + Dispute( + disputeId: 'dispute_001', + orderId: 'order_001', + status: 'initiated', + createdAt: DateTime.now().subtract(const Duration(hours: 1)), + action: 'dispute-initiated-by-you', + ), + Dispute( + disputeId: 'dispute_002', + orderId: 'order_002', + status: 'in-progress', + createdAt: DateTime.now().subtract(const Duration(days: 1)), + action: 'dispute-initiated-by-peer', + adminPubkey: 'admin_123', + ), + ]; } Future getDispute(String disputeId) async { - return await _disputeRepository.getDispute(disputeId); + // Mock implementation + await Future.delayed(const Duration(milliseconds: 300)); + + // Safe prefix extraction to avoid RangeError + final prefixLen = min(8, disputeId.length); + final orderIdSuffix = disputeId.isEmpty ? 'unknown' : disputeId.substring(0, prefixLen); + + return Dispute( + disputeId: disputeId, + orderId: 'order_$orderIdSuffix', + status: 'initiated', + createdAt: DateTime.now().subtract(const Duration(hours: 2)), + action: 'dispute-initiated-by-you', + ); } Future sendDisputeMessage(String disputeId, String message) async { - await _disputeRepository.sendDisputeMessage(disputeId, message); + // Mock implementation + await Future.delayed(const Duration(milliseconds: 200)); } Future initiateDispute(String orderId, String reason) async {