Skip to content
Closed
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
42 changes: 24 additions & 18 deletions src/components/AddMoney/components/AddMoneyBankDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export default function AddMoneyBankDetails(props: AddMoneyBankDetailsProps) {
const amount = isAddMoneyFlow ? (amountFromUrl ?? '') : requestFulfilmentOnrampData?.depositInstructions?.amount
const onrampData = isAddMoneyFlow ? onrampContext.onrampData : requestFulfilmentOnrampData

// The single source for every surface that hands the reference to the user
// (display, copy, share, summary). Bridge can't reconcile a wire sent with
// anything short of the full depositMessage — never truncate or format it.
const depositReference = onrampData?.depositInstructions?.depositMessage

const currencySymbolBasedOnCountry = useMemo(() => {
// symbol of the detected onramp currency (e.g., €, $)
return getCurrencySymbol(onrampCurrency)
Expand Down Expand Up @@ -236,7 +241,7 @@ ${routingLabel}: ${routingValue}`
}

bankDetails += `
Deposit Reference: ${onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'}
Deposit Reference: ${depositReference}

Please use these details to complete your bank transfer.`

Expand Down Expand Up @@ -273,15 +278,12 @@ Please use these details to complete your bank transfer.`
<Card className="p-4">
<p className="text-xs font-normal text-gray-1">Deposit reference</p>
<div className="flex items-baseline gap-2">
<p className="text-xl font-extrabold text-black md:text-4xl">
{onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'}
{/* break-all handles the visual overflow of the full reference */}
<p className="break-all text-xl font-extrabold text-black md:text-4xl">
{depositReference || 'Loading...'}
</p>
{onrampData?.depositInstructions?.depositMessage && (
<CopyToClipboard
textToCopy={onrampData.depositInstructions.depositMessage?.slice(0, 10)}
fill="black"
iconSize="4"
/>
{depositReference && (
<CopyToClipboard textToCopy={depositReference} fill="black" iconSize="4" />
)}
</div>

Expand Down Expand Up @@ -416,22 +418,26 @@ Please use these details to complete your bank transfer.`
title="Double check in your bank before sending:"
items={[
`Amount: ${formattedCurrencyAmount} (exact)`,
`Reference: ${onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'} (included)`,
`Reference: ${depositReference || 'Loading...'} (included)`,
]}
/>

<Button onClick={() => router.push('/home')} variant="purple" className="w-full" shadowSize="4">
I've sent the transfer
</Button>

<ShareButton
generateText={generateBankDetails}
title="Bank Transfer Details"
variant="primary-soft"
className="w-full"
>
Share Details
</ShareButton>
{/* No share until the reference exists — a shared blob with a
missing reference produces an unreconcilable wire */}
{depositReference && (
<ShareButton
generateText={generateBankDetails}
title="Bank Transfer Details"
variant="primary-soft"
className="w-full"
>
Share Details
</ShareButton>
)}
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* AddMoneyBankDetails — the Bridge bank-transfer instructions screen.
*
* Regression tests for the deposit-reference truncation bug: the reference was
* sliced to 10 chars in display AND copy, so users wired funds with a reference
* Bridge couldn't reconcile (deposit stuck AWAITING_FUNDS). Every surface that
* hands the reference to the user must carry the FULL depositMessage. Nested
* primitives are stubbed so only this component's own logic is under test.
*/
import React from 'react'
import { render, screen } from '@testing-library/react'

jest.mock('next/navigation', () => ({
useRouter: () => ({ push: jest.fn(), replace: jest.fn() }),
useParams: () => ({ country: 'germany' }),
}))
jest.mock('nuqs', () => ({
useQueryState: () => ['100', jest.fn()],
parseAsString: {},
}))
jest.mock('@/context/OnrampFlowContext', () => ({
useOnrampFlow: () => mockUseOnrampFlow(),
}))
jest.mock('@/context/RequestFulfillmentFlowContext', () => ({
RequestFulfillmentBankFlowStep: { BankCountryList: 'BankCountryList' },
useRequestFulfillmentFlow: () => ({
setFlowStep: jest.fn(),
onrampData: undefined,
selectedCountry: undefined,
}),
}))
jest.mock('@/hooks/useOnrampQuote', () => ({
useOnrampQuote: () => ({ netRate: 1, isLoading: false }),
}))

jest.mock('@/components/Global/NavHeader', () => ({ __esModule: true, default: () => <div /> }))
jest.mock('@/components/Global/Card', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
jest.mock('@/components/Global/CopyToClipboard', () => ({
__esModule: true,
default: ({ textToCopy }: { textToCopy: string }) => <div data-testid="copy" data-text={textToCopy} />,
}))
jest.mock('@/components/Global/InfoCard', () => ({
__esModule: true,
default: ({ title, description, items }: { title?: string; description?: string; items?: string[] }) => (
<div>
{title}
{description}
{items?.map((item) => (
<p key={item}>{item}</p>
))}
</div>
),
}))
jest.mock('@/components/Payment/PaymentInfoRow', () => ({
PaymentInfoRow: ({ label, value }: { label: React.ReactNode; value: React.ReactNode }) => (
<div>
{label}: {value}
</div>
),
}))
jest.mock('@/components/0_Bruddle/Button', () => ({
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button onClick={onClick}>{children}</button>
),
}))
let capturedGenerateText: (() => Promise<string>) | undefined
jest.mock('@/components/Global/ShareButton', () => ({
__esModule: true,
default: (props: { generateText: () => Promise<string> }) => {
capturedGenerateText = props.generateText
return <div data-testid="share-button" />
},
}))

const mockUseOnrampFlow = jest.fn()

// import must come after jest.mock
import AddMoneyBankDetails from '../AddMoneyBankDetails'

// 20 chars, the real shape of a Bridge deposit message — anything shorter than
// the full string is un-reconcilable by Bridge
const FULL_REFERENCE = 'BRGTESTREF1234567890'

const baseOnrampData = {
transferId: 'transfer-123',
depositInstructions: {
amount: '100',
currency: 'EUR',
depositMessage: FULL_REFERENCE,
bankName: 'Deutsche Bank',
bankAddress: 'Frankfurt, Germany',
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX',
accountHolderName: 'Peanut Protocol',
},
}

beforeEach(() => {
capturedGenerateText = undefined
mockUseOnrampFlow.mockReturnValue({ onrampData: baseOnrampData })
})

describe('AddMoneyBankDetails deposit reference', () => {
it('displays the full reference untruncated and copies the full reference', () => {
render(<AddMoneyBankDetails onBack={jest.fn()} />)

// display — the full 20-char reference, not the first 10 chars
expect(screen.getByText(FULL_REFERENCE)).toBeInTheDocument()
expect(screen.queryByText(FULL_REFERENCE.slice(0, 10))).not.toBeInTheDocument()

// the reference copy button carries the full value
const copyTexts = screen.getAllByTestId('copy').map((el) => el.getAttribute('data-text'))
expect(copyTexts).toContain(FULL_REFERENCE)
expect(copyTexts).not.toContain(FULL_REFERENCE.slice(0, 10))
})

it('includes the full reference in the "double check before sending" summary', () => {
render(<AddMoneyBankDetails onBack={jest.fn()} />)

expect(screen.getByText(`Reference: ${FULL_REFERENCE} (included)`)).toBeInTheDocument()
})

it('includes the full reference in the shareable bank-details text', async () => {
render(<AddMoneyBankDetails onBack={jest.fn()} />)

expect(capturedGenerateText).toBeDefined()
const details = await capturedGenerateText!()
expect(details).toContain(`Deposit Reference: ${FULL_REFERENCE}`)
})

it('offers no copy or share while the reference is still loading', () => {
mockUseOnrampFlow.mockReturnValue({
onrampData: {
transferId: 'transfer-123',
depositInstructions: { ...baseOnrampData.depositInstructions, depositMessage: undefined },
},
})
render(<AddMoneyBankDetails onBack={jest.fn()} />)

// the 'Loading...' placeholder must never leave the screen — no share
// blob, no copyable value carrying the literal placeholder
expect(screen.queryByTestId('share-button')).not.toBeInTheDocument()
const copyTexts = screen.getAllByTestId('copy').map((el) => el.getAttribute('data-text'))
expect(copyTexts).not.toContain('Loading...')
})
})
Loading