diff --git a/src/app/marketplace/page.tsx b/src/app/marketplace/page.tsx index f606f80..a08d7e8 100644 --- a/src/app/marketplace/page.tsx +++ b/src/app/marketplace/page.tsx @@ -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'; @@ -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); } diff --git a/src/lib/action-error.test.ts b/src/lib/action-error.test.ts new file mode 100644 index 0000000..3ab7b45 --- /dev/null +++ b/src/lib/action-error.test.ts @@ -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'); + }); +}); diff --git a/src/lib/action-error.ts b/src/lib/action-error.ts new file mode 100644 index 0000000..4faf476 --- /dev/null +++ b/src/lib/action-error.ts @@ -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; +}