(null);
const buyerCheckouts = orders.filter(({ order }) => isBuyerCheckoutInProgress(order, currentUserPubky));
const sellerReservations = orders.filter(({ order }) => isSellerReservation(order, currentUserPubky));
const abandonedCheckouts = orders.filter(({ order }) => isAbandonedCheckout(order, currentUserPubky));
@@ -85,12 +87,28 @@ export function MarketplaceOrders() {
);
const orderCounts = getOrderTabCounts(historyOrders, currentUserPubky);
const visibleOrders = historyOrders.filter((view) => isOrderInTab(view, activeTab, currentUserPubky));
+ const listedOrderIds = new Set(
+ [...buyerCheckouts, ...sellerReservations, ...historyOrders, ...abandonedCheckouts].map(({ order }) => order.id),
+ );
+ const anchoredOrder =
+ anchorOrderId && !listedOrderIds.has(anchorOrderId)
+ ? (orders.find(({ order }) => order.id === anchorOrderId)?.order ?? null)
+ : null;
+ const linkedUnlistedOrder =
+ anchoredOrder && isOrderParticipant(anchoredOrder, currentUserPubky) ? anchoredOrder : null;
useMarkMarketplaceOrdersSeen(!isLoading && !error && !needsSession);
+ useEffect(() => {
+ const readAnchor = () => setAnchorOrderId(readOrderAnchorId(window.location.hash));
+ readAnchor();
+ window.addEventListener('hashchange', readAnchor);
+ return () => window.removeEventListener('hashchange', readAnchor);
+ }, []);
+
useEffect(() => {
if (isLoading) return;
- const anchorId = readOrderAnchorId(window.location.hash);
+ const anchorId = anchorOrderId;
if (!anchorId) return;
const view = orders.find((candidate) => candidate.order.id === anchorId);
if (!view) return;
@@ -102,7 +120,7 @@ export function MarketplaceOrders() {
return;
}
document.getElementById(orderAnchorId(anchorId))?.scrollIntoView({ block: 'center' });
- }, [activeTab, currentUserPubky, isLoading, orders]);
+ }, [activeTab, anchorOrderId, currentUserPubky, isLoading, orders]);
useEffect(() => {
if (isLoading) return;
@@ -186,6 +204,34 @@ export function MarketplaceOrders() {
) : orders.length ? (
<>
+ {linkedUnlistedOrder && (
+
+
+ From Activity
+
+
+
+
+
+ {currentUserPubky === linkedUnlistedOrder.buyerPubky ? 'Your purchase' : 'Your sale'}
+
+
+ {unlistedOrderStateLabel(linkedUnlistedOrder)}
+
+
+ {linkedUnlistedOrder.lines.map((line) => (
+
+ {line.title} × {line.quantity}
+
+ ))}
+
+
+
+
+ )}
{buyerCheckouts.length > 0 && (
@@ -598,6 +644,10 @@ function isCurrentUserBuyer(order: MarketplaceOrder, currentUserPubky: string |
return currentUserPubky !== null && order.buyerPubky === currentUserPubky;
}
+function isOrderParticipant(order: MarketplaceOrder, currentUserPubky: string | null): boolean {
+ return isCurrentUserBuyer(order, currentUserPubky) || isCurrentUserSeller(order, currentUserPubky);
+}
+
function isSellerAwaitingPayment(
{ order, payment }: Pick,
currentUserPubky: string | null,
diff --git a/src/libs/commerce/checkout-phase.test.ts b/src/libs/commerce/checkout-phase.test.ts
index f20b1bc141..a14dfbe3da 100644
--- a/src/libs/commerce/checkout-phase.test.ts
+++ b/src/libs/commerce/checkout-phase.test.ts
@@ -17,12 +17,21 @@ import {
reservedWhileYouPayCopy,
resolveCreatedCheckoutOrderIds,
sellerReservationCopy,
+ unlistedOrderStateLabel,
} from './checkout-phase';
const BUYER = 'b'.repeat(52);
const SELLER = 's'.repeat(52);
describe('checkout-phase', () => {
+ it('names the state of an order no Orders section lists', () => {
+ expect(unlistedOrderStateLabel({ state: 'cancelled', receiptId: null })).toBe('Cancelled before payment');
+ expect(unlistedOrderStateLabel({ state: 'cancelled' })).toBe('Cancelled before payment');
+ expect(unlistedOrderStateLabel({ state: 'cancelled', receiptId: 'receipt-1' })).toBe('Cancelled');
+ expect(unlistedOrderStateLabel({ state: 'pending_payment' })).toBe('Awaiting payment');
+ expect(unlistedOrderStateLabel({ state: 'return_requested' })).toBe('Return requested');
+ });
+
it('builds the checkout route without minting a second id', () => {
expect(getMarketplaceCheckoutRoute()).toBe(MARKETPLACE_ROUTES.CHECKOUT);
expect(getMarketplaceCheckoutRoute('018f47d2-6a27-7c23-a49d-000000000001')).toBe(
diff --git a/src/libs/commerce/checkout-phase.ts b/src/libs/commerce/checkout-phase.ts
index 7334217310..b885c19042 100644
--- a/src/libs/commerce/checkout-phase.ts
+++ b/src/libs/commerce/checkout-phase.ts
@@ -102,6 +102,14 @@ export function isSellerPaidOrder(
);
}
+/** State pill for an order an Activity link opens but no Orders section lists (a seller's unpaid cancel). */
+export function unlistedOrderStateLabel(order: { state: string; receiptId?: string | null }): string {
+ if (order.state === 'cancelled' && !hasReceipt(order)) return 'Cancelled before payment';
+ if (isPendingPaymentState(order.state)) return 'Awaiting payment';
+ const label = order.state.replaceAll('_', ' ');
+ return label.charAt(0).toUpperCase() + label.slice(1);
+}
+
export function isBuyerCheckoutInProgress(
order: { state: string; buyerPubky: string },
buyerPubky: string | null,
diff --git a/src/test/vrt/marketplace/MarketplaceOrders.vrt.test.tsx b/src/test/vrt/marketplace/MarketplaceOrders.vrt.test.tsx
index d10da95ef7..3e551cafad 100644
--- a/src/test/vrt/marketplace/MarketplaceOrders.vrt.test.tsx
+++ b/src/test/vrt/marketplace/MarketplaceOrders.vrt.test.tsx
@@ -210,8 +210,32 @@ const fixtures = vi.hoisted(async () => {
},
];
+ const sellerUnpaidCancelled = createOrderFixture('cancelled', {
+ id: '018f47d2-6a27-7c23-a49d-000000000741',
+ buyerPubky: 't'.repeat(52),
+ sellerPubky: ORDER_FIXTURE_BUYER,
+ receiptId: null,
+ lines: [
+ {
+ listingAggregateId: `listing:${ORDER_FIXTURE_BUYER}_lamp`,
+ listingRevision: 1,
+ contentHash: 'f'.repeat(64),
+ title: 'Brass desk lamp',
+ quantity: 1,
+ unitPrice: { amountMinor: 4_500, currency: 'USD', exponent: 2 },
+ subtotal: { amountMinor: 4_500, currency: 'USD', exponent: 2 },
+ },
+ ],
+ });
+ const activityLinkedUnlisted = [
+ ...sellerPendingPayment,
+ { order: sellerUnpaidCancelled, payment: null, receipt: null },
+ ];
+
return {
buyer: ORDER_FIXTURE_BUYER,
+ activityLinkedUnlisted,
+ activityLinkedUnlistedId: sellerUnpaidCancelled.id,
everyOrderState: createOrderViewsForEveryState(),
everyPaymentState: createOrderViewsForEveryPaymentState(),
sellerNeedsAttention,
@@ -412,6 +436,31 @@ describe('Marketplace orders — visual regression', () => {
await expect(expectVrtSurface('marketplace-orders')).toMatchScreenshot('orders-pending-payment-seller-desktop');
});
+ for (const [label, viewport] of [
+ ['desktop', VRT_VIEWPORT_DESKTOP],
+ ['mobile', VRT_VIEWPORT_MOBILE],
+ ] as const) {
+ it(`renders an Activity-linked unpaid cancel the page does not list at ${label} viewport`, async () => {
+ const { activityLinkedUnlisted, activityLinkedUnlistedId } = await fixtures;
+ ordersState.orders = activityLinkedUnlisted;
+ ordersState.isLoading = false;
+ ordersState.error = null;
+ const previous = `${window.location.pathname}${window.location.search}${window.location.hash}`;
+ window.history.replaceState(null, '', `${window.location.pathname}#order-${activityLinkedUnlistedId}`);
+ try {
+ const screen = await renderForVRT(, { viewport });
+ await expect.element(screen.getByRole('heading', { name: 'From Activity' })).toBeVisible();
+ await expect.element(screen.getByText('Cancelled before payment')).toBeVisible();
+ await expect.element(screen.getByText('Order 018f47d2').first()).toBeVisible();
+ await expect(expectVrtSurface('marketplace-orders')).toMatchScreenshot(
+ `orders-activity-linked-unlisted-${label}`,
+ );
+ } finally {
+ window.history.replaceState(null, '', previous);
+ }
+ });
+ }
+
it('rejects an incorrect production surface marker', async () => {
ordersState.orders = [];
ordersState.isLoading = false;
diff --git a/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/Marketplace-orders---visual-regression-renders-an-Activity-linked-unpaid-cancel-the-page-does-not-list-at-desktop-viewport-1.png b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/Marketplace-orders---visual-regression-renders-an-Activity-linked-unpaid-cancel-the-page-does-not-list-at-desktop-viewport-1.png
new file mode 100644
index 0000000000..9c9e746c4c
Binary files /dev/null and b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/Marketplace-orders---visual-regression-renders-an-Activity-linked-unpaid-cancel-the-page-does-not-list-at-desktop-viewport-1.png differ
diff --git a/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-chromium-linux.png b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-chromium-linux.png
new file mode 100644
index 0000000000..1aa7163160
Binary files /dev/null and b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-chromium-linux.png differ
diff --git a/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-firefox-linux.png b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-firefox-linux.png
new file mode 100644
index 0000000000..41e35b65ec
Binary files /dev/null and b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-desktop-firefox-linux.png differ
diff --git a/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-chromium-linux.png b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-chromium-linux.png
new file mode 100644
index 0000000000..a0f7bc26b7
Binary files /dev/null and b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-chromium-linux.png differ
diff --git a/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-firefox-linux.png b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-firefox-linux.png
new file mode 100644
index 0000000000..438162b413
Binary files /dev/null and b/src/test/vrt/marketplace/__screenshots__/MarketplaceOrders.vrt.test.tsx/orders-activity-linked-unlisted-mobile-firefox-linux.png differ