diff --git a/src/components/AddMoney/components/AddMoneyBankDetails.tsx b/src/components/AddMoney/components/AddMoneyBankDetails.tsx index 7cbf2592d..12e3bac39 100644 --- a/src/components/AddMoney/components/AddMoneyBankDetails.tsx +++ b/src/components/AddMoney/components/AddMoneyBankDetails.tsx @@ -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) @@ -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.` @@ -273,15 +278,12 @@ Please use these details to complete your bank transfer.`

Deposit reference

-

- {onrampData?.depositInstructions?.depositMessage?.slice(0, 10) || 'Loading...'} + {/* break-all handles the visual overflow of the full reference */} +

+ {depositReference || 'Loading...'}

- {onrampData?.depositInstructions?.depositMessage && ( - + {depositReference && ( + )}
@@ -416,7 +418,7 @@ 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)`, ]} /> @@ -424,14 +426,18 @@ Please use these details to complete your bank transfer.` I've sent the transfer - - Share Details - + {/* No share until the reference exists — a shared blob with a + missing reference produces an unreconcilable wire */} + {depositReference && ( + + Share Details + + )} ) diff --git a/src/components/AddMoney/components/__tests__/AddMoneyBankDetails.test.tsx b/src/components/AddMoney/components/__tests__/AddMoneyBankDetails.test.tsx new file mode 100644 index 000000000..83ce2b2a4 --- /dev/null +++ b/src/components/AddMoney/components/__tests__/AddMoneyBankDetails.test.tsx @@ -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: () =>
})) +jest.mock('@/components/Global/Card', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) +jest.mock('@/components/Global/CopyToClipboard', () => ({ + __esModule: true, + default: ({ textToCopy }: { textToCopy: string }) =>
, +})) +jest.mock('@/components/Global/InfoCard', () => ({ + __esModule: true, + default: ({ title, description, items }: { title?: string; description?: string; items?: string[] }) => ( +
+ {title} + {description} + {items?.map((item) => ( +

{item}

+ ))} +
+ ), +})) +jest.mock('@/components/Payment/PaymentInfoRow', () => ({ + PaymentInfoRow: ({ label, value }: { label: React.ReactNode; value: React.ReactNode }) => ( +
+ {label}: {value} +
+ ), +})) +jest.mock('@/components/0_Bruddle/Button', () => ({ + Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), +})) +let capturedGenerateText: (() => Promise) | undefined +jest.mock('@/components/Global/ShareButton', () => ({ + __esModule: true, + default: (props: { generateText: () => Promise }) => { + capturedGenerateText = props.generateText + return
+ }, +})) + +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() + + // 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() + + expect(screen.getByText(`Reference: ${FULL_REFERENCE} (included)`)).toBeInTheDocument() + }) + + it('includes the full reference in the shareable bank-details text', async () => { + render() + + 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() + + // 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...') + }) +})