Skip to content
Open
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
36 changes: 36 additions & 0 deletions e2e/whatsapp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from '@playwright/test';
import { installMockApi } from './fixtures/mock-api';

test.beforeEach(async ({ page }) => {
await installMockApi(page);
});

test('bottom nav opens the WhatsApp view', async ({ page }) => {
await page.goto('/');

await page
.getByRole('navigation', { name: 'Navegación principal' })
.getByRole('link', { name: 'WhatsApp' })
.click();

await expect(page).toHaveURL(/\/whatsapp$/);
await expect(page.getByText('Tu wallet en WhatsApp')).toBeVisible();
await expect(page.getByText('Enviar por BRE-B')).toBeVisible();
await expect(page.getByText('Recargar por PSE')).toBeVisible();
});

test('home card links to the WhatsApp view', async ({ page }) => {
await page.goto('/');

await page.getByRole('link', { name: /Usa tu wallet por WhatsApp/ }).click();

await expect(page).toHaveURL(/\/whatsapp$/);
});

test('CTA stays disabled until the number is configured', async ({ page }) => {
await page.goto('/whatsapp');

const ctas = page.getByRole('button', { name: /Abrir WhatsApp/ });
await expect(ctas.first()).toBeDisabled();
await expect(ctas.first()).toContainText('Próximamente');
});
36 changes: 36 additions & 0 deletions src/components/bottom-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Link, useLocation } from '@tanstack/react-router';
import { ArrowLeftRight, CreditCard, Home, UserCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { WhatsAppIcon } from '~/components/whatsapp-icon';
import { cn } from '~/lib/utils';

export function BottomNav() {
Expand All @@ -12,6 +13,12 @@ export function BottomNav() {
const navItems = [
{ href: '/', label: t('nav.home'), icon: Home },
{ href: '/movements', label: t('nav.movements'), icon: ArrowLeftRight },
{
href: '/whatsapp',
label: t('nav.whatsapp'),
icon: WhatsAppIcon,
highlight: true,
},
{ href: '/card', label: t('nav.card'), icon: CreditCard },
{ href: '/profile', label: t('nav.profile'), icon: UserCircle },
];
Expand All @@ -25,6 +32,35 @@ export function BottomNav() {
{navItems.map((item) => {
const isActive = pathname === item.href;
const Icon = item.icon;
if (item.highlight) {
return (
<Link
key={item.href}
to={item.href}
className="flex min-w-[58px] flex-col items-center gap-0.5 px-1 text-center"
aria-current={isActive ? 'page' : undefined}
>
<span
className={cn(
'-mt-6 flex h-12 w-12 items-center justify-center rounded-full bg-[#25D366] text-white shadow-[0_12px_24px_-10px_rgb(37_211_102_/_0.8)] ring-4 ring-background transition-transform',
isActive ? 'scale-105' : 'hover:scale-105',
)}
>
<Icon className="h-6 w-6" />
</span>
<span
className={cn(
'text-[10px] leading-tight',
isActive
? 'font-semibold text-foreground'
: 'text-muted-foreground',
)}
>
{item.label}
</span>
</Link>
);
}
return (
<Link
key={item.href}
Expand Down
15 changes: 15 additions & 0 deletions src/components/whatsapp-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { SVGProps } from 'react';

export function WhatsAppIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
focusable="false"
{...props}
>
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z" />
</svg>
);
}
13 changes: 13 additions & 0 deletions src/config/whatsapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PUBLIC_APP_ENV } from './env';

/**
* Official Console WhatsApp number per environment, in international format
* without `+` (as `wa.me` expects). `null` keeps the CTA disabled until the
* number is assigned.
*/
const WHATSAPP_NUMBERS: Record<'dev' | 'prod', string | null> = {
dev: null,
prod: null,
};

export const WHATSAPP_NUMBER = WHATSAPP_NUMBERS[PUBLIC_APP_ENV];
64 changes: 64 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@
"topup": "Top up",
"send": "Send",
"needsAccount": "Create a pocket first to use this."
},
"whatsappCard": {
"title": "Use your wallet on WhatsApp",
"description": "Send and top up money by chatting."
}
},
"send": {
Expand Down Expand Up @@ -674,6 +678,7 @@
"home": "Home",
"accounts": "Pockets",
"movements": "Movements",
"whatsapp": "WhatsApp",
"card": "Card",
"profile": "Profile"
},
Expand Down Expand Up @@ -780,5 +785,64 @@
"title": "You need a pocket",
"description": "Create a pocket to top up, send money, use BRE-B and create cards.",
"loadError": "We couldn't load your pockets."
},
"whatsapp": {
"title": "WhatsApp",
"cta": "Open WhatsApp",
"prefilled": "Hi Bloque, I want to use my wallet on WhatsApp.",
"prefilledWithAlias": "Hi Bloque, I want to use my wallet on WhatsApp. My alias is {{alias}}.",
"hero": {
"title": "Your wallet on WhatsApp",
"subtitle": "Send and top up money by chatting, without opening the app."
},
"chat": {
"ariaLabel": "Sample conversation with Bloque on WhatsApp",
"user1": "Send 50,000 to the key @juan",
"bloque1": "You're sending $50,000 COP to Juan Pérez via BRE-B. To confirm, type the code we sent you.",
"user2": "482913",
"bloque2": "Done! Your transfer was successful ✅"
},
"operations": {
"title": "What you can do",
"brebSend": {
"title": "Send via BRE-B",
"description": "To any BRE-B key, instantly."
},
"banks": {
"title": "Send to banks",
"description": "To Colombian bank accounts."
},
"brebTopup": {
"title": "Top up via BRE-B",
"description": "Receive money in your wallet with your key."
},
"pseTopup": {
"title": "Top up via PSE",
"description": "Top up from your online banking."
}
},
"steps": {
"title": "How it works",
"open": {
"title": "Open the chat",
"description": "Tap the button and message us on WhatsApp."
},
"ask": {
"title": "Ask for what you need",
"description": "Write the way you talk: “top up 100,000 via PSE”."
},
"confirm": {
"title": "Confirm the transaction",
"description": "We'll ask for an OTP code and some details before moving your money."
}
},
"alias": {
"title": "Identify yourself with your alias",
"description": "Use the same email or phone you sign in to the wallet with."
},
"security": {
"title": "Your money, protected",
"description": "To move money we'll ask for an OTP code and confirmation details. Share them only in Bloque's official chat and never with anyone else."
}
}
}
64 changes: 64 additions & 0 deletions src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@
"topup": "Recargar",
"send": "Enviar",
"needsAccount": "Crea un bolsillo primero para poder usar esta opción."
},
"whatsappCard": {
"title": "Usa tu wallet por WhatsApp",
"description": "Envía y recarga dinero chateando."
}
},
"send": {
Expand Down Expand Up @@ -674,6 +678,7 @@
"home": "Inicio",
"accounts": "Bolsillos",
"movements": "Movimientos",
"whatsapp": "WhatsApp",
"card": "Tarjeta",
"profile": "Perfil"
},
Expand Down Expand Up @@ -780,5 +785,64 @@
"title": "Necesitas un bolsillo",
"description": "Crea un bolsillo para poder recargar, enviar dinero, usar BRE-B y crear tarjetas.",
"loadError": "No pudimos cargar tus bolsillos."
},
"whatsapp": {
"title": "WhatsApp",
"cta": "Abrir WhatsApp",
"prefilled": "Hola Bloque, quiero usar mi wallet por WhatsApp.",
"prefilledWithAlias": "Hola Bloque, quiero usar mi wallet por WhatsApp. Mi alias es {{alias}}.",
"hero": {
"title": "Tu wallet en WhatsApp",
"subtitle": "Envía y recarga dinero chateando, sin abrir la app."
},
"chat": {
"ariaLabel": "Ejemplo de conversación con Bloque en WhatsApp",
"user1": "Envía 50.000 a la llave @juan",
"bloque1": "Vas a enviar $50.000 COP a Juan Pérez por BRE-B. Para confirmar, escribe el código que te enviamos.",
"user2": "482913",
"bloque2": "¡Listo! Tu envío fue exitoso ✅"
},
"operations": {
"title": "Qué puedes hacer",
"brebSend": {
"title": "Enviar por BRE-B",
"description": "A cualquier llave BRE-B, al instante."
},
"banks": {
"title": "Enviar a bancos",
"description": "A cuentas de bancos colombianos."
},
"brebTopup": {
"title": "Recargar por BRE-B",
"description": "Recibe dinero en tu wallet con tu llave."
},
"pseTopup": {
"title": "Recargar por PSE",
"description": "Recarga desde tu banco en línea."
}
},
"steps": {
"title": "Cómo funciona",
"open": {
"title": "Abre el chat",
"description": "Toca el botón y escríbenos desde WhatsApp."
},
"ask": {
"title": "Pide lo que necesitas",
"description": "Escribe como hablas: «recarga 100.000 por PSE»."
},
"confirm": {
"title": "Confirma el movimiento",
"description": "Te pediremos un código OTP y algunos datos antes de mover tu dinero."
}
},
"alias": {
"title": "Identifícate con tu alias",
"description": "Usa el mismo correo o teléfono con el que ingresas a la wallet."
},
"security": {
"title": "Tu dinero, protegido",
"description": "Para mover dinero te pediremos un código OTP y datos de confirmación. Compártelos solo en el chat oficial de Bloque y nunca con otras personas."
}
}
}
16 changes: 16 additions & 0 deletions src/lib/whatsapp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from 'bun:test';
import { buildWhatsAppUrl } from './whatsapp';

describe('buildWhatsAppUrl', () => {
test('strips non-digits from the number', () => {
expect(buildWhatsAppUrl('+57 300-123 4567', 'Hola')).toBe(
'https://wa.me/573001234567?text=Hola',
);
});

test('encodes the prefilled text', () => {
expect(buildWhatsAppUrl('573001234567', 'Hola, soy a+b@x.co')).toBe(
'https://wa.me/573001234567?text=Hola%2C%20soy%20a%2Bb%40x.co',
);
});
});
5 changes: 5 additions & 0 deletions src/lib/whatsapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** `wa.me` deep link that opens a chat with `number` and a prefilled `text`. */
export function buildWhatsAppUrl(number: string, text: string) {
const digits = number.replace(/\D/g, '');
return `https://wa.me/${digits}?text=${encodeURIComponent(text)}`;
}
Loading
Loading