diff --git a/src/components/send/QuoteCard.test.tsx b/src/components/send/QuoteCard.test.tsx
new file mode 100644
index 0000000..8aa0da8
--- /dev/null
+++ b/src/components/send/QuoteCard.test.tsx
@@ -0,0 +1,57 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import React from 'react'
+import { QuoteCard } from './QuoteCard'
+import type { Quote } from '@/types'
+import { NATIVE_XLM } from '@/types'
+
+const mockQuote: Quote = {
+ id: 'quote_123',
+ sourceAsset: NATIVE_XLM,
+ destinationAsset: { ...NATIVE_XLM, code: 'USDC', issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' },
+ sendAmount: '100',
+ receiveAmount: '12.5',
+ exchangeRate: '0.125',
+ networkFee: '0.00001',
+ serviceFee: '0',
+ totalFee: '0.00001',
+ estimatedSeconds: 5,
+ slippageTolerance: '0.5',
+ priceImpact: '0.01',
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
+ path: [],
+}
+
+describe('QuoteCard', () => {
+ it('renders quote details correctly', () => {
+ render()
+ expect(screen.getByText('Exchange Quote')).toBeInTheDocument()
+ expect(screen.getByText('100.0000')).toBeInTheDocument()
+ expect(screen.getByText('12.5000')).toBeInTheDocument()
+ })
+
+ it('renders Refresh button when onRefresh is provided and calls it when clicked (#39)', () => {
+ const onRefresh = vi.fn()
+ render()
+
+ const refreshButton = screen.getByRole('button', { name: /refresh/i })
+ expect(refreshButton).toBeInTheDocument()
+
+ fireEvent.click(refreshButton)
+ expect(onRefresh).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not render Refresh button when onRefresh is not provided', () => {
+ render()
+ expect(screen.queryByRole('button', { name: /refresh/i })).not.toBeInTheDocument()
+ })
+
+ it('shows expired warning when quote is expired', () => {
+ const expiredQuote = {
+ ...mockQuote,
+ expiresAt: new Date(Date.now() - 10_000).toISOString(),
+ }
+ render()
+ expect(screen.getByText(/This quote has expired/i)).toBeInTheDocument()
+ })
+})
diff --git a/src/hooks/useSendPayment.ts b/src/hooks/useSendPayment.ts
index 5e954ba..d812dff 100644
--- a/src/hooks/useSendPayment.ts
+++ b/src/hooks/useSendPayment.ts
@@ -159,6 +159,26 @@ export function useSendPayment() {
[isConnected, publicKey, supportedAssets, quoteMutation],
)
+ const refreshQuote = useCallback(() => {
+ if (!state.quote || !state.formValues) return
+
+ if (state.quote.id) {
+ setState((s) => ({ ...s, step: 'quoting', error: null }))
+ quoteApi
+ .refreshQuote(state.quote.id)
+ .then((newQuote) => {
+ setState((s) => ({ ...s, step: 'review', quote: newQuote, error: null }))
+ })
+ .catch(() => {
+ if (state.formValues) {
+ requestQuote(state.formValues)
+ }
+ })
+ } else if (state.formValues) {
+ requestQuote(state.formValues)
+ }
+ }, [state.quote, state.formValues, requestQuote])
+
const confirmSend = useCallback(() => {
sendMutation.mutate()
}, [sendMutation])
@@ -186,6 +206,7 @@ export function useSendPayment() {
return {
state,
requestQuote,
+ refreshQuote,
confirmSend,
reset,
goBack,
diff --git a/src/pages/PayRequest.tsx b/src/pages/PayRequest.tsx
index 4990e08..07be189 100644
--- a/src/pages/PayRequest.tsx
+++ b/src/pages/PayRequest.tsx
@@ -23,6 +23,7 @@ export default function PayRequest() {
const {
state,
requestQuote,
+ refreshQuote,
confirmSend,
reset,
goBack,
@@ -141,7 +142,14 @@ export default function PayRequest() {
)}
{state.step === 'review' && state.quote && (
-
+
)}
{state.quote && state.formValues && (
diff --git a/src/pages/Send.tsx b/src/pages/Send.tsx
index 1986652..5656872 100644
--- a/src/pages/Send.tsx
+++ b/src/pages/Send.tsx
@@ -76,6 +76,7 @@ export default function Send() {
const {
state,
requestQuote,
+ refreshQuote,
confirmSend,
reset,
goBack,
@@ -152,6 +153,7 @@ export default function Send() {