diff --git a/apps/dashboard/.gitignore b/apps/dashboard/.gitignore index 290bcd476..aed807893 100644 --- a/apps/dashboard/.gitignore +++ b/apps/dashboard/.gitignore @@ -5,3 +5,4 @@ dist .tanstack dev-boot.log *.log +**/.env diff --git a/apps/dashboard/e2e/auth-gate.spec.ts b/apps/dashboard/e2e/auth-gate.spec.ts index 11b172d7a..fc99ceb35 100644 --- a/apps/dashboard/e2e/auth-gate.spec.ts +++ b/apps/dashboard/e2e/auth-gate.spec.ts @@ -37,6 +37,18 @@ test("a seeded session renders the app shell instead of redirecting", async ({ p expect(backend.unexpectedExternalRequests).toEqual([]); }); +test("an expired access token refreshes before the dashboard renders", async ({ page }) => { + const backend = await mockBackend(page, { verifyTokenValid: false }); + const expiredToken = `header.${Buffer.from(JSON.stringify({ exp: 1 })).toString("base64url")}.signature`; + await seedSession(page, expiredToken); + + await page.goto("/overview"); + + await expect(page.getByRole("heading", { name: "Onboarding" })).toBeVisible({ timeout: 20_000 }); + await expect(page).toHaveURL(/\/overview/); + expect(backend.auth.refreshes).toBe(1); +}); + test("a started onboarding account renders without crashing the overview", async ({ page }) => { await mockBackend(page, { onboardingState: "started" }); await seedSession(page); diff --git a/apps/dashboard/e2e/onboarding-alfredpay-mxn.spec.ts b/apps/dashboard/e2e/onboarding-alfredpay-mxn.spec.ts index 9a697d4da..28e5ab88e 100644 --- a/apps/dashboard/e2e/onboarding-alfredpay-mxn.spec.ts +++ b/apps/dashboard/e2e/onboarding-alfredpay-mxn.spec.ts @@ -86,7 +86,7 @@ test("Alfredpay MX KYC: continuing in the background, the card reacts to approva backend.kyc.approved = true; // No interaction, no reload — the 15s onboarding-status refetch flips the card to approved. - await expect(page.getByRole("button", { name: "View payout accounts" })).toBeVisible({ timeout: 25_000 }); + await expect(page.getByRole("button", { name: "View pay-out accounts" })).toBeVisible({ timeout: 25_000 }); }); test("Alfredpay MX KYC: closing on the details form and reloading keeps the card resumable", async ({ page }) => { diff --git a/apps/dashboard/e2e/onramp-journeys.spec.ts b/apps/dashboard/e2e/onramp-journeys.spec.ts index b000c49cc..4ec0932c9 100644 --- a/apps/dashboard/e2e/onramp-journeys.spec.ts +++ b/apps/dashboard/e2e/onramp-journeys.spec.ts @@ -13,17 +13,17 @@ const CASES = [ { currency: "ARS", expected: "CVU" } ] as const; -test("transfer modes are route-backed and Cross-border is complete", async ({ page }) => { +test("transfer modes are route-backed and Fiat-to-Fiat is complete", async ({ page }) => { const backend = await mockBackend(page); await seedSession(page); await page.goto("/transfer"); - await page.getByRole("tab", { name: "Cross-border" }).click(); + await page.getByRole("tab", { name: "Fiat-to-Fiat" }).click(); await expect(page).toHaveURL(/mode=cross-border/); - await expect(page.getByText("Cross-border transfers are coming soon")).toBeVisible(); + await expect(page.getByText("Fiat-to-Fiat transfers are coming soon")).toBeVisible(); await page.reload(); - await expect(page.getByRole("tab", { name: "Cross-border" })).toHaveAttribute("data-state", "active"); + await expect(page.getByRole("tab", { name: "Fiat-to-Fiat" })).toHaveAttribute("data-state", "active"); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); }); @@ -231,7 +231,7 @@ for (const journey of CASES) { await page.getByRole("button", { name: "I have made the payment" }).click(); await expect.poll(() => backend.startRequests.length).toBe(1); await expect(page).toHaveURL(/\/transactions/); - await expect(page.getByText("Onramp", { exact: true })).toBeVisible(); + await expect(page.getByText("Pay-in", { exact: true })).toBeVisible(); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); }); diff --git a/apps/dashboard/e2e/payout-accounts.spec.ts b/apps/dashboard/e2e/payout-accounts.spec.ts index 36a771f4b..a960237ea 100644 --- a/apps/dashboard/e2e/payout-accounts.spec.ts +++ b/apps/dashboard/e2e/payout-accounts.spec.ts @@ -8,23 +8,23 @@ test("an approved AlfredPay corridor adds a self payout account", async ({ page await page.goto("/overview"); const card = page.getByTestId("corridor-card-MX"); - await expect(card.getByRole("button", { name: "Add payout account" })).toBeVisible({ timeout: 20_000 }); - await expect(card.getByText(/to enable reception of money through offramps/)).toBeVisible(); + await expect(card.getByRole("button", { name: "Add pay-out account" })).toBeVisible({ timeout: 20_000 }); + await expect(card.getByText(/to enable reception of money through pay-outs/)).toBeVisible(); const progress = card.getByRole("progressbar", { name: "Mexico onboarding progress" }); await expect(progress).toHaveAttribute("aria-valuenow", "90"); await expect(progress.locator('[data-slot="progress-indicator"]')).toHaveClass(/bg-primary/); - await card.getByRole("button", { name: "Add payout account" }).click(); + await card.getByRole("button", { name: "Add pay-out account" }).click(); const dialog = page.getByRole("dialog"); - await expect(dialog.getByText(/enables reception of money through offramps/)).toBeVisible(); + await expect(dialog.getByText(/enables reception of money through pay-outs/)).toBeVisible(); await dialog.getByLabel("CLABE").fill("646180157000000004"); await dialog.getByLabel("Account holder name").fill("Vortex E2E CLABE"); - await dialog.getByRole("button", { name: "Save payout account" }).click(); + await dialog.getByRole("button", { name: "Save pay-out account" }).click(); await page.keyboard.press("Escape"); await expect(dialog).toBeHidden(); - await expect(card.getByRole("button", { name: "View payout accounts" })).toBeVisible(); + await expect(card.getByRole("button", { name: "View pay-out accounts" })).toBeVisible(); await expect(progress).toHaveAttribute("aria-valuenow", "100"); await expect(progress.locator('[data-slot="progress-indicator"]')).toHaveClass(/bg-success/); expect(backend.fiatAccountRequests).toEqual([ @@ -55,21 +55,21 @@ test("a provider-rejected payout account surfaces field and form errors instead await page.goto("/overview"); const card = page.getByTestId("corridor-card-MX"); - await card.getByRole("button", { name: "Add payout account" }).click(); + await card.getByRole("button", { name: "Add pay-out account" }).click(); const dialog = page.getByRole("dialog"); await dialog.getByLabel("CLABE").fill("646180157000000004"); await dialog.getByLabel("Account holder name").fill("Vortex E2E CLABE"); - await dialog.getByRole("button", { name: "Save payout account" }).click(); + await dialog.getByRole("button", { name: "Save pay-out account" }).click(); await expect(dialog.getByText("This account number was rejected by the payout provider.")).toBeVisible(); await expect( dialog.getByText("The payout provider rejected this account number. Double-check it and try again.") ).toBeVisible(); // The form stays open for correction and nothing was added. - await expect(dialog.getByRole("button", { name: "Save payout account" })).toBeEnabled(); + await expect(dialog.getByRole("button", { name: "Save pay-out account" })).toBeEnabled(); await page.keyboard.press("Escape"); await expect(dialog).toBeHidden(); - await expect(card.getByRole("button", { name: "Add payout account" })).toBeVisible(); + await expect(card.getByRole("button", { name: "Add pay-out account" })).toBeVisible(); expect(backend.fiatAccountRequests).toHaveLength(1); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); @@ -91,7 +91,7 @@ test("an approved AlfredPay corridor removes a payout account after confirmation await page.goto("/overview"); const card = page.getByTestId("corridor-card-MX"); - await card.getByRole("button", { name: "View payout accounts" }).click(); + await card.getByRole("button", { name: "View pay-out accounts" }).click(); const dialog = page.getByRole("dialog"); const remove = dialog.getByRole("button", { name: "Remove account ending in 0004" }); await remove.click(); @@ -104,7 +104,7 @@ test("an approved AlfredPay corridor removes a payout account after confirmation await expect(dialog.getByText("Vortex E2E CLABE", { exact: false })).toBeHidden(); await page.keyboard.press("Escape"); await expect(dialog).toBeHidden(); - await expect(card.getByRole("button", { name: "Add payout account" })).toBeVisible(); + await expect(card.getByRole("button", { name: "Add pay-out account" })).toBeVisible(); expect(backend.fiatAccountDeleteRequests).toEqual([{ country: "MX", fiatAccountId: "fiat-account-to-delete" }]); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); diff --git a/apps/dashboard/e2e/quote-explorer.spec.ts b/apps/dashboard/e2e/quote-explorer.spec.ts index a31b05e75..15dc36cdf 100644 --- a/apps/dashboard/e2e/quote-explorer.spec.ts +++ b/apps/dashboard/e2e/quote-explorer.spec.ts @@ -14,9 +14,13 @@ test("EUR is quotable on BUY and an unapproved corridor routes to onboarding", a await expect.poll(() => backend.quoteRequests.length, { timeout: 20_000 }).toBeGreaterThan(0); expect(backend.quoteRequests.at(-1)).toMatchObject({ inputCurrency: "EUR", paymentMethod: "sepa", rampType: "BUY" }); - const onboardingCta = page.getByRole("link", { name: "Get approved for Europe" }); + const onboardingCta = page.getByRole("link", { name: "Start onboarding for Europe" }); await expect(onboardingCta).toBeVisible({ timeout: 20_000 }); await expect(onboardingCta).toHaveAttribute("href", "/overview?onboarding=EU"); + await onboardingCta.click(); + + await expect(page).toHaveURL(/\/overview\?onboarding=EU$/); + await expect(page.getByRole("dialog").getByText("KYC is currently disabled in Europe.")).toBeVisible(); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); diff --git a/apps/dashboard/e2e/support/mockBackend.ts b/apps/dashboard/e2e/support/mockBackend.ts index 593a4e092..43cc0dd9c 100644 --- a/apps/dashboard/e2e/support/mockBackend.ts +++ b/apps/dashboard/e2e/support/mockBackend.ts @@ -256,6 +256,7 @@ interface MockBackendOptions { selectionRequired?: boolean; // Full response for POST /v1/auth/verify-otp. Default: a successful session. verifyOtp?: (requestBody: Record) => { status: number; body: unknown }; + verifyTokenValid?: boolean; // How many GET /v1/ramp/:id polls report an in-progress ramp before flipping to COMPLETE. pendingStatusPolls?: number; // Drives the Alfredpay MX KYC endpoints and, unless reflectOnboarding is false, makes @@ -475,6 +476,10 @@ export async function mockBackend(page: Page, options: MockBackendOptions = {}) await fulfillJson(result.body, result.status); return; } + if (path === "/v1/auth/verify" && method === "POST") { + await fulfillJson({ user_id: E2E_USER_ID, valid: options.verifyTokenValid !== false }); + return; + } if (path === "/v1/auth/refresh" && method === "POST") { auth.refreshes += 1; await fulfillJson({ access_token: "e2e-access-token", refresh_token: "e2e-refresh-token", success: true }); diff --git a/apps/dashboard/e2e/support/session.ts b/apps/dashboard/e2e/support/session.ts index c77497af7..aa7946e16 100644 --- a/apps/dashboard/e2e/support/session.ts +++ b/apps/dashboard/e2e/support/session.ts @@ -14,18 +14,17 @@ export const SESSION_KEYS = { } as const; /** - * Boots the app already authenticated: useAuthStore reads the session from localStorage at - * module init, so it has to be there before any app script runs. The access token is not a - * JWT on purpose — AuthService.isAuthenticated() treats an undecodable expiry as valid. + * Boots the app with a stored session before any app script runs. The default access token is + * not a JWT on purpose so tests can opt into specific expiry behavior when needed. */ -export async function seedSession(page: Page) { +export async function seedSession(page: Page, accessToken = "e2e-access-token") { await page.addInitScript( - ({ email, keys, userId }) => { - localStorage.setItem(keys.accessToken, "e2e-access-token"); + ({ accessToken: token, email, keys, userId }) => { + localStorage.setItem(keys.accessToken, token); localStorage.setItem(keys.refreshToken, "e2e-refresh-token"); localStorage.setItem(keys.userId, userId); localStorage.setItem(keys.userEmail, email); }, - { email: E2E_USER_EMAIL, keys: SESSION_KEYS, userId: E2E_USER_ID } + { accessToken, email: E2E_USER_EMAIL, keys: SESSION_KEYS, userId: E2E_USER_ID } ); } diff --git a/apps/dashboard/e2e/transactions.spec.ts b/apps/dashboard/e2e/transactions.spec.ts index c0a70650b..096eac1ba 100644 --- a/apps/dashboard/e2e/transactions.spec.ts +++ b/apps/dashboard/e2e/transactions.spec.ts @@ -38,16 +38,16 @@ test("transaction destinations and failure help are direction-aware", async ({ p await seedSession(page); await page.goto("/transactions"); - const onrampRow = page.getByRole("row").filter({ hasText: "Onramp" }); + const onrampRow = page.getByRole("row").filter({ hasText: "Pay-in" }); await expect(onrampRow).toContainText("0x1111…1111"); await onrampRow.getByRole("button", { name: "Get help" }).click(); - await expect(page.getByText("We'll email you about this failed onramp.", { exact: true })).toBeVisible(); + await expect(page.getByText("We'll email you about this failed pay-in.", { exact: true })).toBeVisible(); - const offrampRow = page.getByRole("row").filter({ hasText: "Offramp" }); - await expect(offrampRow).toContainText("Payout account"); + const offrampRow = page.getByRole("row").filter({ hasText: "Pay-out" }); + await expect(offrampRow).toContainText("Pay-out account"); await expect(offrampRow).not.toContainText("0x2222…2222"); await offrampRow.getByRole("button", { name: "Get help" }).click(); - await expect(page.getByText("We'll email you about this failed payout.", { exact: true })).toBeVisible(); + await expect(page.getByText("We'll email you about this failed pay-out.", { exact: true })).toBeVisible(); expect(backend.unmatchedRequests).toEqual([]); expect(backend.unexpectedExternalRequests).toEqual([]); diff --git a/apps/dashboard/src/components/onboarding/PayoutAccountsSection.tsx b/apps/dashboard/src/components/onboarding/PayoutAccountsSection.tsx index 61d1a1d45..fcae75734 100644 --- a/apps/dashboard/src/components/onboarding/PayoutAccountsSection.tsx +++ b/apps/dashboard/src/components/onboarding/PayoutAccountsSection.tsx @@ -23,19 +23,19 @@ export function PayoutAccountsSection({ accounts, corridorId, error, isLoading, } if (isLoading) { - return

Loading payout accounts…

; + return

Loading pay-out accounts…

; } if (isApiError(error) && error.status === 404) { return (

- Payout setup not available yet — finish verification first. + Pay-out setup not available yet — finish verification first.

); } if (error) { return (
-

Couldn't load payout accounts.

+

Couldn't load pay-out accounts.

@@ -49,16 +49,16 @@ export function PayoutAccountsSection({ accounts, corridorId, error, isLoading, {savedAccounts.length === 0 ? ( <>

- Add a payout account to enable reception of money through offramps. Onramps and third-party payments work without + Add a pay-out account to enable reception of money through pay-outs. Pay-ins and third-party payments work without one.

) : ( )} - Payout accounts + Pay-out accounts - Accounts available for receiving money through {config.methodLabel}. Payout accounts are not needed for onramps + Accounts available for receiving money through {config.methodLabel}. Payout accounts are not needed for pay-ins or payments to third-party recipients. @@ -105,7 +105,7 @@ export function FiatAccountDialog({ accounts, corridorId, onOpenChange, onViewCh

- {account.metadata?.accountHolderName || account.accountName || "Payout account"} · •••• + {account.metadata?.accountHolderName || account.accountName || "Pay-out account"} · •••• {account.accountNumber.slice(-4)}

{config.methodLabel}

@@ -138,9 +138,9 @@ export function FiatAccountDialog({ accounts, corridorId, onOpenChange, onViewCh ) : ( <> - Add payout account + Add pay-out account - This account enables reception of money through offramps. Onramps and payments to third-party recipients work + This account enables reception of money through pay-outs. Pay-ins and payments to third-party recipients work without one. @@ -204,7 +204,7 @@ export function FiatAccountDialog({ accounts, corridorId, onOpenChange, onViewCh )} diff --git a/apps/dashboard/src/components/quote/QuoteExplorer.tsx b/apps/dashboard/src/components/quote/QuoteExplorer.tsx index a219dcbc5..16bbc2187 100644 --- a/apps/dashboard/src/components/quote/QuoteExplorer.tsx +++ b/apps/dashboard/src/components/quote/QuoteExplorer.tsx @@ -305,7 +305,7 @@ function QuoteCta({ amount, corridorId, isApproved, isBuy, network, token }: Quo diff --git a/apps/dashboard/src/components/recipients/RecipientDialog.tsx b/apps/dashboard/src/components/recipients/RecipientDialog.tsx index d7078d14d..464f47a10 100644 --- a/apps/dashboard/src/components/recipients/RecipientDialog.tsx +++ b/apps/dashboard/src/components/recipients/RecipientDialog.tsx @@ -155,7 +155,7 @@ export function RecipientDialog({ Add a recipient Choose who you're paying and name the link. We'll generate an invite link you send them yourself — they complete - KYC/KYB and add their payout details to receive transfers. + KYC/KYB and add their pay-out details to receive transfers.
@@ -289,7 +289,7 @@ function InviteShare({ created, onDone }: { created: CreatedInvite; onDone: () = Invite link ready - Send this {created.corridorName} link to your recipient. They'll complete KYC/KYB and add their payout details — then + Send this {created.corridorName} link to your recipient. They'll complete KYC/KYB and add their pay-out details — then you can pay them. diff --git a/apps/dashboard/src/components/recipients/RecipientsTable.tsx b/apps/dashboard/src/components/recipients/RecipientsTable.tsx index ffcdffeb7..0084d189a 100644 --- a/apps/dashboard/src/components/recipients/RecipientsTable.tsx +++ b/apps/dashboard/src/components/recipients/RecipientsTable.tsx @@ -27,7 +27,7 @@ export function RecipientsTable({ recipients }: { recipients: Recipient[] }) { Recipient Country - Payout + Pay-out Status Action diff --git a/apps/dashboard/src/components/transactions/TransactionsTable.tsx b/apps/dashboard/src/components/transactions/TransactionsTable.tsx index cdcb986a6..fe6979e6b 100644 --- a/apps/dashboard/src/components/transactions/TransactionsTable.tsx +++ b/apps/dashboard/src/components/transactions/TransactionsTable.tsx @@ -74,7 +74,7 @@ export function TransactionsTable({ month: "short" })} - {tx.direction === "BUY" ? "Onramp" : "Offramp"} + {tx.direction === "BUY" ? "Pay-in" : "Pay-out"}
@@ -127,7 +127,7 @@ export function TransactionsTable({ } function FailedAction({ direction, reason }: { direction: Transaction["direction"]; reason?: string }) { - const transferLabel = direction === "BUY" ? "onramp" : "payout"; + const transferLabel = direction === "BUY" ? "pay-in" : "pay-out"; return ( diff --git a/apps/dashboard/src/components/transfer/OnrampForm.tsx b/apps/dashboard/src/components/transfer/OnrampForm.tsx index 810640539..f5f11e3fb 100644 --- a/apps/dashboard/src/components/transfer/OnrampForm.tsx +++ b/apps/dashboard/src/components/transfer/OnrampForm.tsx @@ -128,7 +128,7 @@ export function OnrampForm({ account, prefill }: { account: SenderAccount; prefi

Another account has an unfinished transfer

-

Switch back to that account to resume it before starting a new onramp.

+

Switch back to that account to resume it before starting a new pay-in.

); @@ -163,7 +163,7 @@ export function OnrampForm({ account, prefill }: { account: SenderAccount; prefi return (
-

Complete onboarding for a supported corridor before starting an onramp.

+

Complete onboarding for a supported corridor before starting a pay-in.

); } @@ -283,7 +283,7 @@ export function OnrampForm({ account, prefill }: { account: SenderAccount; prefi {error ? (
-

We couldn’t fetch an onramp quote right now. Try another amount or token.

+

We couldn’t fetch a pay-in quote right now. Try another amount or token.

) : Number(amount) <= 0 ? (

diff --git a/apps/dashboard/src/components/transfer/OnrampPaymentInstructions.tsx b/apps/dashboard/src/components/transfer/OnrampPaymentInstructions.tsx index 006aa0bdd..fe4014e70 100644 --- a/apps/dashboard/src/components/transfer/OnrampPaymentInstructions.tsx +++ b/apps/dashboard/src/components/transfer/OnrampPaymentInstructions.tsx @@ -100,12 +100,12 @@ export function OnrampPaymentInstructions({ ramp }: { ramp: RampProcess }) { const subscription = transferActor.subscribe(snapshot => { if (snapshot.matches("Tracking")) { subscription.unsubscribe(); - toast.success("Onramp initiated", { description: "We’ll update your transaction as the payment settles." }); + toast.success("Pay-in initiated", { description: "We’ll update your transaction as the payment settles." }); navigate({ to: "/transactions" }); } else if (snapshot.matches("AwaitingPayment") || snapshot.matches("Failed")) { // A failed start returns to AwaitingPayment so the ramp and instructions survive. subscription.unsubscribe(); - toast.error("Could not start onramp", { description: snapshot.context.errorMessage ?? undefined }); + toast.error("Could not start pay-in", { description: snapshot.context.errorMessage ?? undefined }); } }); transferActor.send({ type: "PAYMENT_CONFIRMED" }); diff --git a/apps/dashboard/src/components/transfer/TransferForm.tsx b/apps/dashboard/src/components/transfer/TransferForm.tsx index 31e9a9490..edc9125f7 100644 --- a/apps/dashboard/src/components/transfer/TransferForm.tsx +++ b/apps/dashboard/src/components/transfer/TransferForm.tsx @@ -196,7 +196,7 @@ export function TransferForm({ account, prefill, recipients, preselectRecipientI })} -

Choose one of your approved payout accounts.

+

Choose one of your approved pay-out accounts.

{selected && !isSendable && ( @@ -229,7 +229,7 @@ export function TransferForm({ account, prefill, recipients, preselectRecipientI {corridor.flag} {corridor.name} - + {PAYMENT_METHOD_LABEL[selected.bankDetails.method]} · {selected.bankDetails.value}
@@ -251,7 +251,7 @@ export function TransferForm({ account, prefill, recipients, preselectRecipientI
- +