Skip to content
Merged
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
38 changes: 34 additions & 4 deletions src/components/account/accounts-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { Account } from '~/domain/accounts/types';
import { formatCOP, formatUSD, sortBalancesForDisplay } from '~/lib/formatters';
import { cn } from '~/lib/utils';
import { getProductKindIcon } from './product-presentation';

function formatAccountBalanceChip(asset: string, current: string): string {
Expand Down Expand Up @@ -61,10 +62,18 @@ export function AccountsCarousel({
accounts,
onSelectAccount,
onAddAccount,
highlightAdd = false,
}: {
accounts: Account[];
onSelectAccount: (urn: string) => void;
onAddAccount: () => void;
/**
* Highlights the "+ new account" card to guide a genuinely accountless
* user there. Callers must gate this on their accounts query having
* settled — `accounts` reads as `[]` while it's still loading too, and
* that must not read as "confirmed empty" (see BQE-2653).
*/
highlightAdd?: boolean;
}) {
const { t } = useTranslation();
return (
Expand All @@ -83,13 +92,34 @@ export function AccountsCarousel({
<button
type="button"
onClick={onAddAccount}
className="flex h-[6.5rem] w-[9.5rem] shrink-0 snap-center flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border bg-card/35 transition-colors hover:bg-muted"
className={cn(
'flex h-[6.5rem] w-[9.5rem] shrink-0 snap-center flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed bg-card/35 transition-colors hover:bg-muted',
highlightAdd ? 'border-primary/60' : 'border-border',
)}
aria-label={t('accountsCarousel.createNewAria')}
>
<div className="flex h-8 w-8 items-center justify-center rounded-2xl border border-border">
<Plus className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
<div
className={cn(
'flex h-8 w-8 items-center justify-center rounded-2xl border',
highlightAdd ? 'border-primary/60' : 'border-border',
)}
>
<Plus
className={cn(
'h-4 w-4',
highlightAdd ? 'text-primary' : 'text-muted-foreground',
)}
strokeWidth={1.5}
/>
</div>
<span className="text-[10px] font-medium text-muted-foreground">
<span
className={cn(
'text-[10px] font-medium',
highlightAdd
? 'text-primary underline underline-offset-2'
: 'text-muted-foreground',
)}
>
{t('accountsCarousel.newAccount')}
</span>
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/domain/accounts/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export type CreateBrebKeyInput = {

export type CreatePolygonAccountInput = { name?: string; ledgerId?: string };

export type CreateVirtualAccountInput = { name?: string };
export type CreateVirtualAccountInput = { name?: string; ledgerId?: string };

export type CreateExternalUsBankAccountInput = {
/** URL the hosted Plaid Link page redirects to once linking finishes. */
Expand Down
13 changes: 10 additions & 3 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@
"brebKeyLabel": "BRE-B key",
"newCard": "New card",
"polygonAccount": "Polygon account",
"newVirtualAccount": "New account",
"cardNameLabel": "Card name",
"optionalNameLabel": "Name (optional)",
"cardNamePlaceholder": "Personal",
"polygonNamePlaceholder": "Main",
"virtualAccountNamePlaceholder": "Personal",
"sharesBalanceOf": "It will share the balance of {{label}}.",
"defaultCardName": "Card",
"cardCreatedToast": "Card created successfully",
"polygonCreatedToast": "Polygon account created",
"virtualAccountCreatedToast": "Account created successfully",
"createProductErrorToast": "Couldn't create the product. Please try again.",
"transferToOwnAccount": "Transfer to another account",
"transferDrawerTitle": "Transfer funds",
Expand Down Expand Up @@ -328,7 +331,8 @@
"quickActions": {
"title": "Quick actions",
"topup": "Top up",
"send": "Send"
"send": "Send",
"needsAccount": "Create an account first to use this."
}
},
"send": {
Expand Down Expand Up @@ -396,7 +400,8 @@
"noRateToContinue": "No rate available to continue.",
"amountStep": {
"label": "Amount to send (COP)",
"minAmount": "Minimum amount: $10,000 COP"
"minAmount": "Minimum amount: $10,000 COP",
"feeLabel": "Fee"
}
},
"usBanks": {
Expand All @@ -405,6 +410,7 @@
"noRateForOrder": "No rate selected to create the order.",
"invalidAmountForOrder": "Invalid amount to create the order.",
"noSourceAccount": "No source account available.",
"kycRejectedError": "Your identity verification wasn't approved, so we can't complete this operation. Retry verification from your profile.",
"minAmountToast": "The minimum amount is $10 USD.",
"noRateToContinue": "No rate available to continue.",
"transferSentToast": "RTP transfer started successfully.",
Expand Down Expand Up @@ -514,7 +520,7 @@
"documentNumberPlaceholder": "E.g.: 123456789"
},
"confirmStep": {
"summary": "Top-up summary",
"summary": "Summary",
"sending": "Sending...",
"sendWithdrawal": "Send withdrawal",
"verifyDisclaimer": "Verify that the bank details are correct before sending."
Expand Down Expand Up @@ -567,6 +573,7 @@
"invalidAmountForOrder": "Invalid amount to create the order.",
"noSourceAccount": "We couldn't find a linked bank account to pull from.",
"noDestinationAccount": "We couldn't find an available destination account.",
"kycRejectedError": "Your identity verification wasn't approved, so we can't complete this operation. Retry verification from your profile.",
"minAmountToast": "The minimum amount is $10 USD.",
"noRateToContinue": "No rate available to continue.",
"topupStartedToast": "ACH top-up started successfully.",
Expand Down
13 changes: 10 additions & 3 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@
"brebKeyLabel": "Llave BRE-B",
"newCard": "Nueva tarjeta",
"polygonAccount": "Cuenta Polygon",
"newVirtualAccount": "Nueva cuenta",
"cardNameLabel": "Nombre de la tarjeta",
"optionalNameLabel": "Nombre (opcional)",
"cardNamePlaceholder": "Personal",
"polygonNamePlaceholder": "Principal",
"virtualAccountNamePlaceholder": "Personal",
"sharesBalanceOf": "Compartirá el saldo de {{label}}.",
"defaultCardName": "Tarjeta",
"cardCreatedToast": "Tarjeta creada exitosamente",
"polygonCreatedToast": "Cuenta Polygon creada",
"virtualAccountCreatedToast": "Cuenta creada exitosamente",
"createProductErrorToast": "No se pudo crear el producto. Intenta de nuevo.",
"transferToOwnAccount": "Transferir a otra cuenta",
"transferDrawerTitle": "Transferir fondos",
Expand Down Expand Up @@ -328,7 +331,8 @@
"quickActions": {
"title": "Acciones rápidas",
"topup": "Recargar",
"send": "Enviar"
"send": "Enviar",
"needsAccount": "Crea una cuenta primero para poder usar esta opción."
}
},
"send": {
Expand Down Expand Up @@ -396,7 +400,8 @@
"noRateToContinue": "No hay tasa disponible para continuar.",
"amountStep": {
"label": "Monto a enviar (COP)",
"minAmount": "Monto mínimo: $10,000 COP"
"minAmount": "Monto mínimo: $10,000 COP",
"feeLabel": "Comisión"
}
},
"usBanks": {
Expand All @@ -405,6 +410,7 @@
"noRateForOrder": "No hay tasa seleccionada para crear la orden.",
"invalidAmountForOrder": "Monto inválido para crear la orden.",
"noSourceAccount": "No hay cuenta origen disponible.",
"kycRejectedError": "Tu verificación de identidad no fue aprobada, por eso no podemos completar esta operación. Vuelve a intentar la verificación desde tu perfil.",
"minAmountToast": "El monto mínimo es $10 USD.",
"noRateToContinue": "No hay tasa disponible para continuar.",
"transferSentToast": "Transferencia RTP iniciada correctamente.",
Expand Down Expand Up @@ -514,7 +520,7 @@
"documentNumberPlaceholder": "Ej: 123456789"
},
"confirmStep": {
"summary": "Resumen de recarga",
"summary": "Resumen",
"sending": "Enviando...",
"sendWithdrawal": "Enviar retiro",
"verifyDisclaimer": "Verifica que los datos bancarios sean correctos antes de enviar."
Expand Down Expand Up @@ -567,6 +573,7 @@
"invalidAmountForOrder": "Monto inválido para crear la orden.",
"noSourceAccount": "No encontramos una cuenta bancaria vinculada desde donde recargar.",
"noDestinationAccount": "No encontramos una cuenta destino disponible.",
"kycRejectedError": "Tu verificación de identidad no fue aprobada, por eso no podemos completar esta operación. Vuelve a intentar la verificación desde tu perfil.",
"minAmountToast": "El monto mínimo es $10 USD.",
"noRateToContinue": "No hay tasa disponible para continuar.",
"topupStartedToast": "Recarga ACH iniciada correctamente.",
Expand Down
7 changes: 4 additions & 3 deletions src/infra/bloque/accounts-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,10 @@ async function transfer(input: TransferInput): Promise<TransferOutcome> {
async function createVirtualAccount(
input: CreateVirtualAccountInput,
): Promise<Product> {
const account = await bloque.accounts.virtual.create(
input.name ? { name: input.name } : {},
);
const account = await bloque.accounts.virtual.create({
...(input.name ? { name: input.name } : {}),
...(input.ledgerId ? { ledgerId: input.ledgerId } : {}),
});
return mapToProduct(account as ListedAccount);
}

Expand Down
53 changes: 39 additions & 14 deletions src/routes/_authed/(home)/-components/quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
import { Link } from '@tanstack/react-router';
import { KeyRound, Landmark, Send } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { cn } from '~/lib/utils';

export function QuickActions() {
/**
* `topup`/`send`/BRE-B all fail deep inside their own flow with a confusing
* "no destination account" error when the user has no account at all yet
* (see BQE-2653) — disabling them up front, before that flow is ever
* reached, is cheaper to understand than any error message once inside it.
*/
export function QuickActions({ hasAccount }: { hasAccount: boolean }) {
const { t } = useTranslation();
const actions = [
{
Expand Down Expand Up @@ -33,21 +40,39 @@ export function QuickActions() {
<div className="grid grid-cols-3 gap-2">
{actions.map((action) => {
const Icon = action.icon;
const content = (
<div
className={cn(
'flex flex-col items-center gap-1.5 rounded-2xl border border-border/85 px-2 py-3.5 transition-all duration-200',
hasAccount
? 'bg-card shadow-[0_14px_28px_-30px_color-mix(in_oklch,var(--foreground)_55%,transparent)] dark:shadow-[0_14px_28px_-30px_rgb(0_0_0_/_0.7)] hover:bg-muted/70 cursor-pointer'
: 'bg-card/50 opacity-50 cursor-not-allowed',
)}
>
<div className="flex h-9 w-9 items-center justify-center rounded-xl border border-primary/25 bg-primary/[0.07]">
<Icon className="h-4 w-4 text-primary" strokeWidth={1.5} />
</div>
<span className="text-[11px] font-medium text-foreground leading-tight text-center">
{action.label}
</span>
</div>
);

if (!hasAccount) {
return (
<button
key={action.label}
type="button"
onClick={() => toast.info(t('home.quickActions.needsAccount'))}
>
{content}
</button>
);
}

return (
<Link key={action.label} to={action.href}>
<div
className={cn(
'flex flex-col items-center gap-1.5 rounded-2xl border border-border/85 px-2 py-3.5 transition-all duration-200',
'bg-card shadow-[0_14px_28px_-30px_color-mix(in_oklch,var(--foreground)_55%,transparent)] dark:shadow-[0_14px_28px_-30px_rgb(0_0_0_/_0.7)] hover:bg-muted/70 cursor-pointer',
)}
>
<div className="flex h-9 w-9 items-center justify-center rounded-xl border border-primary/25 bg-primary/[0.07]">
<Icon className="h-4 w-4 text-primary" strokeWidth={1.5} />
</div>
<span className="text-[11px] font-medium text-foreground leading-tight text-center">
{action.label}
</span>
</div>
{content}
</Link>
);
})}
Expand Down
7 changes: 6 additions & 1 deletion src/routes/_authed/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ function RouteComponent() {
const [showCreateAccount, setShowCreateAccount] = useState(false);

const accounts = accountsQuery.data ?? [];
// While the accounts query is still resolving, `accounts` is `[]` — that
// must not read as "no accounts" and disable the quick actions for a user
// who actually has one; only a *settled* empty list means that.
const hasAccount = accountsQuery.isLoading || accounts.length > 0;
const assets: Asset[] = ['USD', 'COP'];
const selectedBalance = parsedBalances[selectedAsset] ?? 0;

Expand Down Expand Up @@ -130,7 +134,7 @@ function RouteComponent() {
</div>
</section>

<QuickActions />
<QuickActions hasAccount={hasAccount} />

<div className="my-1 h-px w-full bg-gradient-to-r from-transparent via-border to-transparent" />

Expand All @@ -152,6 +156,7 @@ function RouteComponent() {
navigate({ to: '/accounts/$urn', params: { urn } })
}
onAddAccount={() => setShowCreateAccount(true)}
highlightAdd={!accountsQuery.isLoading && accounts.length === 0}
/>
</section>

Expand Down
Loading
Loading