From 7a60099bab192af3d052f4d35d9440dc52cf218f Mon Sep 17 00:00:00 2001
From: SkateSpots
Date: Sat, 10 Oct 2026 09:32:52 +0700
Subject: [PATCH] fix(marketplace): show an outage instead of an empty
trade/reputation state
TradesPage, SellerDashboard, TradeReputationPanel, TradeReputationCard and
ShippingStatusCard now surface a relay outage explicitly instead of
rendering nothing-waiting / no-trades / hidden reputation. Shared
marketUi.loadFailed (en/es).
---
MARKETPLACE_FLOW_AUDIT.md | 5 +++++
src/components/SellerDashboard.tsx | 6 ++++--
src/components/ShippingStatusCard.tsx | 4 +++-
src/components/TradeReputationCard.tsx | 6 +++++-
src/components/TradeReputationPanel.tsx | 6 ++++--
src/i18n/en.ts | 1 +
src/i18n/es.ts | 1 +
src/pages/TradesPage.tsx | 20 +++++++++++++++-----
8 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/MARKETPLACE_FLOW_AUDIT.md b/MARKETPLACE_FLOW_AUDIT.md
index e22040c..0438787 100644
--- a/MARKETPLACE_FLOW_AUDIT.md
+++ b/MARKETPLACE_FLOW_AUDIT.md
@@ -222,6 +222,11 @@ Second batch (payment, attestation, escrow):
escrow (`item.escrow === 'required'`); ask-seller is available on an active
listing (not only once sold); payment receipts are scoped to the payer's
pubkey, so a stale/foreign receipt cannot be attached by another account.
+14. **Outage states (trade/reputation/seller/shipping):** `TradesPage`,
+ `SellerDashboard`, `TradeReputationPanel`, `TradeReputationCard` and
+ `ShippingStatusCard` now show an explicit "could not reach the relays"
+ notice (`marketUi.loadFailed`) instead of an empty / "nothing waiting" /
+ "no trades" state when their query errors.
## 7. Recommended order
diff --git a/src/components/SellerDashboard.tsx b/src/components/SellerDashboard.tsx
index de737c7..15c0122 100644
--- a/src/components/SellerDashboard.tsx
+++ b/src/components/SellerDashboard.tsx
@@ -73,7 +73,7 @@ function MyListingRow({ item }: { item: MarketplaceItem }) {
export function SellerDashboard() {
const { t } = useTranslation();
const { user } = useCurrentUser();
- const { data: items, isLoading } = useMarketplaceItems(200);
+ const { data: items, isLoading, isError } = useMarketplaceItems(200);
const mine = (items ?? []).filter((i) => i.pubkey === user?.pubkey);
const active = mine.filter((i) => i.status !== 'sold');
@@ -98,7 +98,9 @@ export function SellerDashboard() {
- {isLoading ? (
+ {isError ? (
+ {t('marketUi.loadFailed')}
+ ) : isLoading ? (
{t('marketUi.seller.loading')}
) : mine.length === 0 ? (
diff --git a/src/components/ShippingStatusCard.tsx b/src/components/ShippingStatusCard.tsx
index e34e8be..81460f3 100644
--- a/src/components/ShippingStatusCard.tsx
+++ b/src/components/ShippingStatusCard.tsx
@@ -23,7 +23,7 @@ export function ShippingStatusCard({ item }: { item: MarketplaceItem }) {
const { toast } = useToast();
const listingRef = { pubkey: item.pubkey, identifier: item.identifier };
const { data: trade } = useListingTrade(listingRef, user?.pubkey);
- const { data: statuses = [] } = useShipmentStatus(listingRef);
+ const { data: statuses = [], isError: statusError } = useShipmentStatus(listingRef);
const { setStatus, isPending } = useSetShipmentStatus();
const [tracking, setTracking] = useState('');
@@ -80,6 +80,8 @@ export function ShippingStatusCard({ item }: { item: MarketplaceItem }) {
{issue && · {t('marketUi.shipping.issue')}}
+ {statusError && {t('marketUi.loadFailed')}
}
+
setTracking(e.target.value)}
diff --git a/src/components/TradeReputationCard.tsx b/src/components/TradeReputationCard.tsx
index 4844382..f819fdf 100644
--- a/src/components/TradeReputationCard.tsx
+++ b/src/components/TradeReputationCard.tsx
@@ -24,7 +24,7 @@ function Stat({ label, value, accent }: { label: string; value: number; accent?:
*/
export function TradeReputationCard({ pubkey, className }: { pubkey: string; className?: string }) {
const { t } = useTranslation();
- const { data } = useTradeReputation(pubkey);
+ const { data, isError } = useTradeReputation(pubkey);
const verifiedWith = data?.verifiedWith ?? [];
const partnerPubkeys = verifiedWith.map((v) => v.pubkey);
// Trust-weight each mutually-confirmed trade by the counterparty's web-of-trust
@@ -36,6 +36,10 @@ export function TradeReputationCard({ pubkey, className }: { pubkey: string; cla
const { data: rings } = useTradeRings(pubkey, partnerPubkeys);
const total = (data?.sales ?? 0) + (data?.purchases ?? 0);
+ // A relay outage must not silently read as "no reputation".
+ if (isError) {
+ return {t('marketUi.loadFailed')}
;
+ }
if (!data || total === 0) return null;
const people = data.verifiedCounterparties;
diff --git a/src/components/TradeReputationPanel.tsx b/src/components/TradeReputationPanel.tsx
index f7bb677..b1ce7cf 100644
--- a/src/components/TradeReputationPanel.tsx
+++ b/src/components/TradeReputationPanel.tsx
@@ -93,7 +93,7 @@ function ReviewSkeleton() {
*/
export function TradeReputationPanel({ pubkey }: { pubkey: string }) {
const { t } = useTranslation();
- const { data, isLoading } = useTradeReputation(pubkey);
+ const { data, isLoading, isError } = useTradeReputation(pubkey);
const attestations = [...(data?.attestations ?? [])].sort((a, b) => b.created_at - a.created_at);
const reviews = attestations.filter((a) => a.pubkey !== pubkey);
const ratings = attestations
@@ -126,7 +126,9 @@ export function TradeReputationPanel({ pubkey }: { pubkey: string }) {
)}
- {isLoading ? (
+ {isError ? (
+ {t('marketUi.loadFailed')}
+ ) : isLoading ? (
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 06a5712..7f77748 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -1772,6 +1772,7 @@ marketUi: {
paidWithProof: 'Saved a verifiable receipt — confirm the trade to make it count.',
paidBody: 'Ask the seller to confirm the trade so it counts for both of you.',
},
+ loadFailed: 'Could not reach the relays. Check your connection and try again.',
offers: {
accepted: 'Accepted',
declined: 'Declined',
diff --git a/src/i18n/es.ts b/src/i18n/es.ts
index 88c6892..d65d13b 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -1772,6 +1772,7 @@ marketUi: {
paidWithProof: 'Se guardó un recibo verificable: confirma el trato para que cuente.',
paidBody: 'Pide al vendedor que confirme el trato para que cuente para ambos.',
},
+ loadFailed: 'No se pudo contactar con los relays. Comprueba tu conexión e inténtalo de nuevo.',
offers: {
accepted: 'Aceptada',
declined: 'Rechazada',
diff --git a/src/pages/TradesPage.tsx b/src/pages/TradesPage.tsx
index c740bbd..0934692 100644
--- a/src/pages/TradesPage.tsx
+++ b/src/pages/TradesPage.tsx
@@ -56,9 +56,11 @@ export default function TradesPage() {
const { toast } = useToast();
const { publishTrade, isPending: publishing } = useTradeAttestation();
- const { data: incoming, isLoading: loadingIncoming } = useIncomingTradeRequests(user?.pubkey);
- const { data: pending, isLoading: loadingPending } = usePendingTradeConfirmations(user?.pubkey);
- const { data: reputation, isLoading: loadingRep } = useTradeReputation(user?.pubkey);
+ const { data: incoming, isLoading: loadingIncoming, isError: incomingError } = useIncomingTradeRequests(user?.pubkey);
+ const { data: pending, isLoading: loadingPending, isError: pendingError } = usePendingTradeConfirmations(user?.pubkey);
+ const { data: reputation, isLoading: loadingRep, isError: repError } = useTradeReputation(user?.pubkey);
+ // A relay outage must not look like "nothing waiting" / "no trades".
+ const tradeError = incomingError || pendingError || repError;
const [busy, setBusy] = useState
(null);
const [revokeTarget, setRevokeTarget] = useState<{
@@ -226,7 +228,11 @@ export default function TradesPage() {
/>
))}
- {!loadingIncoming && !loadingPending && actionNeeded === 0 && (
+ {tradeError && (
+ {t('marketUi.loadFailed')}
+ )}
+
+ {!tradeError && !loadingIncoming && !loadingPending && actionNeeded === 0 && (
{t('marketUi.trades.nothingWaiting')}
)}
@@ -239,7 +245,11 @@ export default function TradesPage() {
{loadingRep && }
- {!loadingRep && authored.length === 0 && (
+ {repError && (
+ {t('marketUi.loadFailed')}
+ )}
+
+ {!repError && !loadingRep && authored.length === 0 && (