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
2 changes: 1 addition & 1 deletion e2e/accounts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test('lands on the ledger-grouped accounts list with a mocked, authenticated ses
await expect(
page
.getByRole('navigation', { name: 'Navegación principal' })
.getByText('Cuentas'),
.getByText('Movimientos'),
).toBeVisible();
});

Expand Down
2 changes: 1 addition & 1 deletion src/components/account/account-carousel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@ describe('AccountCarousel', () => {
/>,
);

expect(screen.getByText('1500.50 COP')).toBeInTheDocument();
expect(screen.getByText('$ 1.501')).toBeInTheDocument();
});
});
10 changes: 6 additions & 4 deletions src/components/account/account-carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Label } from '~/components/ui/label';
import type { Account } from '~/domain/accounts/types';
import { type Asset, formatAmount } from '~/lib/formatters';
import { cn } from '~/lib/utils';
import { getProductKindIcon } from './product-presentation';

Expand All @@ -13,11 +14,12 @@ function formatAccountBalance(
account: Account,
asset: string,
precision: number,
unit: Asset,
): string {
const entry = account.balances.find((balance) => balance.asset === asset);
const parsed = entry ? Number.parseInt(entry.current, 10) : Number.NaN;
const amount = Number.isNaN(parsed) ? 0 : parsed / 10 ** precision;
return amount.toFixed(2);
return formatAmount(unit, amount);
}

function AccountCard({
Expand All @@ -31,7 +33,7 @@ function AccountCard({
account: Account;
asset: string;
precision: number;
unit: string;
unit: Asset;
isActive: boolean;
onClick: () => void;
}) {
Expand All @@ -40,7 +42,7 @@ function AccountCard({
(product) => product.urn === account.primaryUrn,
);
const Icon = getProductKindIcon(primary?.kind ?? 'other');
const balanceLabel = `${formatAccountBalance(account, asset, precision)} ${unit}`;
const balanceLabel = formatAccountBalance(account, asset, precision, unit);

return (
<button
Expand Down Expand Up @@ -102,7 +104,7 @@ export function AccountCarousel({
accounts: Account[];
asset: string;
precision: number;
unit: string;
unit: Asset;
value: string | null;
onChange: (ledgerId: string) => void;
label?: string;
Expand Down
17 changes: 2 additions & 15 deletions src/components/bottom-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
'use client';

import { Link, useLocation } from '@tanstack/react-router';
import {
ArrowLeftRight,
CreditCard,
Home,
Landmark,
UserCircle,
} from 'lucide-react';
import { ArrowLeftRight, CreditCard, Home, UserCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAccounts } from '~/hooks/accounts/use-accounts';
import { cn } from '~/lib/utils';

export function BottomNav() {
const { t } = useTranslation();
const { pathname } = useLocation();
const accountsQuery = useAccounts();
const hasMultipleAccounts = (accountsQuery.data?.length ?? 0) > 1;

const secondItem = hasMultipleAccounts
? { href: '/accounts', label: t('nav.accounts'), icon: Landmark }
: { href: '/movements', label: t('nav.movements'), icon: ArrowLeftRight };

const navItems = [
{ href: '/', label: t('nav.home'), icon: Home },
secondItem,
{ href: '/movements', label: t('nav.movements'), icon: ArrowLeftRight },
{ href: '/card', label: t('nav.card'), icon: CreditCard },
{ href: '/profile', label: t('nav.profile'), icon: UserCircle },
];
Expand Down
26 changes: 19 additions & 7 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,22 +337,27 @@
},
"send": {
"title": "Send",
"noFee": "No cost",
"groups": {
"colombia": "Colombia",
"us": "United States"
},
"options": {
"brebKeys": {
"title": "BRE-B keys",
"description": "Send to any Colombian bank instantly."
},
"colombianBanks": {
"title": "Colombian banks",
"description": "Transfer to bank accounts in Colombia."
"title": "Banks",
"description": "Transfer to bank accounts."
},
"bloqueFriends": {
"title": "Friends on Bloque",
"description": "Send to contacts within Bloque."
},
"usBanks": {
"title": "US banks/wallets",
"description": "Send USD to banks and wallets in the United States."
"title": "Banks/wallets",
"description": "Send USD via RTP to banks and wallets."
},
"blockchain": {
"title": "Blockchain addresses",
Expand Down Expand Up @@ -394,6 +399,7 @@
"noRateForOrder": "No rate selected to create the order.",
"invalidAmountForOrder": "Invalid amount to create the order.",
"noSourceAccount": "No source account available.",
"sourceAccountLabel": "Source account",
"selectDestinationBank": "Select a destination bank.",
"unsupportedBank": "The selected bank isn't valid for this transfer.",
"minAmountToast": "The minimum amount is $10,000 COP.",
Expand All @@ -410,6 +416,7 @@
"noRateForOrder": "No rate selected to create the order.",
"invalidAmountForOrder": "Invalid amount to create the order.",
"noSourceAccount": "No source account available.",
"sourceAccountLabel": "Source 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.",
Expand Down Expand Up @@ -447,18 +454,22 @@
"rateExpiredToast": "The rate expired. Recalculating...",
"startErrorToast": "Couldn't start the top-up.",
"noRateRetryToast": "No rate available. Please try again.",
"groups": {
"colombia": "Colombia",
"us": "United States"
},
"methods": {
"colombianBanks": {
"title": "Colombian banks",
"title": "Banks",
"subtitle": "PSE"
},
"brebKeys": {
"title": "BRE-B keys",
"subtitle": "Register and manage your keys"
},
"usBanks": {
"title": "From US banks",
"subtitle": "ACH / Wire"
"title": "From banks",
"subtitle": "ACH"
},
"blockchain": {
"title": "From blockchain addresses",
Expand All @@ -471,6 +482,7 @@
"comingSoon": "Top-up with card coming soon."
}
},
"destinationAccountLabel": "Destination account",
"iWantToReceive": "I want to receive",
"pseAmountLabel": "Amount to pay via PSE (COP)",
"minAmount": "Minimum amount: $5,000 COP",
Expand Down
26 changes: 19 additions & 7 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,22 +337,27 @@
},
"send": {
"title": "Enviar",
"noFee": "Sin costo",
"groups": {
"colombia": "Colombia",
"us": "Estados Unidos"
},
"options": {
"brebKeys": {
"title": "Llaves BRE-B",
"description": "Envia a cualquier banco colombiano al instante."
},
"colombianBanks": {
"title": "Bancos colombianos",
"description": "Transfiere a cuentas bancarias en Colombia."
"title": "Bancos",
"description": "Transfiere a cuentas bancarias."
},
"bloqueFriends": {
"title": "Amigos en bloque",
"description": "Envia a contactos dentro de Bloque."
},
"usBanks": {
"title": "Bancos/billeteras en EE.UU.",
"description": "Envia USD a bancos y wallets en Estados Unidos."
"title": "Bancos/billeteras",
"description": "Envia USD vía RTP a bancos y wallets."
},
"blockchain": {
"title": "Direcciones blockchain",
Expand Down Expand Up @@ -394,6 +399,7 @@
"noRateForOrder": "No hay tasa seleccionada para crear la orden.",
"invalidAmountForOrder": "Monto inválido para crear la orden.",
"noSourceAccount": "No hay cuenta origen disponible.",
"sourceAccountLabel": "Cuenta origen",
"selectDestinationBank": "Selecciona un banco destino.",
"unsupportedBank": "El banco seleccionado no es válido para esta transferencia.",
"minAmountToast": "El monto mínimo es $10,000 COP.",
Expand All @@ -410,6 +416,7 @@
"noRateForOrder": "No hay tasa seleccionada para crear la orden.",
"invalidAmountForOrder": "Monto inválido para crear la orden.",
"noSourceAccount": "No hay cuenta origen disponible.",
"sourceAccountLabel": "Cuenta origen",
"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.",
Expand Down Expand Up @@ -447,18 +454,22 @@
"rateExpiredToast": "La tasa expiró. Recalculando...",
"startErrorToast": "No se pudo iniciar la recarga.",
"noRateRetryToast": "No hay tasa disponible. Intenta de nuevo.",
"groups": {
"colombia": "Colombia",
"us": "Estados Unidos"
},
"methods": {
"colombianBanks": {
"title": "Bancos colombianos",
"title": "Bancos",
"subtitle": "PSE"
},
"brebKeys": {
"title": "Llaves BRE-B",
"subtitle": "Registra y gestiona tus llaves"
},
"usBanks": {
"title": "Desde bancos en EE.UU.",
"subtitle": "ACH / Wire"
"title": "Desde bancos",
"subtitle": "ACH"
},
"blockchain": {
"title": "Desde direcciones blockchain",
Expand All @@ -471,6 +482,7 @@
"comingSoon": "Recarga con tarjeta disponible próximamente."
}
},
"destinationAccountLabel": "Cuenta destino",
"iWantToReceive": "Quiero recibir",
"pseAmountLabel": "Monto a pagar por PSE (COP)",
"minAmount": "Monto minimo: $5,000 COP",
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="ui-theme">
<ThemeProvider defaultTheme="light" storageKey="ui-theme">
<QueryClientProvider client={queryClient}>
<AuthProvider>
{PUBLIC_APP_ENV === 'dev' && (
Expand Down
37 changes: 33 additions & 4 deletions src/routes/_authed/accounts/$urn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useTransfer } from '~/hooks/accounts/use-transfer';
import { useCreateVirtualAccount } from '~/hooks/accounts/use-virtual-account';
import type { Asset, Movement } from '~/lib/formatters';
import { formatCOP, formatUSD, sortBalancesForDisplay } from '~/lib/formatters';
import { goBackOrFallback } from '~/lib/navigation';
import { cn } from '~/lib/utils';

export const Route = createFileRoute('/_authed/accounts/$urn')({
Expand Down Expand Up @@ -78,6 +79,25 @@ function majorToMinor(amountMajor: number, rawAsset: string): string {
return Math.round(amountMajor * 10 ** precision).toString();
}

/**
* COP has no meaningful sub-unit in this app's UX (every other COP input is
* digit-only, whole pesos) — a "." here would be a thousands separator, not
* a decimal point, and `parseFloat` has no way to tell those apart. Only
* assets with a real decimal convention (USD, KSM) get a "." at all, capped
* to that asset's own on-chain precision rather than a fixed 2 places.
*/
function sanitizeTransferAmountInput(raw: string, rawAsset: string): string {
if (getAssetLabel(rawAsset) === 'COP') return raw.replace(/\D/g, '');
const [, precisionStr] = rawAsset.split('/');
const precision = Number.parseInt(precisionStr, 10);
const maxDecimals = Number.isNaN(precision) ? 2 : precision;
const cleaned = raw.replace(/[^\d.]/g, '');
const [intPart, ...rest] = cleaned.split('.');
return rest.length > 0
? `${intPart}.${rest.join('').slice(0, maxDecimals)}`
: intPart;
}

function formatAssetBalance(balance: AssetBalance) {
const asset = getAssetLabel(balance.asset);
const amount = parseAmount(balance.current, balance.asset);
Expand Down Expand Up @@ -243,13 +263,18 @@ function RouteComponent() {
return (
<div className="flex flex-col gap-5">
<div className="flex items-center gap-2">
<Link
to="/accounts"
<button
type="button"
onClick={() =>
goBackOrFallback(() => {
void navigate({ to: '/accounts' });
})
}
className="inline-flex items-center gap-1 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" />
{t('common.back')}
</Link>
</button>
<h1 className="text-xl font-bold tracking-[-0.025em] text-foreground">
{t('accounts.detail.title')}
</h1>
Expand Down Expand Up @@ -632,7 +657,11 @@ function RouteComponent() {
id="transfer-amount"
inputMode="decimal"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
onChange={(e) =>
setTransferAmount(
sanitizeTransferAmountInput(e.target.value, selectedAsset),
)
}
placeholder="0"
disabled={transferMutation.isPending}
className="h-12 rounded-xl"
Expand Down
12 changes: 9 additions & 3 deletions src/routes/_authed/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Account } from '~/domain/accounts/types';
import { useAccounts } from '~/hooks/accounts/use-accounts';
import i18n from '~/i18n/config';
import { formatCOP, formatUSD, sortBalancesForDisplay } from '~/lib/formatters';
import { goBackOrFallback } from '~/lib/navigation';

export const Route = createFileRoute('/_authed/accounts/')({
component: RouteComponent,
Expand Down Expand Up @@ -72,13 +73,18 @@ function RouteComponent() {
return (
<div className="flex flex-col gap-5">
<div className="flex items-center gap-2">
<Link
to="/profile"
<button
type="button"
onClick={() =>
goBackOrFallback(() => {
void navigate({ to: '/profile' });
})
}
className="inline-flex items-center gap-1 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" />
{t('common.back')}
</Link>
</button>
<h1 className="text-xl font-bold tracking-[-0.025em] text-foreground">
{t('accounts.title')}
</h1>
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_authed/breb-keys/pay-transfer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ function RouteComponent() {
/>
</div>

{fundedAccounts.length > 1 ? (
{fundedAccounts.length > 0 ? (
<AccountCarousel
accounts={fundedAccounts}
asset={FROM_ASSET}
Expand Down
Loading
Loading