Skip to content

Conversation

@AndreaDiazCorreia
Copy link
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Aug 25, 2025

PR Description: Implement Mobile App Dispute System
Summary
This PR implements a comprehensive dispute system for the MostroP2P mobile app, allowing users to create, view, and manage disputes with proper order ID tracking and status visualization.

Features Implemented

  1. Dispute Event Fetching
    Fetch kind 38383 events with z=dispute tags from Mostro
    Extract disputeId from d tag and status from s tag
    Deduplicate by keeping latest event per dispute ID
  2. Order ID Resolution
    Created _resolveOrderIdForDispute() method that searches DM events
    Looks for payload.dispute array where first element matches disputeId
    Returns corresponding order.id from the DM
    Handles cases where order ID cannot be resolved with "Unknown Order ID" fallback
  3. Status Badge System
    "initiated": Yellow (statusPendingBackground/Text)
    "in-progress": Green (statusSuccessBackground/Text)
    "resolved"/"solved": Blue (statusSettledBackground/Text)
  4. Localization Support
    Added "disputeStatusInitiated" strings to all language files (en, es, it)
    Full i18n support for dispute status labels
    Files Added/Modified
    /lib/data/repositories/dispute_repository.dart - Core dispute data handling
    /lib/features/disputes/widgets/dispute_status_badge.dart - Status visualization
    /lib/features/disputes/widgets/dispute_order_id.dart - Order ID display component
    /lib/l10n/intl_*.arb - Localization strings
    User Experience
    Users can view all their disputes in a dedicated list
    Each dispute shows the correct order ID and current status
    Status colors provide clear visual feedback
    Chat functionality available for in-progress disputes
    Graceful handling of edge cases (missing order IDs)
image image

Important

There may be bugs with disputes; for any UI bug or error, please open a new issue because the PR is already very large.

Summary by CodeRabbit

  • New Features

    • Full disputes workflow: list, details page, status display, and per-dispute chat with admin (real-time updates).
    • Create disputes from trade details with success/failure feedback.
    • Chat > Disputes tab now navigates to dispute details.
  • Improvements

    • Better empty/loading/error states with retry.
    • Token/admin assignment indicators and chat message UI (verified/awaiting).
    • Amount strings now include currency across screens.
    • Extensive EN/ES/IT translations for disputes and chat.
  • Chores

    • Added logging dependency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

Walkthrough

Adds a disputes feature: routing to a dispute details/chat screen, expanded dispute models and chat model, repository/service for dispute events and messaging via Mostro/Nostr, Riverpod providers/notifiers, multiple dispute UI widgets (list/detail/communication/input), i18n keys (EN/ES/IT), Mostro parsing hardening, logging dependency, and minor config/localization refactors.

Changes

Cohort / File(s) Summary
Routing
lib/core/app_routes.dart
Adds /dispute_details/:disputeId route that builds DisputeChatScreen using default transition.
Config
lib/core/config.dart
Minor formatting cleanup (whitespace).
Models — Dispute
lib/data/models/dispute.dart
Expands Dispute fields/constructors, adds JSON/Nostr parsing, equality, copyWith, and new UI view-model DisputeData plus enums/semantic keys.
Models — Chat/Event
lib/data/models/dispute_chat.dart, lib/data/models/dispute_event.dart
New DisputeChat model (messages, token verification, helpers) and DisputeEvent parser/validator for kind 38383.
Repository
lib/data/repositories/dispute_repository.dart
New DisputeRepository to list/fetch/create disputes and subscribe to dispute events via Nostr/Mostro.
Providers
lib/features/disputes/providers/dispute_providers.dart
Adds Riverpod providers: disputeRepositoryProvider, userDisputesProvider, disputeDetailsProvider, disputeEventsStreamProvider, createDisputeProvider.
Notifier
lib/features/disputes/notifiers/dispute_chat_notifier.dart
New disputeChatProvider and DisputeChatNotifier managing chat streams, admin assignment, and sendMessage (NIP‑17 encrypted) with optimistic updates.
Screens
lib/features/disputes/screens/dispute_chat_screen.dart
New DisputeChatScreen that loads dispute details (with enhanced lookup), shows DisputeInfoCard, communication and input sections, and handles loading/error states.
Widgets — Dispute UI
lib/features/disputes/widgets/*
Multiple new widgets: DisputesList, DisputeListItem, DisputeInfoCard, DisputeContent, DisputeHeader, DisputeOrderId, DisputeDescription, DisputeIcon, DisputeStatusBadge, DisputeStatusContent, DisputeCommunicationSection, DisputeInputSection.
Chat Integration
lib/features/chat/screens/chat_rooms_list.dart
Adds DisputesList tab, replaces placeholder with real widget, and uses dynamic tab descriptions.
Trade/Order UI i18n calls
lib/features/chat/widgets/trade_information_tab.dart, lib/features/order/screens/take_order_screen.dart, lib/shared/widgets/order_cards.dart
Replace forAmount(...) with forAmountWithCurrency(...) (args unchanged).
Trade Flow
lib/features/trades/screens/trade_detail_screen.dart
Use createDispute via dispute provider; show success/failure snackbars and call existing order notifier on success.
Notifications
lib/features/order/notfiers/abstract_mostro_notifier.dart
Notification payloads now prefer dispute.disputeToken (fallback to disputeId).
Mostro Service
lib/services/mostro_service.dart
Hardens decrypted event processing with validation, structured parsing, and improved logging.
Dispute Service
lib/services/dispute_service.dart
New service subscribing to dispute events (kind 38383 / z=dispute) and mapping them to order notifier MostroMessages.
Session Notifier
lib/shared/notifiers/session_notifier.dart
Remove sensitive shared-key value from logs; log only success.
Localization
lib/l10n/intl_en.arb, lib/l10n/intl_es.arb, lib/l10n/intl_it.arb
Add extensive dispute/chat strings; rename forAmountforAmountWithCurrency and update placeholders.
Dependencies
pubspec.yaml
Adds logging: ^1.2.0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as DisputeChatScreen
  participant Prov as dispute_providers
  participant Repo as DisputeRepository
  participant Nostr as NostrService

  User->>UI: Open /dispute_details/:disputeId
  UI->>Prov: disputeDetailsProvider(disputeId)
  Prov->>Repo: getDisputeDetails(disputeId)
  Repo->>Nostr: Query events (kind 38383) / storage
  Nostr-->>Repo: Events / decrypted messages
  Repo-->>Prov: Dispute
  Prov-->>UI: AsyncValue<Dispute?>

  UI->>Prov: disputeChatProvider(disputeId)
  Prov->>Repo: subscribeToDisputeEvents()
  Repo->>Nostr: Subscribe (kind 38383)
  Nostr-->>Repo: Event stream
  Repo-->>Prov: Stream<Dispute>
  Prov-->>UI: Updates (admin assignment / token)

  User->>UI: Send message
  UI->>Prov: notifier.sendMessage(content)
  Prov->>Nostr: Encrypted DM (NIP-17) to admin
  Nostr-->>Prov: Ack / Error
  Prov-->>UI: Optimistic update / rollback on error
Loading
sequenceDiagram
  autonumber
  participant DisputeSvc as DisputeService
  participant Nostr as NostrService
  participant OrderN as OrderNotifier

  DisputeSvc->>Nostr: Subscribe kind 38383 (z=dispute)
  Nostr-->>DisputeSvc: DisputeEvent
  DisputeSvc->>OrderN: handleEvent(MostroMessage{action, payload: Dispute})
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch
  • chebizarro

Poem

I twitch my whiskers at a new route,
Hop into disputes with a nimble toot.
Tokens and badges, admin in sight,
Nostr winds send messages through night.
I nibble logs neat — keys hidden tight. 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch andrea/issue-97

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
lib/features/chat/widgets/trade_information_tab.dart (1)

26-37: Don’t fabricate dates; surface “Unknown date” instead

_getOrderCreationDate() always returns a non-null DateTime (falls back to “now - 1 hour”), which makes the unknownDate branch unreachable and displays a misleading date.

Apply this to return null when unknown so the UI can show the localized “Unknown date”:

-  DateTime _getOrderCreationDate() {
+  DateTime? _getOrderCreationDate() {
     if (order?.createdAt != null && order!.createdAt! > 0) {
       // Convert Unix timestamp to DateTime
       return DateTime.fromMillisecondsSinceEpoch(order!.createdAt! * 1000);
     }
-    
-    // Fallback: use current time minus a reasonable amount
-    // This is better than showing "Unknown date"
-    return DateTime.now().subtract(const Duration(hours: 1));
+    // Unknown creation time
+    return null;
   }

No call-site change needed since _formatOrderDate(DateTime?) already handles null:

-  _formatOrderDate(_getOrderCreationDate(), context),
+  _formatOrderDate(_getOrderCreationDate(), context),

Also applies to: 199-200

lib/services/mostro_service.dart (1)

228-229: Don’t log full outgoing payloads

order.toJson() can include invoices and other sensitive fields. Avoid logging the full payload.

Prefer minimal, non-sensitive metadata:

-    _logger.i('Sending DM, Event ID: ${event.id} with payload: ${order.toJson()}');
+    _logger.i('Sending DM, Event ID: ${event.id}, action: ${order.action}, hasPayload: ${order.payload != null}');
lib/shared/widgets/order_cards.dart (1)

58-74: Bug: currency appears twice after the switch to forAmountWithCurrency

You’re passing amountString (which already contains "$amount $currency $flag") as the first argument and currency as the second. This renders duplicated currency, e.g., “por 100 USD 🇺🇸 USD”.

Apply this diff to build the string correctly and keep the flag as a separate span:

-                    text: S.of(context)!.forAmountWithCurrency(amountString, currency),
+                    text: S.of(context)!.forAmountWithCurrency(amount, currency),
                     style: const TextStyle(
                       color: Colors.white70,
                       fontSize: 16,
                     ),
                     children: [
+                      if (currencyFlag != null && currencyFlag.isNotEmpty)
+                        TextSpan(
+                          text: ' $currencyFlag',
+                          style: const TextStyle(
+                            color: Colors.white60,
+                            fontSize: 15,
+                          ),
+                        ),
                       if (priceText != null && priceText!.isNotEmpty)
                         TextSpan(
                           text: ' $priceText',
                           style: const TextStyle(
                             color: Colors.white60,
                             fontSize: 15,
                           ),
                         ),
                     ],

And outside this hunk (for cleanliness), remove amountString and keep currencyFlag:

final currencyFlag = CurrencyUtils.getFlagFromCurrencyData(currency, currencyData);

This avoids duplicate text and preserves the flag.

lib/features/order/screens/take_order_screen.dart (1)

156-166: Duplicate currency in “for amount with currency” text

amountString already includes the currency (and flag), so passing order.currency again duplicates it (e.g., “for 100 USD 🇺🇸 USD”). Pass only the numeric amount to the i18n string and, if desired, append the flag separately.

Apply this diff:

-                        text: S.of(context)!.forAmountWithCurrency(amountString, order.currency ?? ''),
+                        text: S.of(context)!.forAmountWithCurrency(
+                          order.fiatAmount.toString(),
+                          order.currency ?? '',
+                        ),
                         style: const TextStyle(
                           color: Colors.white70,
                           fontSize: 16,
                         ),
-                        children: [
+                        children: [
+                          TextSpan(text: ' $currencyFlag'),
                           if (priceText.isNotEmpty)
                             TextSpan(
                               text: ' $priceText',
                               style: const TextStyle(
                                 color: Colors.white60,
                                 fontSize: 15,
                               ),
                             ),
lib/l10n/intl_it.arb (1)

730-741: Duplicate key: forAmount reintroduced while also adding forAmountWithCurrency.

forAmount already exists earlier in the file. Keep one definition and prefer forAmountWithCurrency for clarity; remove this duplicate.

- "forAmount": "per {amount}",

Also audit callsites as suggested for EN.

🧹 Nitpick comments (40)
lib/shared/notifiers/session_notifier.dart (1)

181-183: Review Sensitive Logger Calls for Clarity

I found two remaining logger invocations that output key material—though they’re only logging the public halves, it’s worth confirming they’re intentional and renaming them for clarity:

• lib/features/chat/notifiers/chat_room_notifier.dart:233
_logger.i('Session found with shared key: ${session.sharedKey?.public}');

• lib/data/repositories/dispute_repository.dart:31
_logger.d(' Session: orderId=${session.orderId}, tradeKey=${session.tradeKey.public}');

These log the public components (.public) of the shared/trade keys, which aren’t secrets—but the messages imply they’re full “shared key” or “trade key.” To avoid confusion, consider renaming them to explicitly reference “public key,” e.g.:

- _logger.i('Session found with shared key: ${session.sharedKey?.public}');
+ _logger.i('Session public key for session ${session.id}: ${session.sharedKey?.public}');

And similarly for tradeKey.public.

Aside from these, I didn’t find any other logger calls exposing sensitive fields. Great work sanitizing the derived key log!

lib/features/chat/widgets/trade_information_tab.dart (2)

135-135: LGTM: updated i18n key usage for fiat amount

Switching to forAmountWithCurrency(amount, currency) aligns with the new ARB keys and improves clarity. One minor improvement: avoid .toString() to preserve locale-specific formatting (thousands separators/decimals).

Example:

- S.of(context)!.forAmountWithCurrency(order!.fiatAmount.toString(), order!.fiatCode),
+ S.of(context)!.forAmountWithCurrency(
+   NumberFormat.decimalPattern().format(order!.fiatAmount),
+   order!.fiatCode,
+ ),

115-126: Localize status labels instead of raw .toUpperCase()

order!.status.value.toUpperCase() is user-facing text and should be localized. Consider mapping known statuses to i18n keys (e.g., orderStatusActive, orderStatusPending, etc.) for consistency with the disputes status badges.

If you add keys, the usage could be:

- order!.status.value.toUpperCase(),
+ {
+   'active': S.of(context)!.orderStatusActive,
+   'pending': S.of(context)!.orderStatusPending,
+   'completed': S.of(context)!.orderStatusCompleted,
+ }[order!.status.value] ?? order!.status.value.toUpperCase(),

This preserves a fallback while providing localized strings for known statuses.

Also applies to: 121-129

lib/services/mostro_service.dart (3)

45-52: Mark events as processed only after successful parse/persist

The event is marked stored before decryption/parsing. If parsing fails, the event is permanently skipped on retries, potentially dropping messages.

Move the putItem call to after the message is successfully persisted:

   if (await eventStore.hasItem(event.id!)) return;
-  await eventStore.putItem(
-    event.id!,
-    {
-      'id': event.id,
-      'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
-    },
-  );

…and after addMessage(...) succeeds:

         await messageStorage.addMessage(decryptedEvent.id!, msg);
-        
+        await eventStore.putItem(
+          event.id!,
+          {
+            'id': event.id,
+            'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
+          },
+        );

If idempotency is critical and duplicates are a concern, consider a two-phase marker (processingprocessed) with retry-safe semantics.

Also applies to: 100-105


75-87: Be resilient to alternate DM shapes

Current parsing assumes jsonDecode(content) yields a non-empty List whose first element is a Map. If upstream sends a direct Map (single-object), this will be dropped.

Either confirm upstream always wraps content in a single-element array, or support both shapes:

-        final result = jsonDecode(decryptedEvent.content!);
-        if (result is! List || result.isEmpty) {
+        final result = jsonDecode(decryptedEvent.content!);
+        if (result is List && result.isNotEmpty) {
+          // ok
+        } else if (result is Map<String, dynamic>) {
+          // normalize to list-of-one
+          result = [result];
+        } else {
           _logger.w('Invalid message format: Expected non-empty list, got $result');
           return;
         }

Adjust types accordingly.


55-63: Recipient Tag Is Spec-Mandated for Direct Messages

According to both NIP-04 (kind 4) and NIP-17 (kind 14), every encrypted direct‐message event MUST include a “p” tag identifying the recipient’s public key (nostr-nips.com, e2encrypted.com). In our code, event.recipient is implemented as pulling the first “p” tag from the event’s tags in NostrEventExtensions.recipient (). The gift-wrap wrapper we create in mostroWrap also always emits a “p” tag for the shared key, so any incoming wrapper event will have recipient populated.

Thus, filtering on event.recipient to find the matching session is consistent with the Nostr spec and safe for all properly formed DM events—no change is required to make the lookup work under normal conditions.

Optional robustness improvement: if you ever process events from untrusted or non-standard sources, you could catch cases where event.recipient is null and instead loop through each session’s private key to attempt NostrUtils.decryptNIP59Event, matching on the resulting inner event’s pubkey or tags. This fallback is purely defensive and not strictly necessary.

lib/data/models/dispute_event.dart (3)

44-51: Normalize status and restrict to allowed set

Today, upstream may send variants like "in-progress", "in_progress", or "solved". Normalizing to a controlled set makes downstream UI more robust and aligns with the status badge system.

Proposed helper (outside selected range) to add below the factory:

static String _normalizeStatus(String raw) {
  final v = raw.trim().toLowerCase();
  if (v == 'initiated' || v == 'started') return 'initiated';
  if (v == 'in-progress' || v == 'in_progress' || v == 'ongoing') return 'in-progress';
  if (v == 'resolved' || v == 'solved') return 'resolved';
  if (v == 'closed') return 'closed';
  return 'unknown';
}

Then update the factory’s status assignment:

-    final statusValue = (sTag != null && sTag.length > 1 && sTag[1].toString().isNotEmpty)
-        ? sTag[1]
-        : 'unknown';
+    final statusValueRaw = (sTag != null && sTag.length > 1 && sTag[1].toString().isNotEmpty)
+        ? sTag[1].toString()
+        : 'unknown';
+    final statusValue = _normalizeStatus(statusValueRaw);

52-56: Optionally enforce z=dispute to drop misclassified events early

PR objective states we fetch events with z=dispute. Enforcing this in the parser prevents accidental ingestion if upstream filters regress.

If you want to enforce:

-    // Optionally verify 'z' tag indicates dispute; do not throw if absent
-    event.tags?.firstWhere(
-      (tag) => tag.isNotEmpty && tag[0] == 'z' && tag.length > 1 && tag[1] == 'dispute',
-      orElse: () => [],
-    );
+    final isDispute = (event.tags ?? const <List<dynamic>>[])
+        .any((tag) => tag.isNotEmpty && tag[0] == 'z' && tag.length > 1 && tag[1] == 'dispute');
+    if (!isDispute) {
+      throw ArgumentError('DisputeEvent requires z=dispute tag');
+    }

Would you like this stricter behavior, or keep it permissive?


107-110: Minor: include orderId in toString for easier diagnostics

This helps during logs while resolving "Unknown Order ID" cases from the repository.

-    return 'DisputeEvent(id: $id, disputeId: $disputeId, status: $status, createdAt: $createdAt)';
+    return 'DisputeEvent(id: $id, disputeId: $disputeId, status: $status, createdAt: $createdAt, orderId: ${orderId ?? 'null'})';
lib/features/disputes/widgets/dispute_icon.dart (1)

11-15: Use const where possible

Icon can be const for better performance.

-      child: Icon(
+      child: const Icon(
         Icons.warning_amber,
         color: Colors.amber,
         size: 32,
       ),
lib/features/disputes/widgets/dispute_order_id.dart (1)

11-16: Make order IDs resilient in tight layouts and improve accessibility

Long IDs can overflow on narrow screens, and screen readers benefit from a labeled value. Below is the pattern you should apply across the codebase—not just in dispute_order_id.dart—to ensure consistency.

 import 'package:flutter/material.dart';
+import 'package:mostro_mobile/generated/l10n.dart';

   @override
   Widget build(BuildContext context) {
-    return Text(
-      orderId,
-      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-            fontWeight: FontWeight.w500,
-          ),
-    );
+    return Semantics(
+      label: S.of(context).disputeOrderIdSemantics(orderId),
+      child: Text(
+        orderId,
+        maxLines: 1,
+        overflow: TextOverflow.ellipsis,
+        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+              fontWeight: FontWeight.w500,
+            ),
+      ),
+    );
   }

You’ll need to add this ARB entry:

  • disputeOrderIdSemantics: "Order ID: {id}" (with placeholder {id}).

Please apply the same pattern wherever an order ID is displayed without overflow handling:

lib/shared/widgets/order_cards.dart
Expanded(child: Text(orderId, …)) → wrap in Semantics, add maxLines: 1 and overflow: TextOverflow.ellipsis.

lib/features/chat/widgets/trade_information_tab.dart (line 74)
Text(orderId, …) → wrap in Semantics and add truncation props.

lib/features/disputes/widgets/dispute_order_id.dart
As shown above.

lib/features/trades/widgets/mostro_message_detail_widget.dart
We didn’t find a direct Text(orderId) in a quick scan—but please double-check any place this ID is rendered in a Text widget and apply the same treatment.

lib/features/disputes/widgets/dispute_list_item.dart (1)

18-43: Use Material+InkWell for ripple, focus, and better a11y; make DisputeIcon const

GestureDetector works but lacks Material semantics/feedback. InkWell inside a Material improves UX and accessibility, and const on DisputeIcon reduces rebuild cost.

-    return GestureDetector(
-      onTap: onTap,
-      child: Container(
-        decoration: BoxDecoration(
-          border: Border(
-            bottom: BorderSide(
-              color: Colors.white.withValues(alpha: 0.05),
-              width: 1.0,
-            ),
-          ),
-        ),
-        child: Padding(
-          padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
-          child: Row(
-            crossAxisAlignment: CrossAxisAlignment.start,
-            children: [
-              DisputeIcon(),
-              const SizedBox(width: 16),
-              Expanded(
-                child: DisputeContent(dispute: dispute),
-              ),
-            ],
-          ),
-        ),
-      ),
-    );
+    return Material(
+      color: Colors.transparent,
+      child: InkWell(
+        onTap: onTap,
+        child: Container(
+          decoration: BoxDecoration(
+            border: Border(
+              bottom: BorderSide(
+                color: Colors.white.withValues(alpha: 0.05),
+                width: 1.0,
+              ),
+            ),
+          ),
+          padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              const DisputeIcon(),
+              const SizedBox(width: 16),
+              Expanded(
+                child: DisputeContent(dispute: dispute),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );

Nit: if AppTheme exposes a divider token, prefer that over hardcoded Colors.white.withValues(alpha: 0.05).

lib/features/disputes/widgets/dispute_status_content.dart (2)

16-16: Broaden resolved-status detection and avoid repeated toLowerCase() calls.

Support all known synonyms from the PR (“resolved”, “solved”) and compute lowercase once to avoid duplicating calls.

Apply:

-    bool isResolved = dispute.status.toLowerCase() == 'resolved' || dispute.status.toLowerCase() == 'closed';
+    final status = dispute.status.toLowerCase();
+    const resolvedStatuses = {'resolved', 'solved', 'closed', 'settled'};
+    final isResolved = resolvedStatuses.contains(status);

20-29: Align resolved-state colors with the badge’s settled palette.

The PR maps resolved/solved to the “settled” (blue) palette. This container uses Mostro green, which may read as “success” instead of “settled”. Prefer the same tokens the badge uses (e.g., statusSettledBackground/Text) for consistency.

If available in AppTheme, consider:

-          color: AppTheme.mostroGreen.withValues(alpha: 0.1),
+          color: AppTheme.statusSettledBackground.withValues(alpha: 0.1),
...
-            color: AppTheme.mostroGreen.withValues(alpha: 0.3),
+            color: AppTheme.statusSettledBackground.withValues(alpha: 0.3),
...
-              color: AppTheme.mostroGreen,
+              color: AppTheme.statusSettledText,
...
-                    style: TextStyle(
-                      color: AppTheme.mostroGreen,
+                    style: TextStyle(
+                      color: AppTheme.statusSettledText,

Please confirm the exact token names in AppTheme.

Also applies to: 45-50, 53-59

lib/features/disputes/widgets/disputes_list.dart (1)

97-103: Avoid exposing raw error details to end users.

Rendering error.toString() can leak internal details. Show a friendly message and log the error instead.

Apply:

-            Text(
-              error.toString(),
-              style: TextStyle(
-                color: AppTheme.textSecondary,
-                fontSize: 14,
-              ),
-              textAlign: TextAlign.center,
-            ),
+            // Consider logging `error` & `stack` via your logger instead of showing it.
+            const SizedBox.shrink(),

If you still want context, consider a “Show details” expandable panel gated behind a debug flag.

lib/features/disputes/widgets/dispute_input_section.dart (2)

29-57: Good async handling and mounted checks. Consider dismissing the keyboard after send.

The loading guard and mounted checks look solid. UX nit: dismiss the keyboard after a successful send.

Apply:

       await notifier.sendMessage(message);
       _messageController.clear();
+      if (mounted) {
+        FocusScope.of(context).unfocus();
+      }

63-71: Prefer theme tokens over raw white for dividers.

Minor consistency: if AppTheme defines a divider or surface-variant color, prefer that over Colors.white.withValues(alpha: 0.05).

Example:

-          top: BorderSide(
-            color: Colors.white.withValues(alpha: 0.05),
+          top: BorderSide(
+            color: AppTheme.divider, // verify or pick the closest token
             width: 1.0,
           ),
lib/services/dispute_service.dart (2)

118-146: Handle additional status synonyms (“solved”, “closed”, “settled”).

PR notes include “resolved/solved”. Add these to map to adminSettled to keep behavior consistent across the app.

Apply:

-      case 'resolved':
+      case 'resolved':
+      case 'solved':
+      case 'closed':
+      case 'settled':
         action = mostro.Action.adminSettled;
         _logger.info('Admin settled dispute for order $orderId');
         break;

64-77: Minor logging fix: pass error once; avoid duplicating it in the message and the error field.

Logger.severe(message, error, stackTrace) already captures error; including $e in the message is redundant.

Apply:

-    } catch (e, stackTrace) {
-      _logger.severe('Error handling dispute event: $e', e, stackTrace);
+    } catch (e, stackTrace) {
+      _logger.severe('Error handling dispute event', e, stackTrace);
     }

Repeat similarly for other severe() calls if desired.

lib/features/disputes/providers/dispute_providers.dart (5)

17-20: Use ref.watch to propagate repository re-creation to consumers.

Using read will not rebuild if the repository changes. Prefer watch for dependencies within providers.

-final userDisputesProvider = FutureProvider<List<Dispute>>((ref) async {
-  final repository = ref.read(disputeRepositoryProvider);
+final userDisputesProvider = FutureProvider<List<Dispute>>((ref) async {
+  final repository = ref.watch(disputeRepositoryProvider);
   return repository.fetchUserDisputes();
 });

23-26: Same here: prefer watch over read.

-final disputeDetailsProvider = FutureProvider.family<Dispute?, String>((ref, disputeId) async {
-  final repository = ref.read(disputeRepositoryProvider);
+final disputeDetailsProvider = FutureProvider.family<Dispute?, String>((ref, disputeId) async {
+  final repository = ref.watch(disputeRepositoryProvider);
   return repository.getDisputeDetails(disputeId);
 });

28-33: Stream should probably be scoped per dispute to avoid over-subscribing.

If subscribeToDisputeEvents() emits all disputes, make this a StreamProvider.family(disputeId) with filtering to reduce bandwidth and rebuilds. If the repository already scopes by user, consider documenting it.


35-38: watch for repository here too.

-final createDisputeProvider = FutureProvider.family<bool, String>((ref, orderId) async {
-  final repository = ref.read(disputeRepositoryProvider);
+final createDisputeProvider = FutureProvider.family<bool, String>((ref, orderId) async {
+  final repository = ref.watch(disputeRepositoryProvider);
   return repository.createDispute(orderId);
 });

8-14: Avoid passing Ref into repositories to keep data layer UI-framework-agnostic.

Inject concrete dependencies (nostrService, mostroPubkey, any callbacks) instead of ref. This improves testability and reusability across DI frameworks.

lib/features/disputes/widgets/dispute_communication_section.dart (2)

226-233: Avoid leaking raw error details to end-users.

Showing error.toString() can expose internal info. Prefer a generic localized hint and log details instead.

-          Text(
-            error.toString(),
-            style: TextStyle(
-              color: AppTheme.textSecondary,
-              fontSize: 14,
-            ),
-            textAlign: TextAlign.center,
-          ),
+          // Consider logging the error instead of showing internals
+          Text(
+            S.of(context)!.tryAgainLater,
+            style: TextStyle(
+              color: AppTheme.textSecondary,
+              fontSize: 14,
+            ),
+            textAlign: TextAlign.center,
+          ),

271-285: Theme consistency: prefer AppTheme over hard-coded Colors.amber/white.

To keep visual consistency and theming flexibility, consider moving amber/white colors to AppTheme (e.g., a warning color) and reference them here. No change required if AppTheme lacks equivalents.

lib/features/disputes/notifiers/dispute_chat_notifier.dart (2)

258-263: Add a private helper to cancel subscriptions; reuse in init and dispose.

   @override
   void dispose() {
     _logger.i('Disposing dispute chat notifier for dispute: $disputeId');
-    _chatSubscription?.cancel();
-    _disputeSubscription?.cancel();
+    _cancelSubscriptions();
     super.dispose();
   }
+
+  Future<void> _cancelSubscriptions() async {
+    try {
+      await _chatSubscription?.cancel();
+      await _disputeSubscription?.cancel();
+    } finally {
+      _chatSubscription = null;
+      _disputeSubscription = null;
+    }
+  }

145-159: Deduplicate incoming messages by event id to avoid duplicates from multiple relays.

   void _handleChatMessage(NostrEvent event) {
     try {
       _logger.i('Received chat message for dispute: $disputeId');
       
       final currentState = state.value;
       if (currentState == null) return;
 
-      // TODO: Decrypt gift wrap message
-      // For now, we'll add the raw event to messages
-      final updatedMessages = [...currentState.messages, event];
+      // TODO: Decrypt gift wrap message
+      // Basic dedupe by event id
+      final alreadyExists = currentState.messages.any((m) => m.id == event.id);
+      if (alreadyExists) return;
+      final updatedMessages = [...currentState.messages, event];
lib/data/models/dispute_chat.dart (3)

73-83: Fallback lastMessageAt when message timestamp is missing.

Guarantees recency sorting even when an incoming event lacks createdAt.

   DisputeChat addMessage(NostrEvent message) {
     final updatedMessages = [...messages, message];
     final messageTime = message.createdAt;
-    final newLastMessageAt = messageTime;
+    final newLastMessageAt = messageTime ?? DateTime.now();
     
     return copyWith(
       messages: updatedMessages,
       lastMessageAt: newLastMessageAt,
     );
   }

106-123: Equality ignores messages and timestamps; this can mask state changes.

Overriding == without considering messages can lead to subtle bugs in collections/tests. Either remove ==/hashCode or incorporate fields that change with new messages (e.g., messages.length, lastMessageAt).

   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     return other is DisputeChat &&
         other.disputeId == disputeId &&
         other.adminPubkey == adminPubkey &&
         other.userPubkey == userPubkey &&
-        other.disputeToken == disputeToken;
+        other.disputeToken == disputeToken &&
+        other.messages.length == messages.length &&
+        other.lastMessageAt == lastMessageAt;
   }
 
   @override
   int get hashCode => Object.hash(
         disputeId,
         adminPubkey,
         userPubkey,
-        disputeToken,
+        disputeToken,
+        messages.length,
+        lastMessageAt,
       );

23-44: Token verification heuristic is OK; consider strict token matching.

If tokens can appear as substrings of other content, prefer strict matching (e.g., word boundary or a dedicated tag/format).

lib/features/disputes/screens/dispute_chat_screen.dart (2)

70-102: Avoid re-watching the same disputeDetailsProvider in the enhanced lookup.

You already have dispute from the top-level watch of disputeDetailsProvider(widget.disputeId). Re-watching the same provider here can cause unnecessary rebuilds. Prefer computing the “enhanced” data in the repository or expose a single provider that resolves orderId internally, or just use the first value and let it refresh when available.


70-121: Nested scrolling can be heavy with the chat ListView inside a SingleChildScrollView.

Given DisputeCommunicationSection contains a ListView, consider:

  • Removing the outer SingleChildScrollView and using a CustomScrollView with slivers.
  • Or making the inner ListView non-scrollable (shrinkWrap + NeverScrollableScrollPhysics) and letting the outer scroll handle it.

This avoids expensive nested scroll computations and constraints issues.

lib/data/repositories/dispute_repository.dart (5)

73-85: Avoid per-dispute network calls (N+1) when resolving status.

Calling _getDisputeStatus inside the loop issues one fetch per dispute. This will not scale and will slow down the list for users with many disputes. Prefer a single query that fetches all 38383 events for the set of disputeIds and then reduce in-memory.

Apply this sketch to batch the fetch:

- for (final message in allMessages) {
+ final toResolve = <String>{};
+ for (final message in allMessages) {
    ...
-   if (message.payload is Dispute) {
-     final dispute = message.payload as Dispute;
-     final status = await _getDisputeStatus(dispute.disputeId);
+   if (message.payload is Dispute) {
+     final dispute = message.payload as Dispute;
+     toResolve.add(dispute.disputeId);
      ...
   }
- }
+ }
+
+ // Batch-fetch latest statuses for all disputes
+ final latestStatuses = await _getLatestStatuses(toResolve);
+ 
+ // then when building disputeMap:
+ final status = latestStatuses[dispute.disputeId] ?? 'initiated';

Add a private _getLatestStatuses(Set<String> ids) that performs one filter with multiple '#d' values and returns a Map<String,String>.


85-101: Use event timestamps instead of DateTime.now() for admin assignment.

adminTookAt should reflect the event time that Mostro emitted, not the local device time. If the message object carries a timestamp, prefer it; otherwise, derive from the source event.

- adminTookAt: DateTime.now(),
+ adminTookAt: message.createdAt ?? DateTime.now(),

If message.createdAt isn’t available, consider looking up the corresponding 38383 event for that dispute and use its created_at.


339-349: Heuristic fallback can mis-assign disputes when multiple orders exist.

Defaulting to the single active order is risky. At minimum, guard behind a feature flag or log a structured warning that includes the disputeId and the candidate order IDs.

Consider returning null instead of making an assumption, and let the UI show “Unknown Order ID” with a help CTA.


409-454: Don’t rethrow inside the stream map; drop unparseable events instead.

rethrow will surface as Stream.error, potentially collapsing the UI stream. Prefer filtering out bad events and logging.

- return _nostrService.subscribeToEvents(request).map((event) {
+ return _nostrService.subscribeToEvents(request).map((event) {
    try {
      ...
-   } catch (e) {
-     _logger.w('Failed to parse DisputeEvent for ${event.id}: $e');
-     // Fallback...
-     try {
-       ...
-     } catch (e2) {
-       _logger.w('Fallback parse also failed for ${event.id}: $e2');
-       rethrow;
-     }
-   }
- }).handleError((error) {
-   _logger.e('Error in dispute events stream: $error');
- });
+   } catch (e) {
+     _logger.w('Dropping unparsable dispute event ${event.id}: $e');
+     return null;
+   }
+ }).where((d) => d != null).cast<Dispute>();

Optionally, take a since argument so callers can resume from the last known timestamp instead of DateTime.now().


23-50: Debug logging prints entire event content.

Public 38383 events are fine, but keep logs lean; avoid logging full content arrays if they may later include user-provided text.

Replace with IDs, kind, created_at, and the ‘d’ and ‘s’ tags only.

lib/data/models/dispute.dart (2)

155-161: Handle seconds vs milliseconds in created_at.

Some payloads encode seconds. Guard for both.

- if (timestamp is int) {
-   createdAt = DateTime.fromMillisecondsSinceEpoch(timestamp);
- }
+ if (timestamp is int) {
+   final ms = timestamp > 1000000000000 ? timestamp : timestamp * 1000;
+   createdAt = DateTime.fromMillisecondsSinceEpoch(ms);
+ }

Same comment applies to admin_took_at.


300-318: Robust createdAt conversion in legacy factory.

disputeEvent.createdAt may be a DateTime. Current code treats non-int as “now”, losing fidelity.

createdAt: DateTime.fromMillisecondsSinceEpoch(
-  disputeEvent.createdAt is int 
-    ? disputeEvent.createdAt * 1000 
-    : DateTime.now().millisecondsSinceEpoch
+  disputeEvent.createdAt is int
+    ? disputeEvent.createdAt * 1000
+    : (disputeEvent.createdAt is DateTime
+        ? (disputeEvent.createdAt as DateTime).millisecondsSinceEpoch
+        : DateTime.now().millisecondsSinceEpoch)
),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9a0cc73 and 240b48d.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • lib/core/app_routes.dart (2 hunks)
  • lib/core/config.dart (1 hunks)
  • lib/data/models/dispute.dart (2 hunks)
  • lib/data/models/dispute_chat.dart (1 hunks)
  • lib/data/models/dispute_event.dart (1 hunks)
  • lib/data/repositories/dispute_repository.dart (1 hunks)
  • lib/features/chat/screens/chat_rooms_list.dart (5 hunks)
  • lib/features/chat/widgets/trade_information_tab.dart (1 hunks)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
  • lib/features/disputes/providers/dispute_providers.dart (1 hunks)
  • lib/features/disputes/screens/dispute_chat_screen.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_communication_section.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_content.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_description.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_header.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_icon.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_info_card.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_input_section.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_list_item.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_order_id.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_status_badge.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_status_content.dart (1 hunks)
  • lib/features/disputes/widgets/disputes_list.dart (1 hunks)
  • lib/features/order/notfiers/abstract_mostro_notifier.dart (1 hunks)
  • lib/features/order/screens/take_order_screen.dart (1 hunks)
  • lib/features/trades/screens/trade_detail_screen.dart (3 hunks)
  • lib/l10n/intl_en.arb (3 hunks)
  • lib/l10n/intl_es.arb (3 hunks)
  • lib/l10n/intl_it.arb (3 hunks)
  • lib/services/dispute_service.dart (1 hunks)
  • lib/services/mostro_service.dart (1 hunks)
  • lib/shared/notifiers/session_notifier.dart (1 hunks)
  • lib/shared/widgets/order_cards.dart (1 hunks)
  • pubspec.yaml (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use S.of(context) for all user-facing strings (no hardcoded UI text)
Target zero flutter analyze issues; remove unused imports and use latest non-deprecated APIs (e.g., withValues() over withOpacity())
Check mounted before using BuildContext after async gaps
Prefer const constructors and const widgets where possible

Files:

  • lib/features/disputes/widgets/dispute_icon.dart
  • lib/features/disputes/widgets/dispute_content.dart
  • lib/shared/notifiers/session_notifier.dart
  • lib/services/mostro_service.dart
  • lib/features/order/screens/take_order_screen.dart
  • lib/shared/widgets/order_cards.dart
  • lib/features/disputes/providers/dispute_providers.dart
  • lib/features/chat/screens/chat_rooms_list.dart
  • lib/features/order/notfiers/abstract_mostro_notifier.dart
  • lib/features/disputes/widgets/disputes_list.dart
  • lib/features/disputes/widgets/dispute_list_item.dart
  • lib/features/disputes/widgets/dispute_communication_section.dart
  • lib/features/disputes/widgets/dispute_description.dart
  • lib/core/app_routes.dart
  • lib/features/disputes/widgets/dispute_info_card.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
  • lib/features/disputes/widgets/dispute_header.dart
  • lib/features/disputes/widgets/dispute_status_badge.dart
  • lib/features/disputes/widgets/dispute_order_id.dart
  • lib/features/chat/widgets/trade_information_tab.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/features/disputes/widgets/dispute_status_content.dart
  • lib/data/models/dispute_event.dart
  • lib/data/repositories/dispute_repository.dart
  • lib/features/disputes/widgets/dispute_input_section.dart
  • lib/services/dispute_service.dart
  • lib/core/config.dart
  • lib/data/models/dispute_chat.dart
  • lib/data/models/dispute.dart
  • lib/features/disputes/screens/dispute_chat_screen.dart
pubspec.yaml

📄 CodeRabbit inference engine (CLAUDE.md)

Remove unused dependencies from pubspec.yaml

Files:

  • pubspec.yaml
lib/features/**/providers/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Riverpod providers by feature under features/{feature}/providers/

Files:

  • lib/features/disputes/providers/dispute_providers.dart
lib/core/app_routes.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Configure app navigation using GoRouter in core/app_routes.dart

Files:

  • lib/core/app_routes.dart
lib/data/repositories/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Use the Repository pattern for all data access in data/repositories/

Files:

  • lib/data/repositories/dispute_repository.dart
lib/l10n/intl_{en,es,it}.arb

📄 CodeRabbit inference engine (CLAUDE.md)

Add new localization keys to all ARB files (en, es, it)

Files:

  • lib/l10n/intl_es.arb
  • lib/l10n/intl_it.arb
  • lib/l10n/intl_en.arb
lib/l10n/*.arb

📄 CodeRabbit inference engine (CLAUDE.md)

After modifying ARB files, regenerate localization (flutter gen-l10n or build_runner as configured)

Files:

  • lib/l10n/intl_es.arb
  • lib/l10n/intl_it.arb
  • lib/l10n/intl_en.arb
🧠 Learnings (6)
📚 Learning: 2025-08-24T16:01:53.107Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.107Z
Learning: Applies to lib/features/**/providers/**/*.dart : Organize Riverpod providers by feature under features/{feature}/providers/

Applied to files:

  • lib/features/disputes/providers/dispute_providers.dart
📚 Learning: 2025-05-06T15:49:26.443Z
Learnt from: chebizarro
PR: MostroP2P/mobile#74
File: lib/services/mostro_service.dart:70-76
Timestamp: 2025-05-06T15:49:26.443Z
Learning: In the Mostro Mobile codebase, Riverpod code generation is used with `Riverpod` annotations. Providers like `eventStorageProvider` are generated in `.g.dart` files from annotated functions in the main provider files. These providers are accessible by importing the main provider file (e.g., `mostro_service_provider.dart`), not by importing a separate provider file.

Applied to files:

  • lib/features/disputes/providers/dispute_providers.dart
📚 Learning: 2025-08-24T16:01:53.107Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.107Z
Learning: Applies to lib/core/app_routes.dart : Configure app navigation using GoRouter in core/app_routes.dart

Applied to files:

  • lib/core/app_routes.dart
📚 Learning: 2025-08-24T16:01:53.107Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.107Z
Learning: Applies to lib/**/*.dart : Use S.of(context) for all user-facing strings (no hardcoded UI text)

Applied to files:

  • lib/features/chat/widgets/trade_information_tab.dart
📚 Learning: 2025-08-24T16:01:53.107Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.107Z
Learning: Applies to lib/data/repositories/**/*.dart : Use the Repository pattern for all data access in data/repositories/

Applied to files:

  • lib/data/repositories/dispute_repository.dart
📚 Learning: 2025-08-24T16:01:53.107Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.107Z
Learning: Applies to lib/l10n/intl_{en,es,it}.arb : Add new localization keys to all ARB files (en, es, it)

Applied to files:

  • lib/l10n/intl_it.arb
🪛 RuboCop (1.76.1)
lib/l10n/intl_es.arb

[warning] 867-867: Duplicated key in hash literal.

(Lint/DuplicateHashKey)


[warning] 878-878: Duplicated key in hash literal.

(Lint/DuplicateHashKey)

lib/l10n/intl_it.arb

[warning] 730-730: Duplicated key in hash literal.

(Lint/DuplicateHashKey)


[warning] 901-901: Duplicated key in hash literal.

(Lint/DuplicateHashKey)


[warning] 912-912: Duplicated key in hash literal.

(Lint/DuplicateHashKey)

lib/l10n/intl_en.arb

[warning] 828-828: Duplicated key in hash literal.

(Lint/DuplicateHashKey)


[warning] 839-839: Duplicated key in hash literal.

(Lint/DuplicateHashKey)

🔇 Additional comments (25)
pubspec.yaml (1)

54-54: The package:logging import in lib/services/dispute_service.dart is indeed used, so we cannot simply drop the logging dependency without breaking that file. Before removing it from pubspec.yaml, address the existing usage:

• Either refactor dispute_service.dart to use your preferred logger package (e.g., replace package:logging imports, Logger instantiation, and calls to log.* with logger.*), then safely remove logging: ^1.2.0 from pubspec.yaml.
• Or, if that refactor isn’t intended now, keep the logging dependency to satisfy the existing code.

Next steps:

  1. Review lib/services/dispute_service.dart and decide if it should be migrated to the logger package.
  2. If migrating, update imports and usage in that file, verify no other files depend on logging, then remove the entry from pubspec.yaml.
  3. If not migrating immediately, leave logging in place until the refactor can be done.
lib/core/config.dart (1)

12-15: I've requested review of the settings persistence logic to confirm that outdated persisted mostroPublicKey values won’t override the new default at runtime. This will ensure safe rollout of the updated key without requiring manual user resets.

I’ll examine settings_notifier.dart and any storage-related code (e.g., SharedPreferences or equivalent) to verify how persisted settings are loaded and whether they merge or replace defaults correctly.

lib/data/models/dispute_event.dart (1)

75-82: Defensive: don’t bang event.id!; throw a precise error if missing

A few Nostr libraries treat id as nullable during construction or parsing. Prefer an explicit check and error.

-    return DisputeEvent(
-      id: event.id!,
+    final evId = event.id;
+    if (evId == null || evId.isEmpty) {
+      throw ArgumentError('Event must have a non-empty id for DisputeEvent');
+    }
+    return DisputeEvent(
+      id: evId,
       disputeId: dTag[1],
       status: statusValue,
       createdAt: timestamp,
       pubkey: event.pubkey,
       orderId: orderId,
     );
⛔ Skipped due to learnings
Learnt from: chebizarro
PR: MostroP2P/mobile#74
File: lib/notifications/notification_service.dart:54-59
Timestamp: 2025-05-08T16:31:29.582Z
Learning: In the Nostr protocol, event.id will never be null in events returned by relay subscriptions, so null safety checks for this property are unnecessary.
lib/l10n/intl_es.arb (4)

652-681: New dispute strings look consistent and idiomatic

Wording, placeholders, and tone are consistent with the existing Spanish localization.


838-839: Consistency improvement acknowledged

Using a simple, user-friendly past-tense message for order cancellation aligns with other timeout/cancel messages.


864-890: Ignore duplicate-key removal—no duplicates detected in intl_es.arb

I ran the provided duplicate-detection script across all ARB locales and it reported zero duplicate keys in lib/l10n/intl_es.arb (and in the other locale files). The keys “retry” and “orderIdLabel” each only appear once in the Spanish file, so there’s nothing to remove here.

Likely an incorrect or invalid review comment.


701-711: Key rename verification complete – ensure numeric-only first argument

I’ve confirmed that there are no remaining calls to the old forAmount key and all three call sites now use forAmountWithCurrency:

  • lib/shared/widgets/order_cards.dart (line 58)
  • lib/features/chat/widgets/trade_information_tab.dart (line 135)
  • lib/features/order/screens/take_order_screen.dart (line 158)

Please double-check that each amountString (and similarly passed values in your other two files) contains only the numeric amount (e.g. "100.00") and not a composite like "100.00 USD". If any include extra text, split them so the {amount} placeholder stays pure digits and the {currency} placeholder receives the currency code.

lib/features/order/notfiers/abstract_mostro_notifier.dart (1)

113-118: Use disputeToken preferentially: LGTM; verify l10n placeholders and PII considerations

Good change using dispute.disputeToken ?? dispute.disputeId. Confirm:

  • l10n strings for both actions include {user_token} (ES does).
  • The token is safe to surface in notifications (not a secret beyond dispute participants).

If there’s any locale still using {dispute_id} in these messages, update them to {user_token}.

Also applies to: 120-125

lib/core/app_routes.dart (1)

147-157: New dispute route wiring looks correct

/dispute_details/:disputeId renders DisputeChatScreen with the provided parameter and uses the default transition inside the Shell. This integrates with the listeners as intended.

lib/features/disputes/widgets/dispute_info_card.dart (1)

21-23: [msg]

lib/features/chat/screens/chat_rooms_list.dart (4)

60-63: LGTM: switched to withValues()

Using Color.withValues(alpha: …) aligns with “latest non-deprecated APIs” in the guidelines.


88-91: LGTM: consistent divider styling

Repeating the withValues() pattern keeps visual consistency across sections.


110-112: Good integration: DisputesList is now wired into the Disputes tab

Using a dedicated const DisputesList keeps concerns separated and will simplify future maintenance/testing.


94-99: Centralizing tab descriptions via helper is a good move

This improves readability (single source for the dynamic header text) and makes it easy to extend when tabs change.

lib/features/disputes/widgets/dispute_content.dart (1)

7-26: Clean composition; spacing and alignment look good

The widget composes header, order, and description logically with minimal coupling. SizedBox spacers are const. Nothing blocking here.

lib/services/dispute_service.dart (1)

101-106: Verify availability of ref.exists(...) in your Riverpod version.

Not all Riverpod versions expose ref.exists for this check. If unavailable, consider an alternative (e.g., checking against an orders registry provider, or guarding reads with a selector).

If needed, a safe fallback is to skip the existence check and let handleEvent be a no-op for unknown orders after verifying notifier.mounted, or maintain a dedicated provider that tracks known order IDs and consult that instead:

final knownOrders = _ref.read(ordersIndexProvider); // Set<String>
if (!knownOrders.contains(orderId)) {
  _logger.info('No order notifier found for order $orderId');
  return;
}
lib/features/disputes/providers/dispute_providers.dart (1)

1-5: Good placement and separation of providers by feature.

Providers are correctly organized under features/disputes/providers, matching the project convention.

lib/data/repositories/dispute_repository.dart (1)

150-206: Repository pattern and error handling look solid.

Constructor injection, scoped providers, and catch blocks are consistent with the existing architecture. Nice guardrails around missing sessions and private keys.

lib/data/models/dispute.dart (2)

333-348: Status-to-description coupling is brittle and duplicates i18n.

The switch includes settled and seller-refunded, but your status badges use resolved/solved. Move this mapping to UI with localization and maintain a single canonical mapping of status strings.

Would you like me to produce a shared DisputeStatus enum + mapper used by both badges and descriptions to eliminate drift?


179-202: Model extensions look good and consistent.

copyWith, equality, hashCode, and toJson are coherent and cover new fields.

lib/l10n/intl_en.arb (3)

621-646: Nice coverage for dispute UI text.

The new dispute keys and placeholders look coherent and adequate for the new screens.


801-814: Admin chat copy looks good.

Labels and helper strings will map well to the communication section.


670-680: All translation callsites updated; no remaining forAmount usages

No instances of the old forAmount key were found in the codebase, and all existing references now correctly call forAmountWithCurrency. You can consider removing the unused forAmount entry from intl_en.arb, but there are no outstanding callsite updates needed.

lib/l10n/intl_it.arb (2)

681-706: Good Italian translations for dispute flows.

Coverage looks complete for the new screens and instructions.


874-887: Admin chat strings: solid.

Wording aligns with the UX expectations for assignment and token verification.

Copy link
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cuando el administrador toma la disputa, aunque aparece en el SnackBar el mensaje admin-took-dispute y la disputa se muestra como in-progress, aún le sale al usuario: Esperando asignación de administrador.
Debería avisarle del admin que la tomó

Captura desde 2025-08-25 15-40-45 Captura desde 2025-08-25 15-39-54

Copy link
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 hora despues volvi a abrir la app y dice "Disputa no encontrada", estos son los logs

┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ #0   DisputeRepository.getDisputeDetails (package:mostro_mobile/data/repositories/dispute_repository.dart:211:15)
│ #1   disputeDetailsProvider.<anonymous closure> (package:mostro_mobile/features/disputes/providers/dispute_providers.dart:25:21)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ 🐛 Fetching dispute details for: 8dcf1e97-6516-4bc1-bdbb-426630efeebd
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ #0   DisputeRepository.getDisputeDetails (package:mostro_mobile/data/repositories/dispute_repository.dart:218:15)
│ #1   disputeDetailsProvider.<anonymous closure> (package:mostro_mobile/features/disputes/providers/dispute_providers.dart:25:21)
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ 🐛 User sessions: 1, orderIds: {2ee421e0-6013-4379-81c9-a2000e0628cc}, pubkeys: [ddb5dbd6bbda11234ba832f6be1ea0629ca1b6fae9ffc3a9faae06b0a8f639c5]
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ #0   DisputeRepository.getDisputeDetails (package:mostro_mobile/data/repositories/dispute_repository.dart:264:15)
│ #1   <asynchronous suspension>
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ 🐛 Searching for disputeId 8dcf1e97-6516-4bc1-bdbb-426630efeebd in 0 DMs and 5 encrypted DMs
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ #0   DisputeRepository.getDisputeDetails (package:mostro_mobile/data/repositories/dispute_repository.dart:307:17)
│ #1   <asynchronous suspension>
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ 🐛 Searching encrypted DMs for dispute 8dcf1e97-6516-4bc1-bdbb-426630efeebd
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ #0   DisputeRepository.getDisputeDetails (package:mostro_mobile/data/repositories/dispute_repository.dart:404:15)
│ #1   <asynchronous suspension>
├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│ ⛔ Failed to get dispute details: Concurrent modification during iteration: Instance(length:6) of '_GrowableList'.
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Captura desde 2025-08-25 16-56-44

Copy link
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Si el admin resuelve la disputa, aunqu een la UI se muestra Dispute resolved, sale la etiqueta de Iniciada
image
  1. Al abrir la disputa dice "Esta disputa fue abierta contra ti por pubkey del admin que tomo la disputa" pero fue el usuario quien abrio la disputa.
Captura desde 2025-08-25 17-03-18
  1. Al final dice "Esperando asignacion del administrador" pero como ya se resolvio, debe decir el veredicto del admin si fua cancelada o completada la orden
image

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
lib/l10n/intl_en.arb (3)

684-687: Fix duplicate key: createdOn appears twice (conflicts with line ~299).

Keep the earlier "createdOn": "Created on" entry and remove this later colon-suffixed variant to prevent l10n generation failures.

Apply this diff:

-  "createdOn": "Created on:",

756-758: Fix duplicate key: proofOfWork is already defined earlier (line ~360).

Remove the later duplicate under “About Screen New Strings”.

-  "proofOfWork": "Proof of Work",

932-954: Fix duplicate and malformed entries in intl_en.arb. Duplicate keys found (createdOn, orderIdLabel, proofOfWork, retry, yourSharedKey) and a JSON syntax error must be resolved—remove duplicates, add the missing comma(s) so the file parses, then run flutter gen-l10n and verify that intl_it.arb and intl_es.arb include all the new keys.

lib/l10n/intl_it.arb (3)

741-744: Fix duplicate key: createdOn appears twice (earlier at line ~262).

Remove this later colon-suffixed variant.

-  "createdOn": "Creato il:",

635-639: Remove duplicate action buttons (already defined at lines ~541–546).

Keeping both copies breaks ARB parsing.

-  "cancelPendingButton": "CANCELLAZIONE IN ATTESA",
-  "acceptCancelButton": "ANNULLA",
-  "completePurchaseButton": "COMPLETA ACQUISTO",
-  "rateButton": "VALUTA",
-  "contactButton": "CONTATTA",

792-792: Fix duplicate key: proofOfWork is already defined earlier (line ~323).

-  "proofOfWork": "Prova di Lavoro",
lib/services/mostro_service.dart (1)

227-228: Avoid logging full outbound payloads (may include invoices/tokens).

Log action/id instead of the full JSON body.

-    _logger.i('Sending DM, Event ID: ${event.id} with payload: ${order.toJson()}');
+    _logger.i('Sending DM. Event ID: ${event.id}, Action: ${order.action}, Target: ${order.id ?? order.requestId}');
lib/l10n/intl_es.arb (1)

1-979: Fix JSON syntax error in intl_es.arb
lib/l10n/intl_es.arb line 956, col 26 – parse error “Expected separator between values.” Correct the malformed JSON (likely a missing comma or bracket), then rerun l10n validation and flutter gen-l10n to ensure metadata entries and critical dispute keys are valid across all locales.

♻️ Duplicate comments (6)
lib/l10n/intl_en.arb (2)

861-861: Remove duplicate key: retry already exists (line ~215).

-  "retry": "Retry",

872-872: Remove duplicate key: orderIdLabel already exists (line ~259).

-  "orderIdLabel": "Order ID",
lib/l10n/intl_it.arb (2)

964-965: Remove duplicate key: retry already exists (line ~215).

-  "retry": "Riprova",

975-976: Remove duplicate key: orderIdLabel already exists (line ~641).

-  "orderIdLabel": "ID Ordine",
lib/services/mostro_service.dart (1)

106-111: Stop logging decrypted message content (sensitive data).

This prints raw private chat/invoices/tokens to logs on parse errors. Log metadata only.

-        _logger.e(
-          'Error parsing message content: ${decryptedEvent.content}',
-          error: e,
-          stackTrace: stackTrace,
-        );
+        _logger.e(
+          'Error parsing message content',
+          error: e,
+          stackTrace: stackTrace,
+        );
lib/data/repositories/dispute_repository.dart (1)

290-327: Decrypt kind-4 DMs before parsing; current JSON decode will fail.

DMs are encrypted; parse only after decryption with session keys.

-      // Search regular DMs
-      for (final event in dmEventsCopy) {
-        try {
-          if (event.content != null && event.content!.isNotEmpty) {
-            final content = jsonDecode(event.content!);
+      // Search regular DMs (kind 4) — decrypt first (NIP-44)
+      for (final event in dmEventsCopy) {
+        for (final session in sessionsCopy) {
+          try {
+            final ciphertext = event.content ?? '';
+            if (ciphertext.isEmpty) continue;
+            final decryptedJson = await NostrUtils.decryptNIP44(
+              ciphertext,
+              session.tradeKey.private,
+              _mostroPubkey,
+            );
+            final content = jsonDecode(decryptedJson);
             if (content is Map<String, dynamic>) {
               final order = content['order'] as Map<String, dynamic>?;
               if (order != null) {
                 final payload = order['payload'] as Map<String, dynamic>?;
                 if (payload != null && payload['dispute'] is List) {
                   final disputeArray = payload['dispute'] as List;
                   _logger.d('Checking DM event: disputeArray=$disputeArray, looking for $disputeId');
                   if (disputeArray.isNotEmpty && disputeArray[0] == disputeId) {
                     foundOrderId = order['id'] as String?;
                     foundAction = order['action'] as String?;
                     // Extract user token from dispute array [disputeId, userToken, peerToken]
                     if (disputeArray.length > 1 && disputeArray[1] != null) {
                       foundToken = disputeArray[1].toString();
                     }
                     _logger.d('✅ Found orderId from DM: $foundOrderId, action: $foundAction, token: $foundToken');
                     break;
                   }
                 } else if (payload != null && payload['dispute'] is String && payload['dispute'] == disputeId) {
                   // Handle case where dispute is a string instead of array
                   foundOrderId = order['id'] as String?;
                   foundAction = order['action'] as String?;
                   _logger.d('✅ Found orderId from DM (string format): $foundOrderId, action: $foundAction');
                   break;
                 } else {
                   _logger.d('DM event payload structure: ${payload?.keys}');
                 }
               }
             }
-          }
-        } catch (e) {
-          _logger.d('Failed to parse DM event: $e');
-        }
+            // success -> stop trying other session keys for this event
+            break;
+          } catch (e) {
+            // try next session key
+            continue;
+          }
+        }
       }
🧹 Nitpick comments (10)
lib/services/mostro_service.dart (2)

45-52: Null-guard NostrEvent id/createdAt before storing.

Defensive check prevents rare NPEs from malformed events.

-    if (await eventStore.hasItem(event.id!)) return;
+    if (event.id == null || event.createdAt == null) {
+      _logger.w('Skipping event with missing id/createdAt.');
+      return;
+    }
+    if (await eventStore.hasItem(event.id!)) return;

45-53: Mark events as processed only after successful parse (optional).

Currently, events are deduped before parsing; a transient parse error causes permanent drop. Consider two-phase bookkeeping (seen vs processed) or putting only after successful decode/store.

Also applies to: 98-105

lib/features/disputes/widgets/dispute_info_card.dart (1)

70-97: Avoid fixed 80px label width for i18n (may truncate in DE/IT/ES).

Consider letting the label flex or using a Table with intrinsic sizing to accommodate longer translations.

lib/features/disputes/widgets/dispute_status_content.dart (1)

83-84: Localize counterparty in bullet text.

-          _buildBulletPoint(S.of(context)!.disputeInstruction4(dispute.counterpartyDisplay)),
+          _buildBulletPoint(S.of(context)!.disputeInstruction4(dispute.counterparty ?? S.of(context)!.unknown)),
lib/features/disputes/providers/dispute_providers.dart (2)

10-13: Narrow rebuilds with select when reading settings.

Only rebuild when Mostro pubkey changes.

-  final settings = ref.watch(settingsProvider);
-  final mostroPubkey = settings.mostroPublicKey;
+  final mostroPubkey = ref.watch(settingsProvider.select((s) => s.mostroPublicKey));

16-19: Remove no-op onDispose block.

Keeps provider minimal until cleanup is needed.

-  // Register cleanup when provider is disposed
-  ref.onDispose(() {
-    // No explicit cleanup method in DisputeRepository, but we can add null safety
-    // for future implementations
-  });
+  // No explicit cleanup needed at the moment.
lib/features/disputes/widgets/dispute_communication_section.dart (1)

304-311: Don’t expose raw error details to users.

Show a friendly message; log details elsewhere.

-          Text(
-            error.toString(),
-            style: TextStyle(
-              color: AppTheme.textSecondary,
-              fontSize: 14,
-            ),
-            textAlign: TextAlign.center,
-          ),
+          Text(
+            S.of(context)!.tryAgainLater,
+            style: TextStyle(
+              color: AppTheme.textSecondary,
+              fontSize: 14,
+            ),
+            textAlign: TextAlign.center,
+          ),

Note: ensure tryAgainLater exists in ARB.

lib/data/repositories/dispute_repository.dart (1)

115-124: Potential N+1 event fetches per dispute; consider batching by IDs.

Collect disputeIds then fetch a single 38383 query with multiple #d values to reduce latency.

lib/data/models/dispute.dart (2)

22-26: Avoid sentinel strings that can surface in UI.

DisputeSemanticKeys values may render literally; prefer nulls and handle localization in widgets.


470-475: Mark “Display” getters as deprecated to discourage UI use.

Widgets should localize unknowns instead of relying on model placeholders.

-  /// Convenience getter for orderId with fallback
-  String get orderIdDisplay => orderId ?? DisputeSemanticKeys.unknownOrderId;
+  /// Convenience getter for orderId with fallback
+  @Deprecated('Avoid in UI; prefer (orderId ?? localizedUnknown) in widgets.')
+  String get orderIdDisplay => orderId ?? DisputeSemanticKeys.unknownOrderId;
   
-  /// Convenience getter for counterparty with fallback
-  String get counterpartyDisplay => counterparty ?? DisputeSemanticKeys.unknownCounterparty;
+  /// Convenience getter for counterparty with fallback
+  @Deprecated('Avoid in UI; prefer (counterparty ?? localizedUnknown) in widgets.')
+  String get counterpartyDisplay => counterparty ?? DisputeSemanticKeys.unknownCounterparty;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 240b48d and b0bed8c.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • lib/core/config.dart (0 hunks)
  • lib/data/models/dispute.dart (2 hunks)
  • lib/data/models/dispute_event.dart (1 hunks)
  • lib/data/repositories/dispute_repository.dart (1 hunks)
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart (1 hunks)
  • lib/features/disputes/providers/dispute_providers.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_communication_section.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_content.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_description.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_info_card.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_status_badge.dart (1 hunks)
  • lib/features/disputes/widgets/dispute_status_content.dart (1 hunks)
  • lib/l10n/intl_en.arb (3 hunks)
  • lib/l10n/intl_es.arb (4 hunks)
  • lib/l10n/intl_it.arb (4 hunks)
  • lib/services/dispute_service.dart (1 hunks)
  • lib/services/mostro_service.dart (1 hunks)
  • pubspec.yaml (1 hunks)
💤 Files with no reviewable changes (1)
  • lib/core/config.dart
🚧 Files skipped from review as they are similar to previous changes (6)
  • pubspec.yaml
  • lib/services/dispute_service.dart
  • lib/features/disputes/widgets/dispute_description.dart
  • lib/features/disputes/widgets/dispute_content.dart
  • lib/data/models/dispute_event.dart
  • lib/features/disputes/notifiers/dispute_chat_notifier.dart
🧰 Additional context used
📓 Path-based instructions (5)
lib/data/repositories/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Use the Repository pattern for all data access in data/repositories/

Files:

  • lib/data/repositories/dispute_repository.dart
lib/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

lib/**/*.dart: Use S.of(context) for all user-facing strings (no hardcoded UI text)
Target zero flutter analyze issues; remove unused imports and use latest non-deprecated APIs (e.g., withValues() over withOpacity())
Check mounted before using BuildContext after async gaps
Prefer const constructors and const widgets where possible

Files:

  • lib/data/repositories/dispute_repository.dart
  • lib/features/disputes/widgets/dispute_status_badge.dart
  • lib/features/disputes/widgets/dispute_info_card.dart
  • lib/features/disputes/widgets/dispute_status_content.dart
  • lib/features/disputes/widgets/dispute_communication_section.dart
  • lib/features/disputes/providers/dispute_providers.dart
  • lib/data/models/dispute.dart
  • lib/services/mostro_service.dart
lib/features/**/providers/**/*.dart

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Riverpod providers by feature under features/{feature}/providers/

Files:

  • lib/features/disputes/providers/dispute_providers.dart
lib/l10n/intl_{en,es,it}.arb

📄 CodeRabbit inference engine (CLAUDE.md)

Add new localization keys to all ARB files (en, es, it)

Files:

  • lib/l10n/intl_es.arb
  • lib/l10n/intl_it.arb
  • lib/l10n/intl_en.arb
lib/l10n/*.arb

📄 CodeRabbit inference engine (CLAUDE.md)

After modifying ARB files, regenerate localization (flutter gen-l10n or build_runner as configured)

Files:

  • lib/l10n/intl_es.arb
  • lib/l10n/intl_it.arb
  • lib/l10n/intl_en.arb
🧠 Learnings (5)
📚 Learning: 2025-08-24T16:01:53.137Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.137Z
Learning: Applies to lib/data/repositories/**/*.dart : Use the Repository pattern for all data access in data/repositories/

Applied to files:

  • lib/data/repositories/dispute_repository.dart
📚 Learning: 2025-08-24T16:01:53.137Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.137Z
Learning: Applies to lib/features/**/providers/**/*.dart : Organize Riverpod providers by feature under features/{feature}/providers/

Applied to files:

  • lib/features/disputes/providers/dispute_providers.dart
📚 Learning: 2025-08-21T14:45:43.974Z
Learnt from: Catrya
PR: MostroP2P/mobile#272
File: lib/features/relays/widgets/relay_selector.dart:13-15
Timestamp: 2025-08-21T14:45:43.974Z
Learning: In the Mostro mobile app's RelaySelector widget (lib/features/relays/widgets/relay_selector.dart), watching relaysProvider.notifier correctly triggers rebuilds because the relaysProvider itself depends on settingsProvider (line 8 in relays_provider.dart). When blacklist changes via toggleMostroRelayBlacklist(), the settingsProvider updates, causing relaysProvider to rebuild, which then notifies widgets watching the notifier. The UI correctly reflects active/inactive states in real-time through this dependency chain.

Applied to files:

  • lib/features/disputes/providers/dispute_providers.dart
📚 Learning: 2025-05-06T15:49:26.443Z
Learnt from: chebizarro
PR: MostroP2P/mobile#74
File: lib/services/mostro_service.dart:70-76
Timestamp: 2025-05-06T15:49:26.443Z
Learning: In the Mostro Mobile codebase, Riverpod code generation is used with `Riverpod` annotations. Providers like `eventStorageProvider` are generated in `.g.dart` files from annotated functions in the main provider files. These providers are accessible by importing the main provider file (e.g., `mostro_service_provider.dart`), not by importing a separate provider file.

Applied to files:

  • lib/features/disputes/providers/dispute_providers.dart
📚 Learning: 2025-08-24T16:01:53.137Z
Learnt from: CR
PR: MostroP2P/mobile#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T16:01:53.137Z
Learning: Applies to lib/l10n/intl_{en,es,it}.arb : Add new localization keys to all ARB files (en, es, it)

Applied to files:

  • lib/l10n/intl_it.arb
🪛 GitHub Actions: Flutter
lib/l10n/intl_es.arb

[error] 956-956: Flutter ARB parsing failed: Unexpected character at line 956 (intl_es.arb). The metadata entry '@_comment_relay_status' appears to be a string instead of a metadata object.

🪛 RuboCop (1.76.1)
lib/l10n/intl_en.arb

[warning] 861-861: Duplicated key in hash literal.

(Lint/DuplicateHashKey)


[warning] 872-872: Duplicated key in hash literal.

(Lint/DuplicateHashKey)

🔇 Additional comments (13)
lib/l10n/intl_en.arb (1)

651-683: LGTM: Dispute translations and placeholders are consistent.

New keys and placeholder metadata align with usage in the dispute UI (status labels, info, communication, input, list, and screen). No issues spotted in these blocks.

Also applies to: 703-713, 833-847, 849-857, 859-862, 864-876, 877-884, 949-954, 955-962

lib/l10n/intl_it.arb (2)

711-740: LGTM: Dispute translations (IT) and placeholders look correct.

The dispute status set and communication/input/info/screen strings are coherent and match EN.

Also applies to: 936-987


990-1012: Regenerate localization and validate cross-locale parity.

After removing duplicates, ensure IT/ES have all EN keys (esp. dispute keys and forAmountWithCurrency).

Use the script provided in the EN comment to:

  • detect duplicate keys in EN/IT/ES,
  • compare key sets (EN → IT/ES),
  • then run flutter gen-l10n locally.
lib/services/mostro_service.dart (1)

64-105: Robust validation improvements — nice work.

Empty-content guard, structured JSON checks, and peer pubkey validation reduce parse-time exceptions and bad data ingestion.

lib/features/disputes/widgets/dispute_status_badge.dart (1)

1-90: LGTM: Normalization + “solved” alias handled correctly.

Mappings use AppTheme.* with withValues(alpha) and S.of(context) keys. Matches PR’s status plan and guards variants (“in progress”, “in_progress”, etc.).

lib/features/disputes/widgets/dispute_info_card.dart (1)

10-68: LGTM: Clear, localized dispute header and IDs, with proper provider usage.

Good use of S.of(context) for all labels and DisputeStatusBadge integration.

lib/features/disputes/widgets/dispute_status_content.dart (1)

128-131: Remove hardcoded English via model’s description; use i18n only.

dispute.description is English. Show a localized message instead.

-        return "${S.of(context)!.disputeStatusInProgress}: ${dispute.description}";
+        return S.of(context)!.disputeStatusInProgress;

Likely an incorrect or invalid review comment.

lib/features/disputes/providers/dispute_providers.dart (1)

8-22: Repository provider: reactive, scoped, and disposable — LGTM.

lib/features/disputes/widgets/dispute_communication_section.dart (1)

215-277: Fixed unbounded layout by using a fixed-height SizedBox — nice.

lib/data/models/dispute.dart (1)

145-176: Event parsing and timestamp extraction — solid.

Tag extraction and ms conversion logic looks robust.

lib/l10n/intl_es.arb (3)

682-689: Dispute status strings look good.

Labels are concise and consistent with the status badge system.


946-953: Nice: dedicated dispute screen error strings.

Placeholders and wording LGTM.


731-741: Remove deprecated forAmount key in intl_es.arb
No occurrences of forAmount remain in Dart code; all calls now use forAmountWithCurrency. You can safely delete the old key from lib/l10n/intl_es.arb.

Comment on lines +449 to +465
/// Backward compatibility getter for description
String get description {
switch (descriptionKey) {
case DisputeDescriptionKey.initiatedByUser:
return 'You opened this dispute';
case DisputeDescriptionKey.initiatedByPeer:
return 'A dispute was opened against you';
case DisputeDescriptionKey.inProgress:
return 'Dispute is being reviewed by an admin';
case DisputeDescriptionKey.resolved:
return 'Dispute has been resolved';
case DisputeDescriptionKey.sellerRefunded:
return 'Dispute resolved - seller refunded';
case DisputeDescriptionKey.unknown:
return 'Unknown status';
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Deprecate hardcoded English description to enforce i18n in UI.

Prevents accidental UI leakage; use descriptionKey in widgets.

-  /// Backward compatibility getter for description
-  String get description {
+  /// Backward compatibility getter for description
+  @Deprecated('Do not use in UI; resolve `descriptionKey` to localized strings in widgets.')
+  String get description {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Backward compatibility getter for description
String get description {
switch (descriptionKey) {
case DisputeDescriptionKey.initiatedByUser:
return 'You opened this dispute';
case DisputeDescriptionKey.initiatedByPeer:
return 'A dispute was opened against you';
case DisputeDescriptionKey.inProgress:
return 'Dispute is being reviewed by an admin';
case DisputeDescriptionKey.resolved:
return 'Dispute has been resolved';
case DisputeDescriptionKey.sellerRefunded:
return 'Dispute resolved - seller refunded';
case DisputeDescriptionKey.unknown:
return 'Unknown status';
}
}
/// Backward compatibility getter for description
@Deprecated('Do not use in UI; resolve `descriptionKey` to localized strings in widgets.')
String get description {
switch (descriptionKey) {
case DisputeDescriptionKey.initiatedByUser:
return 'You opened this dispute';
case DisputeDescriptionKey.initiatedByPeer:
return 'A dispute was opened against you';
case DisputeDescriptionKey.inProgress:
return 'Dispute is being reviewed by an admin';
case DisputeDescriptionKey.resolved:
return 'Dispute has been resolved';
case DisputeDescriptionKey.sellerRefunded:
return 'Dispute resolved - seller refunded';
case DisputeDescriptionKey.unknown:
return 'Unknown status';
}
}
🤖 Prompt for AI Agents
In lib/data/models/dispute.dart around lines 449 to 465, remove the hardcoded
English descriptions in the description getter and deprecate it so UI code must
use descriptionKey for i18n; replace the switch body with a simple deprecated
implementation that returns a non-localized representation (e.g.
descriptionKey.toString() or descriptionKey.name) and add an @Deprecated
annotation and a short doc comment telling callers to use descriptionKey for
localization in widgets.

Comment on lines +66 to +112
// Look for dispute-related messages in stored decrypted messages
for (final message in allMessages) {

// Look for dispute-related actions
if (message.action.toString().contains('dispute')) {
final action = message.action.toString();
final orderId = message.id;

if (action == 'dispute-initiated-by-you' || action == 'dispute-initiated-by-peer') {
// Extract dispute data from payload
if (message.payload is Dispute) {
final dispute = message.payload as Dispute;
final status = await _getDisputeStatus(dispute.disputeId);

disputeMap[dispute.disputeId] = dispute.copyWith(
orderId: orderId,
action: action,
status: status ?? 'initiated',
);
}
} else if (action == 'admin-took-dispute') {
// Update existing disputes with admin info
if (message.payload is Peer) {
final peer = message.payload as Peer;

// Find existing dispute for this order and update with admin info
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
adminPubkey: peer.publicKey,
adminTookAt: DateTime.now(),
status: 'in-progress',
);
}
});
}
} else if (action == 'admin-settled') {
// Update existing disputes when admin resolves them
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
status: 'resolved',
);
}
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

‘admin-settled’ never reached due to outer contains('dispute') guard.

This skips resolved updates. Switch to explicit action handling.

-      // Look for dispute-related messages in stored decrypted messages
-      for (final message in allMessages) {
-        
-        // Look for dispute-related actions
-        if (message.action.toString().contains('dispute')) {
-          final action = message.action.toString();
-          final orderId = message.id;
-          
-          if (action == 'dispute-initiated-by-you' || action == 'dispute-initiated-by-peer') {
+      // Look for dispute-related messages in stored decrypted messages
+      for (final message in allMessages) {
+        final action = message.action.toString();
+        final orderId = message.id;
+
+        if (action == 'dispute-initiated-by-you' || action == 'dispute-initiated-by-peer') {
             // Extract dispute data from payload
             if (message.payload is Dispute) {
               final dispute = message.payload as Dispute;
               final status = await _getDisputeStatus(dispute.disputeId);
               
               disputeMap[dispute.disputeId] = dispute.copyWith(
                 orderId: orderId,
                 action: action,
                 status: status ?? 'initiated',
               );
             }
-          } else if (action == 'admin-took-dispute') {
+        } else if (action == 'admin-took-dispute') {
             // Update existing disputes with admin info
             if (message.payload is Peer) {
               final peer = message.payload as Peer;
               
               // Find existing dispute for this order and update with admin info
               disputeMap.forEach((disputeId, dispute) {
                 if (dispute.orderId == orderId) {
                   disputeMap[disputeId] = dispute.copyWith(
                     adminPubkey: peer.publicKey,
                     adminTookAt: DateTime.now(),
                     status: 'in-progress',
                   );
                 }
               });
             }
-          } else if (action == 'admin-settled') {
+        } else if (action == 'admin-settled') {
             // Update existing disputes when admin resolves them
             disputeMap.forEach((disputeId, dispute) {
               if (dispute.orderId == orderId) {
                 disputeMap[disputeId] = dispute.copyWith(
                   status: 'resolved',
                 );
               }
             });
-          }
-        }
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Look for dispute-related messages in stored decrypted messages
for (final message in allMessages) {
// Look for dispute-related actions
if (message.action.toString().contains('dispute')) {
final action = message.action.toString();
final orderId = message.id;
if (action == 'dispute-initiated-by-you' || action == 'dispute-initiated-by-peer') {
// Extract dispute data from payload
if (message.payload is Dispute) {
final dispute = message.payload as Dispute;
final status = await _getDisputeStatus(dispute.disputeId);
disputeMap[dispute.disputeId] = dispute.copyWith(
orderId: orderId,
action: action,
status: status ?? 'initiated',
);
}
} else if (action == 'admin-took-dispute') {
// Update existing disputes with admin info
if (message.payload is Peer) {
final peer = message.payload as Peer;
// Find existing dispute for this order and update with admin info
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
adminPubkey: peer.publicKey,
adminTookAt: DateTime.now(),
status: 'in-progress',
);
}
});
}
} else if (action == 'admin-settled') {
// Update existing disputes when admin resolves them
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
status: 'resolved',
);
}
});
}
}
// Look for dispute-related messages in stored decrypted messages
for (final message in allMessages) {
final action = message.action.toString();
final orderId = message.id;
if (action == 'dispute-initiated-by-you' || action == 'dispute-initiated-by-peer') {
// Extract dispute data from payload
if (message.payload is Dispute) {
final dispute = message.payload as Dispute;
final status = await _getDisputeStatus(dispute.disputeId);
disputeMap[dispute.disputeId] = dispute.copyWith(
orderId: orderId,
action: action,
status: status ?? 'initiated',
);
}
} else if (action == 'admin-took-dispute') {
// Update existing disputes with admin info
if (message.payload is Peer) {
final peer = message.payload as Peer;
// Find existing dispute for this order and update with admin info
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
adminPubkey: peer.publicKey,
adminTookAt: DateTime.now(),
status: 'in-progress',
);
}
});
}
} else if (action == 'admin-settled') {
// Update existing disputes when admin resolves them
disputeMap.forEach((disputeId, dispute) {
if (dispute.orderId == orderId) {
disputeMap[disputeId] = dispute.copyWith(
status: 'resolved',
);
}
});
}
}
🤖 Prompt for AI Agents
In lib/data/repositories/dispute_repository.dart around lines 66 to 112, the
outer guard if (message.action.toString().contains('dispute')) prevents branches
like 'admin-settled' from ever running; replace the broad contains check with
explicit action handling (e.g., compute action = message.action.toString() and
either remove the outer guard and use direct if/else if checks for
'dispute-initiated-by-you', 'dispute-initiated-by-peer', 'admin-took-dispute',
'admin-settled' etc., or change the outer condition to check against a set/list
of allowed actions) so the 'admin-settled' branch is reachable and resolved
updates are applied.

Comment on lines +153 to +159
Text(
'${S.of(context)!.admin}: ${adminPubkey.substring(0, 16)}...',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
textAlign: TextAlign.center,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid substring range error on short pubkeys.

Use a safe truncation.

-          Text(
-            '${S.of(context)!.admin}: ${adminPubkey.substring(0, 16)}...',
+          Text(
+            '${S.of(context)!.admin}: ${adminPubkey.length <= 16 ? adminPubkey : '${adminPubkey.substring(0, 16)}...'}',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Text(
'${S.of(context)!.admin}: ${adminPubkey.substring(0, 16)}...',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
textAlign: TextAlign.center,
Text(
'${S.of(context)!.admin}: ${adminPubkey.length <= 16 ? adminPubkey : '${adminPubkey.substring(0, 16)}...'}',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
textAlign: TextAlign.center,
🤖 Prompt for AI Agents
In lib/features/disputes/widgets/dispute_communication_section.dart around lines
153 to 159 the code uses adminPubkey.substring(0, 16) which will throw if the
pubkey is shorter than 16 characters; replace the direct substring with a safe
truncation that checks the string length (e.g., take up to min(16,
adminPubkey.length)) and append "..." only when truncation occurred, or use a
small helper/extension to ellipsize the pubkey so it never attempts an
out-of-range substring.


@override
Widget build(BuildContext context) {
bool isResolved = dispute.status.toLowerCase() == 'resolved' || dispute.status.toLowerCase() == 'closed';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix resolved-status detection (use 'solved', not 'closed').

Current check misses “solved” (used elsewhere in the PR) and includes “closed” which may not be emitted. Guard against both to be safe.

-    bool isResolved = dispute.status.toLowerCase() == 'resolved' || dispute.status.toLowerCase() == 'closed';
+    final status = dispute.status.toLowerCase();
+    final isResolved = status == 'resolved' || status == 'solved' || status == 'closed';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool isResolved = dispute.status.toLowerCase() == 'resolved' || dispute.status.toLowerCase() == 'closed';
final status = dispute.status.toLowerCase();
final isResolved = status == 'resolved' || status == 'solved' || status == 'closed';
🤖 Prompt for AI Agents
In lib/features/disputes/widgets/dispute_status_content.dart around line 16, the
boolean uses the wrong tokens to detect a resolved dispute: it checks for
'resolved' or 'closed' but the codebase uses 'solved' elsewhere and 'closed' may
not be emitted. Replace the logic to normalize the status once (e.g., lowercased
trimmed string) and set isResolved to true if the normalized status equals
'resolved' or 'solved' (you may optionally also include 'closed' as a fallback)
so all expected variants are handled.

Comment on lines +124 to +127
return S.of(context)!.disputeOpenedByYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.initiatedByPeer:
return S.of(context)!.disputeOpenedAgainstYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.inProgress:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Localize counterparty; don’t pass model placeholders to UI.

Avoid using counterpartyDisplay (may be a sentinel like unknownCounterparty). Prefer localized fallback.

-        return S.of(context)!.disputeOpenedByYou(dispute.counterpartyDisplay);
+        return S.of(context)!.disputeOpenedByYou(dispute.counterparty ?? S.of(context)!.unknown);
-        return S.of(context)!.disputeOpenedAgainstYou(dispute.counterpartyDisplay);
+        return S.of(context)!.disputeOpenedAgainstYou(dispute.counterparty ?? S.of(context)!.unknown);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return S.of(context)!.disputeOpenedByYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.initiatedByPeer:
return S.of(context)!.disputeOpenedAgainstYou(dispute.counterpartyDisplay);
case DisputeDescriptionKey.inProgress:
return S.of(context)!.disputeOpenedByYou(dispute.counterparty ?? S.of(context)!.unknown);
case DisputeDescriptionKey.initiatedByPeer:
return S.of(context)!.disputeOpenedAgainstYou(dispute.counterparty ?? S.of(context)!.unknown);
case DisputeDescriptionKey.inProgress:
🤖 Prompt for AI Agents
In lib/features/disputes/widgets/dispute_status_content.dart around lines
124-127, the code currently passes dispute.counterpartyDisplay directly into the
localized message which can be a sentinel like "unknownCounterparty"; instead,
detect if dispute.counterpartyDisplay is null/empty or equals the sentinel
constant and in that case pass a localized fallback (e.g.
S.of(context)!.unknownCounterparty or an appropriate localized label), otherwise
pass the actual counterparty name; update both disputeOpenedByYou and
disputeOpenedAgainstYou calls to use this resolved, localized counterparty
string so the UI never receives raw model placeholders.

Comment on lines +692 to +696
"@disputeOpenedByYou": {
"placeholders": {
"counterparty": { "type": "String" }
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t hardcode the counterparty role in “disputeOpenedByYou”.

It says “contra el comprador …” even when the counterparty is the seller. Make it neutral.

-  "disputeOpenedByYou": "Abriste esta disputa contra el comprador {counterparty}, lee atentamente a continuación:",
+  "disputeOpenedByYou": "Abriste esta disputa contra {counterparty}, lee atentamente a continuación:",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@disputeOpenedByYou": {
"placeholders": {
"counterparty": { "type": "String" }
}
},
"disputeOpenedByYou": "Abriste esta disputa contra {counterparty}, lee atentamente a continuación:",
"@disputeOpenedByYou": {
"placeholders": {
"counterparty": { "type": "String" }
}
},
🤖 Prompt for AI Agents
In lib/l10n/intl_es.arb around lines 692 to 696, the "disputeOpenedByYou"
message hardcodes "comprador" which incorrectly labels the counterparty when
they are the seller; change the message text to a neutral formulation that
either injects the existing "counterparty" placeholder (e.g., use the
placeholder token where the role should appear) or use a role-neutral phrase
such as "la otra parte" so the string no longer assumes "comprador"; ensure the
placeholder name matches the "counterparty" placeholder defined and adjust
surrounding grammar to remain correct for Spanish.

"orderCanceled": "La orden fue cancelada",


"@_comment_dispute_communication": "Texto de Comunicación de Disputas",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent future ARB parsing errors: turn newly added “@comment*” strings into metadata objects.

These will be treated as invalid metadata if left as strings. Use objects with a description to keep them as non-translatable notes.

-  "@_comment_dispute_communication": "Texto de Comunicación de Disputas",
+  "@_comment_dispute_communication": { "description": "Texto de Comunicación de Disputas" },

-  "@_comment_dispute_input": "Texto de Entrada de Disputas",
+  "@_comment_dispute_input": { "description": "Texto de Entrada de Disputas" },

-  "@_comment_dispute_list": "Texto de Lista de Disputas",
+  "@_comment_dispute_list": { "description": "Texto de Lista de Disputas" },

-  "@_comment_dispute_info": "Texto de Información de Disputas",
+  "@_comment_dispute_info": { "description": "Texto de Información de Disputas" },

-  "@_comment_dispute_screen": "Texto de Pantalla de Disputas",
+  "@_comment_dispute_screen": { "description": "Texto de Pantalla de Disputas" },

Also applies to: 917-917, 927-927, 932-932, 946-946

🤖 Prompt for AI Agents
In lib/l10n/intl_es.arb around lines 903, 917, 927, 932, and 946, several
entries like "@_comment_dispute_communication" are currently plain strings which
will be treated as invalid metadata by ARB parsers; replace each of these
string-valued keys with proper metadata objects (e.g., change the value from a
string to an object containing a "description" field) so they remain
non-translatable notes and prevent ARB parsing errors.

Comment on lines +930 to +931
"retry": "Reintentar",

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate keys (“retry”, “orderIdLabel”).

These keys already exist earlier in the file; duplicates can cause surprises during codegen and at runtime (last one wins).

-  "retry": "Reintentar",-  "orderIdLabel": "ID de Orden",

Also applies to: 941-941

🤖 Prompt for AI Agents
In lib/l10n/intl_es.arb around lines 930-931 (and similarly at line 941), there
are duplicate localization keys ("retry" and "orderIdLabel") that override
earlier entries; remove the later duplicate entries so each key appears only
once (keep the original translations defined earlier) and run the
localization/codegen step to ensure no duplicates remain.

}


"@_comment_relay_status": "Mensajes de estado de relays",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix CI: metadata entries starting with @ must be objects (not strings).

Flutter gen-l10n fails on this line. Convert the comment to a metadata object.

-  "@_comment_relay_status": "Mensajes de estado de relays",
+  "@_comment_relay_status": { "description": "Mensajes de estado de relays" },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@_comment_relay_status": "Mensajes de estado de relays",
"@_comment_relay_status": { "description": "Mensajes de estado de relays" },
🧰 Tools
🪛 RuboCop (1.76.1)

[fatal] 956-956: unexpected token tSTRING
(Using Ruby 2.7 parser; configure using TargetRubyVersion parameter, under AllCops)

(Lint/Syntax)

🪛 GitHub Actions: Flutter

[error] 956-956: Flutter ARB parsing failed: Unexpected character at line 956 (intl_es.arb). The metadata entry '@_comment_relay_status' appears to be a string instead of a metadata object.

🤖 Prompt for AI Agents
In lib/l10n/intl_es.arb around line 956, the metadata entry starting with '@' is
currently a string which breaks Flutter gen-l10n; change the value from a string
to a JSON object containing at least a description field (for example:
"@_comment_relay_status": {"description": "Mensajes de estado de relays"}) and
ensure surrounding commas and JSON syntax remain valid so the metadata key is an
object rather than a plain string.

Comment on lines +760 to +762
"forAmount": "per {amount}",
"forAmountWithCurrency": "per {amount} {currency}",
"@forAmountWithCurrency": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate key: forAmount defined earlier (line ~619).

This later copy conflicts; keep only one “forAmount” plus the new “forAmountWithCurrency”.

-  "forAmount": "per {amount}",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"forAmount": "per {amount}",
"forAmountWithCurrency": "per {amount} {currency}",
"@forAmountWithCurrency": {
"forAmountWithCurrency": "per {amount} {currency}",
"@forAmountWithCurrency": {
🤖 Prompt for AI Agents
In lib/l10n/intl_it.arb around lines 760 to 762, there is a duplicate
"forAmount" key (already defined earlier ~line 619); remove the duplicated
"forAmount": "per {amount}" entry at these lines and keep the new
"forAmountWithCurrency" and its metadata (@forAmountWithCurrency) intact so the
file contains only one "forAmount" definition plus the new
"forAmountWithCurrency" entry.

@grunch grunch closed this Sep 8, 2025
@Catrya Catrya deleted the andrea/issue-97 branch October 22, 2025 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants