Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth-context';
import { apiFetch, ApiError } from '@/lib/api';
import { apiFetch } from '@/lib/api';
import { actionErrorMessage } from '@/lib/action-error';
import { signAndSubmit } from '@/lib/onchain';
import type { ResaleListing } from '@/lib/types';
import { FormError } from '@/components/form-error';
Expand Down Expand Up @@ -52,7 +53,7 @@ export default function MarketplacePage() {
setNotice('Ticket purchased — find it under My tickets.');
setListings((prev) => prev.filter((l) => l.ticketId !== ticketId));
} catch (err) {
setError(err instanceof ApiError ? err.message : 'Could not complete the purchase.');
setError(actionErrorMessage(err, 'Could not complete the purchase.'));
} finally {
setBuyingTicketId(null);
}
Expand Down
35 changes: 35 additions & 0 deletions src/lib/action-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from 'vitest';

vi.mock('@stellar/freighter-api', () => ({
isConnected: vi.fn(),
requestAccess: vi.fn(),
getAddress: vi.fn(),
signTransaction: vi.fn(),
}));

import { actionErrorMessage } from './action-error';
import { ApiError } from './api';
import { WalletError } from './wallet';

describe('actionErrorMessage', () => {
it('surfaces ApiError messages', () => {
expect(actionErrorMessage(new ApiError('Listing sold out', 409), 'fallback')).toBe(
'Listing sold out',
);
});

it('surfaces WalletError messages', () => {
expect(
actionErrorMessage(new WalletError('User declined access'), 'fallback'),
).toBe('User declined access');
});

it('falls back for unknown errors', () => {
expect(actionErrorMessage(new Error('boom'), 'fallback')).toBe('fallback');
expect(actionErrorMessage('nope', 'fallback')).toBe('fallback');
});

it('falls back when a known error has an empty message', () => {
expect(actionErrorMessage(new WalletError(''), 'fallback')).toBe('fallback');
});
});
14 changes: 14 additions & 0 deletions src/lib/action-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiError } from './api';
import { WalletError } from './wallet';

/**
* User-facing message for a failed wallet-backed action. API and wallet errors
* (rejected Freighter popup, locked extension, wrong network) carry an
* actionable message; anything else falls back to `fallback`.
*/
export function actionErrorMessage(err: unknown, fallback: string): string {
if ((err instanceof ApiError || err instanceof WalletError) && err.message) {
return err.message;
}
return fallback;
}