diff --git a/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx b/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx index fdf4c6108..f6ff2041d 100644 --- a/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx +++ b/src/components/AddMoney/hooks/__tests__/useMantecaDepositPolling.test.tsx @@ -53,13 +53,31 @@ describe('useMantecaDepositPolling', () => { await waitFor(() => expect(result.current.status).toBe('failed')) }) - it('reports "processing" for an intermediate settling status', async () => { - mockGetDepositStatus.mockResolvedValue({ data: { id: 'dep-4', status: 'PROCESSING' } }) + it('keeps "pending" on PROCESSING at stage 1 — QR live, user has NOT paid (the vanished-QR bug)', async () => { + // Manteca flips the synthetic ACTIVE (→ intent PROCESSING) seconds after + // creation; treating that as "settling" hid the QR before anyone could pay. + mockGetDepositStatus.mockResolvedValue({ data: { id: 'dep-4', status: 'PROCESSING', stage: 1 } }) const { result } = renderHook(() => useMantecaDepositPolling('dep-4', jest.fn()), { wrapper }) + await waitFor(() => expect(mockGetDepositStatus).toHaveBeenCalledWith('dep-4')) + expect(result.current.status).toBe('pending') + }) + + it('reports "processing" only once stage >= 2 (fiat received)', async () => { + mockGetDepositStatus.mockResolvedValue({ data: { id: 'dep-5', status: 'PROCESSING', stage: 2 } }) + const { result } = renderHook(() => useMantecaDepositPolling('dep-5', jest.fn()), { wrapper }) + await waitFor(() => expect(result.current.status).toBe('processing')) }) + it('degrades to "pending" when stage is absent (older BE / legacy row)', async () => { + mockGetDepositStatus.mockResolvedValue({ data: { id: 'dep-6', status: 'PROCESSING', stage: null } }) + const { result } = renderHook(() => useMantecaDepositPolling('dep-6', jest.fn()), { wrapper }) + + await waitFor(() => expect(mockGetDepositStatus).toHaveBeenCalledWith('dep-6')) + expect(result.current.status).toBe('pending') + }) + it('does not query when depositId is undefined', () => { const { result } = renderHook(() => useMantecaDepositPolling(undefined, jest.fn()), { wrapper }) diff --git a/src/components/AddMoney/hooks/useMantecaDepositPolling.ts b/src/components/AddMoney/hooks/useMantecaDepositPolling.ts index eee0b3ff4..ae9766b3d 100644 --- a/src/components/AddMoney/hooks/useMantecaDepositPolling.ts +++ b/src/components/AddMoney/hooks/useMantecaDepositPolling.ts @@ -8,8 +8,11 @@ const POLLING_INTERVAL = 5000 // Terminal TransactionIntentStatus values returned by GET /manteca/deposit/:id/status. const TERMINAL_STATUSES = ['COMPLETED', 'FAILED', 'CANCELLED', 'REFUNDED'] -// Non-terminal states that mean "payment detected, settling" — show a processing screen. -const PROCESSING_STATUSES = ['PROCESSING', 'AWAITING_SETTLEMENT'] +// "Payment detected" is signalled by the synthetic's stage, NOT the intent status: +// Manteca flips a ramp-on to ACTIVE (→ intent PROCESSING) seconds after creation, +// while the user hasn't paid — stage 1 is DEPOSIT (QR live, awaiting fiat), and +// only stage >= 2 (ORDER/WITHDRAW) means the fiat actually arrived. +const PAID_STAGE = 2 type MantecaDepositPollStatus = 'pending' | 'processing' | 'completed' | 'failed' @@ -34,9 +37,12 @@ export function useMantecaDepositPolling(depositId: string | undefined, onComple const status: MantecaDepositPollStatus = useMemo(() => { const s = data?.data?.status + const stage = data?.data?.stage if (s === 'COMPLETED') return 'completed' if (s && TERMINAL_STATUSES.includes(s)) return 'failed' - if (s && PROCESSING_STATUSES.includes(s)) return 'processing' + // stage may be null/absent (older BE, legacy row) — then stay 'pending' + // so the QR keeps showing rather than stranding the user on a loader. + if (typeof stage === 'number' && stage >= PAID_STAGE) return 'processing' return 'pending' }, [data]) diff --git a/src/services/manteca.ts b/src/services/manteca.ts index 2429dd3b0..d70742fb7 100644 --- a/src/services/manteca.ts +++ b/src/services/manteca.ts @@ -287,7 +287,10 @@ export const mantecaApi = { } }, - getDepositStatus: async (depositId: string): Promise<{ data?: { id: string; status: string }; error?: string }> => { + getDepositStatus: async ( + depositId: string + // stage: 1 = QR live awaiting fiat, >= 2 = fiat received (settling), null = no snapshot + ): Promise<{ data?: { id: string; status: string; stage?: number | null }; error?: string }> => { try { const response = await serverFetch(`/manteca/deposit/${depositId}/status`) const data = await response.json()