diff --git a/lib/data/models/dispute.dart b/lib/data/models/dispute.dart index f13adb0a..a8cfcc78 100644 --- a/lib/data/models/dispute.dart +++ b/lib/data/models/dispute.dart @@ -6,6 +6,7 @@ import 'package:mostro_mobile/data/models/order.dart'; enum DisputeDescriptionKey { initiatedByUser, // You opened this dispute initiatedByPeer, // A dispute was opened against you + initiatedPendingAdmin,// Dispute initiated, waiting for admin assignment inProgress, // Dispute is being reviewed by an admin resolved, // Dispute has been resolved sellerRefunded, // Dispute resolved - seller refunded @@ -375,8 +376,8 @@ class DisputeData { return DisputeData( disputeId: dispute.disputeId, - orderId: dispute.orderId, // No fallback to hardcoded string - status: dispute.status ?? 'unknown', + orderId: dispute.orderId ?? (orderState?.order?.id), // Use order from orderState if dispute.orderId is null + status: dispute.status ?? 'initiated', descriptionKey: descriptionKey, counterparty: counterpartyName, isCreator: isUserCreator, @@ -428,10 +429,10 @@ class DisputeData { switch (status.toLowerCase()) { case 'initiated': if (isUserCreator == null) { - return DisputeDescriptionKey.unknown; // Unknown creator state + return DisputeDescriptionKey.initiatedPendingAdmin; // Waiting for admin assignment } - return isUserCreator - ? DisputeDescriptionKey.initiatedByUser + return isUserCreator + ? DisputeDescriptionKey.initiatedByUser : DisputeDescriptionKey.initiatedByPeer; case 'in-progress': return DisputeDescriptionKey.inProgress; @@ -452,6 +453,8 @@ class DisputeData { return 'You opened this dispute'; case DisputeDescriptionKey.initiatedByPeer: return 'A dispute was opened against you'; + case DisputeDescriptionKey.initiatedPendingAdmin: + return 'Waiting for admin assignment'; case DisputeDescriptionKey.inProgress: return 'Dispute is being reviewed by an admin'; case DisputeDescriptionKey.resolved: diff --git a/lib/data/repositories/dispute_repository.dart b/lib/data/repositories/dispute_repository.dart index 0ac53b7d..de205fe2 100644 --- a/lib/data/repositories/dispute_repository.dart +++ b/lib/data/repositories/dispute_repository.dart @@ -6,6 +6,7 @@ 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'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; /// Repository for managing dispute creation class DisputeRepository { @@ -63,39 +64,57 @@ class DisputeRepository { } Future> getUserDisputes() async { - // 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', - ), - ]; + try { + _logger.d('Getting user disputes from sessions'); + + // Get all user sessions and check their order states for disputes + final sessions = _ref.read(sessionNotifierProvider); + final disputes = []; + + for (final session in sessions) { + if (session.orderId != null) { + try { + // Get the order state for this session + final orderState = _ref.read(orderNotifierProvider(session.orderId!)); + + if (orderState.dispute != null) { + disputes.add(orderState.dispute!); + } + } catch (e) { + _logger.w('Failed to get order state for order ${session.orderId}: $e'); + } + } + } + + _logger.d('Found ${disputes.length} disputes from sessions'); + return disputes; + } catch (e) { + _logger.e('Failed to get user disputes: $e'); + return []; + } } Future getDispute(String disputeId) async { - // Mock implementation - await Future.delayed(const Duration(milliseconds: 300)); - - return Dispute( - disputeId: disputeId, - orderId: 'order_${disputeId.substring(0, 8)}', - status: 'initiated', - createdAt: DateTime.now().subtract(const Duration(hours: 2)), - action: 'dispute-initiated-by-you', - ); + try { + _logger.d('Getting dispute by ID: $disputeId'); + + // Get all user disputes and find the one with matching ID + final disputes = await getUserDisputes(); + final dispute = disputes.firstWhereOrNull( + (d) => d.disputeId == disputeId, + ); + + if (dispute != null) { + _logger.d('Found dispute with ID: $disputeId'); + } else { + _logger.w('No dispute found with ID: $disputeId'); + } + + return dispute; + } catch (e) { + _logger.e('Failed to get dispute by ID $disputeId: $e'); + return null; + } } Future sendDisputeMessage(String disputeId, String message) async { diff --git a/lib/features/chat/screens/chat_rooms_list.dart b/lib/features/chat/screens/chat_rooms_list.dart index 558c4e1a..6724f509 100644 --- a/lib/features/chat/screens/chat_rooms_list.dart +++ b/lib/features/chat/screens/chat_rooms_list.dart @@ -1,5 +1,6 @@ // NostrEvent is now accessed through ChatRoom model import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; @@ -51,8 +52,11 @@ class ChatRoomsScreen extends ConsumerWidget { ), ), ), - // Tab bar - ChatTabs(currentTab: currentTab), + // Tab bar - only show disputes tab in debug mode + if (kDebugMode) + ChatTabs(currentTab: currentTab) + else + _buildMessagesTabHeader(context), // Description text Container( width: double.infinity, @@ -66,7 +70,9 @@ class ChatRoomsScreen extends ConsumerWidget { ), ), child: Text( - _getTabDescription(context, currentTab), + kDebugMode ? _getTabDescription(context, currentTab) : + S.of(context)?.conversationsDescription ?? + 'Here you\'ll find your conversations with other users during trades.', style: TextStyle( color: AppTheme.textSecondary, fontSize: 14, @@ -75,25 +81,30 @@ class ChatRoomsScreen extends ConsumerWidget { ), // Content area with gesture detection Expanded( - child: GestureDetector( - onHorizontalDragEnd: (details) { - if (details.primaryVelocity != null && - details.primaryVelocity! < 0) { - // Swipe left - go to disputes - ref.read(chatTabProvider.notifier).state = ChatTabType.disputes; - } else if (details.primaryVelocity != null && - details.primaryVelocity! > 0) { - // Swipe right - go to messages - ref.read(chatTabProvider.notifier).state = ChatTabType.messages; - } - }, - child: Container( - color: AppTheme.backgroundDark, - child: currentTab == ChatTabType.messages - ? _buildBody(context, ref) - : const DisputesList(), - ), - ), + child: kDebugMode + ? GestureDetector( + onHorizontalDragEnd: (details) { + if (details.primaryVelocity != null && + details.primaryVelocity! < 0) { + // Swipe left - go to disputes + ref.read(chatTabProvider.notifier).state = ChatTabType.disputes; + } else if (details.primaryVelocity != null && + details.primaryVelocity! > 0) { + // Swipe right - go to messages + ref.read(chatTabProvider.notifier).state = ChatTabType.messages; + } + }, + child: Container( + color: AppTheme.backgroundDark, + child: currentTab == ChatTabType.messages + ? _buildBody(context, ref) + : const DisputesList(), + ), + ) + : Container( + color: AppTheme.backgroundDark, + child: _buildBody(context, ref), + ), ), // Add bottom padding to prevent content from being covered by BottomNavBar SizedBox( @@ -136,6 +147,41 @@ class ChatRoomsScreen extends ConsumerWidget { ); } + Widget _buildMessagesTabHeader(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppTheme.backgroundDark, + border: Border( + bottom: BorderSide( + color: Colors.white.withValues(alpha: 0.1), + width: 1.0, + ), + ), + ), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppTheme.mostroGreen, + width: 3.0, + ), + ), + ), + child: Text( + S.of(context)!.messages, + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.mostroGreen, + fontWeight: FontWeight.w600, + fontSize: 15, + letterSpacing: 0.5, + ), + ), + ), + ); + } + String _getTabDescription(BuildContext context, ChatTabType currentTab) { if (currentTab == ChatTabType.messages) { // Messages tab diff --git a/lib/features/disputes/providers/dispute_providers.dart b/lib/features/disputes/providers/dispute_providers.dart index 0412a77a..4fc15ad6 100644 --- a/lib/features/disputes/providers/dispute_providers.dart +++ b/lib/features/disputes/providers/dispute_providers.dart @@ -1,11 +1,13 @@ 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/models/session.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'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; /// Provider for the dispute repository final disputeRepositoryProvider = Provider.autoDispose((ref) { @@ -16,43 +18,12 @@ final disputeRepositoryProvider = Provider.autoDispose((ref) return DisputeRepository(nostrService, mostroPubkey, ref); }); -/// Provider for dispute details - uses mock data when enabled +/// Provider for dispute details - uses real data from repository final disputeDetailsProvider = FutureProvider.family((ref, disputeId) async { - // Simulate loading time - await Future.delayed(const Duration(milliseconds: 300)); - - if (!DisputeMockData.isMockEnabled) { - // TODO: Implement real dispute loading here - return null; - } - - // Get mock dispute data - final disputeData = DisputeMockData.getDisputeById(disputeId); - if (disputeData == null) return null; - - return Dispute( - disputeId: disputeData.disputeId, - orderId: disputeData.orderId, - status: disputeData.status, - createdAt: disputeData.createdAt, - action: _getActionFromStatus(disputeData.status, disputeData.userRole.name), - adminPubkey: disputeData.status != 'initiated' ? 'admin_123' : null, - ); + final repository = ref.watch(disputeRepositoryProvider); + return repository.getDispute(disputeId); }); -/// Helper function to convert status to action -String _getActionFromStatus(String status, String initiatorRole) { - switch (status) { - case 'initiated': - return 'dispute-initiated-by-you'; - case 'in-progress': - return initiatorRole == 'buyer' ? 'dispute-initiated-by-you' : 'dispute-initiated-by-peer'; - case 'resolved': - return 'dispute-resolved'; - default: - return 'dispute-initiated-by-you'; - } -} /// Stub provider for dispute chat messages - UI only implementation final disputeChatProvider = StateNotifierProvider.family, String>( @@ -61,23 +32,47 @@ final disputeChatProvider = StateNotifierProvider.family>((ref) async { - // Simulate loading time - await Future.delayed(const Duration(milliseconds: 500)); - - if (!DisputeMockData.isMockEnabled) { - // TODO: Implement real disputes loading here - return []; - } - - // Convert mock dispute data to Dispute objects - return DisputeMockData.mockDisputes.map((disputeData) => Dispute( - disputeId: disputeData.disputeId, - orderId: disputeData.orderId, - status: disputeData.status, - createdAt: disputeData.createdAt, - action: _getActionFromStatus(disputeData.status, disputeData.userRole.name), - adminPubkey: disputeData.status != 'initiated' ? 'admin_123' : null, - )).toList(); + final repository = ref.watch(disputeRepositoryProvider); + return repository.getUserDisputes(); +}); + +/// Provider for user disputes as DisputeData (UI view models) +final userDisputeDataProvider = FutureProvider>((ref) async { + final disputes = await ref.watch(userDisputesProvider.future); + final sessions = ref.read(sessionNotifierProvider); + + return disputes.map((dispute) { + // Find the specific session for this dispute's order + Session? matchingSession; + dynamic matchingOrderState; + + // Try to find the session and order state that contains this dispute + for (final session in sessions) { + if (session.orderId != null) { + try { + final orderState = ref.read(orderNotifierProvider(session.orderId!)); + + // Check if this order state contains our dispute + if (orderState.dispute?.disputeId == dispute.disputeId) { + matchingSession = session; + matchingOrderState = orderState; + break; + } + } catch (e) { + // Continue checking other sessions + continue; + } + } + } + + // If we found matching order state, use it for context + if (matchingSession != null && matchingOrderState != null) { + return DisputeData.fromDispute(dispute, orderState: matchingOrderState); + } + + // Fallback: create DisputeData without order context + return DisputeData.fromDispute(dispute); + }).toList(); }); \ No newline at end of file diff --git a/lib/features/disputes/widgets/dispute_status_content.dart b/lib/features/disputes/widgets/dispute_status_content.dart index 21376d5c..62e97a3d 100644 --- a/lib/features/disputes/widgets/dispute_status_content.dart +++ b/lib/features/disputes/widgets/dispute_status_content.dart @@ -124,6 +124,8 @@ class DisputeStatusContent extends StatelessWidget { return S.of(context)!.disputeOpenedByYou(dispute.counterpartyDisplay); case DisputeDescriptionKey.initiatedByPeer: return S.of(context)!.disputeOpenedAgainstYou(dispute.counterpartyDisplay); + case DisputeDescriptionKey.initiatedPendingAdmin: + return S.of(context)!.disputeWaitingForAdmin; case DisputeDescriptionKey.inProgress: // Use status text with a descriptive message return "${S.of(context)!.disputeStatusInProgress}: ${dispute.description}"; diff --git a/lib/features/disputes/widgets/disputes_list.dart b/lib/features/disputes/widgets/disputes_list.dart index 11f0f2d6..fab1b6b2 100644 --- a/lib/features/disputes/widgets/disputes_list.dart +++ b/lib/features/disputes/widgets/disputes_list.dart @@ -1,94 +1,90 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/disputes/widgets/dispute_list_item.dart'; -import 'package:mostro_mobile/data/models/dispute.dart'; +import 'package:mostro_mobile/features/disputes/providers/dispute_providers.dart'; import 'package:mostro_mobile/generated/l10n.dart'; -class DisputesList extends StatelessWidget { +class DisputesList extends ConsumerWidget { const DisputesList({super.key}); @override - Widget build(BuildContext context) { - // Only show hardcoded mock disputes in debug mode - final mockDisputes = kDebugMode ? [ - DisputeData( - disputeId: 'dispute_001', - orderId: 'order_abc123', - status: 'initiated', - descriptionKey: DisputeDescriptionKey.initiatedByUser, - counterparty: 'user_456', - isCreator: true, - createdAt: DateTime.now().subtract(const Duration(hours: 2)), - userRole: UserRole.buyer, - ), - DisputeData( - disputeId: 'dispute_002', - orderId: 'order_def456', - status: 'in-progress', - descriptionKey: DisputeDescriptionKey.inProgress, - counterparty: 'admin_789', - isCreator: false, - createdAt: DateTime.now().subtract(const Duration(days: 1)), - userRole: UserRole.seller, - ), - DisputeData( - disputeId: 'dispute_003', - orderId: 'order_ghi789', - status: 'resolved', - descriptionKey: DisputeDescriptionKey.resolved, - counterparty: 'user_123', - isCreator: null, // Unknown creator state for resolved dispute - createdAt: DateTime.now().subtract(const Duration(days: 3)), - userRole: UserRole.buyer, - ), - ] : []; + Widget build(BuildContext context, WidgetRef ref) { + final disputesAsync = ref.watch(userDisputeDataProvider); - if (mockDisputes.isEmpty) { - return Center( + return disputesAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.gavel, + Icons.error_outline, size: 64, color: AppTheme.textSecondary, ), const SizedBox(height: 16), Text( - kDebugMode ? S.of(context)!.noDisputesAvailable : S.of(context)!.disputesNotAvailable, + S.of(context)!.failedLoadDisputes, style: TextStyle( color: AppTheme.textSecondary, fontSize: 16, ), ), - const SizedBox(height: 8), - Text( - kDebugMode - ? S.of(context)!.disputesWillAppear - : S.of(context)!.disputesComingSoon, - style: TextStyle( - color: AppTheme.textSecondary, - fontSize: 14, - ), - textAlign: TextAlign.center, + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.refresh(userDisputeDataProvider), + child: Text(S.of(context)!.retry), ), ], ), - ); - } + ), + data: (disputes) { + if (disputes.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.gavel, + size: 64, + color: AppTheme.textSecondary, + ), + const SizedBox(height: 16), + Text( + S.of(context)!.noDisputesAvailable, + style: TextStyle( + color: AppTheme.textSecondary, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + S.of(context)!.disputesWillAppear, + style: TextStyle( + color: AppTheme.textSecondary, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: mockDisputes.length, - itemBuilder: (context, index) { - final disputeData = mockDisputes[index]; - - return DisputeListItem( - dispute: disputeData, - onTap: () { - context.push('/dispute_details/${disputeData.disputeId}'); + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: disputes.length, + itemBuilder: (context, index) { + final disputeData = disputes[index]; + + return DisputeListItem( + dispute: disputeData, + onTap: () { + context.push('/dispute_details/${disputeData.disputeId}'); + }, + ); }, ); }, diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 108693d6..249f880a 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -222,6 +222,18 @@ class AbstractMostroNotifier extends StateNotifier { logger.e('disputeInitiatedByYou: Missing Dispute payload for event ${event.id} with action ${event.action}'); return; } + + + // Ensure dispute has the orderId for proper association + final disputeWithOrderId = dispute.copyWith(orderId: orderId); + + // Save dispute in state for listing + state = state.copyWith(dispute: disputeWithOrderId); + + sendNotification(event.action, values: { + 'dispute_id': dispute.disputeId, + }, eventId: event.id); + break; case Action.disputeInitiatedByPeer: @@ -230,6 +242,18 @@ class AbstractMostroNotifier extends StateNotifier { logger.e('disputeInitiatedByPeer: Missing Dispute payload for event ${event.id} with action ${event.action}'); return; } + + + // Ensure dispute has the orderId for proper association + final disputeWithOrderId = dispute.copyWith(orderId: orderId); + + // Save dispute in state for listing + state = state.copyWith(dispute: disputeWithOrderId); + + sendNotification(event.action, values: { + 'dispute_id': dispute.disputeId, + }, eventId: event.id); + break; case Action.adminSettled: diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index d464f4b0..abc20a96 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -706,6 +706,10 @@ "counterparty": { "type": "String" } } }, + "disputeWaitingForAdmin": "Waiting for admin assignment", + "disputeYouOpened": "You opened this dispute", + "disputeOpenedAgainstUser": "A dispute was opened against you", + "disputeResolved": "Dispute has been resolved", "disputeInstruction1": "Wait for a solver to take your dispute. Once they arrive, share any relevant evidence to help clarify the situation.", "disputeInstruction2": "The final decision will be made based on the evidence presented.", "disputeInstruction3": "If you don't respond, the system will assume you don't want to cooperate and you might lose the dispute.", @@ -903,7 +907,6 @@ "counterparty": { "type": "String" } } }, - "orderIdLabel": "Order ID", "disputeIdLabel": "Dispute ID", "seller": "Seller", "buyer": "Buyer", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index aa7980e6..653168e1 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -712,6 +712,10 @@ "counterparty": { "type": "String" } } }, + "disputeWaitingForAdmin": "Esperando asignación de administrador", + "disputeYouOpened": "Tú abriste esta disputa", + "disputeOpenedAgainstUser": "Se abrió una disputa contra ti", + "disputeResolved": "La disputa ha sido resuelta", "disputeInstruction1": "Espera a que un mediador tome tu disputa. Una vez que llegue, comparte cualquier evidencia relevante para ayudar a aclarar la situación.", "disputeInstruction2": "La decisión final se tomará en base a la evidencia presentada.", "disputeInstruction3": "Si no respondes, el sistema asumirá que no deseas cooperar y podrías perder la disputa.", @@ -1046,7 +1050,6 @@ "counterparty": { "type": "String" } } }, - "orderIdLabel": "ID de Orden", "disputeIdLabel": "ID de Disputa", "seller": "Vendedor", "buyer": "Comprador", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index dbf516ec..f8728bd6 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -766,6 +766,10 @@ "counterparty": { "type": "String" } } }, + "disputeWaitingForAdmin": "In attesa di assegnazione amministratore", + "disputeYouOpened": "Hai aperto questa disputa", + "disputeOpenedAgainstUser": "È stata aperta una disputa contro di te", + "disputeResolved": "La disputa è stata risolta", "disputeInstruction1": "Attendi che un risolutore prenda in carico la tua disputa. Una volta arrivato, condividi qualsiasi prova rilevante per aiutare a chiarire la situazione.", "disputeInstruction2": "La decisione finale sarà presa sulla base delle prove presentate.", "disputeInstruction3": "Se non rispondi, il sistema presumerà che tu non voglia collaborare e potresti perdere la disputa.", diff --git a/lib/services/dispute_service.dart b/lib/services/dispute_service.dart index 91a6ff38..7b86551f 100644 --- a/lib/services/dispute_service.dart +++ b/lib/services/dispute_service.dart @@ -50,6 +50,8 @@ class DisputeService { Future sendDisputeMessage(String disputeId, String message) async { // Mock implementation await Future.delayed(const Duration(milliseconds: 200)); + // Mock implementation + await Future.delayed(const Duration(milliseconds: 200)); } Future initiateDispute(String orderId, String reason) async {