From 78f6a8feace040eda001ca922b56cf6b46244490 Mon Sep 17 00:00:00 2001 From: Federico Date: Tue, 20 Oct 2026 16:57:24 -0300 Subject: [PATCH] Agregar link a /instalar en el login (solo celular) y ajustar el paso de Android --- public/instalar/index.html | 2 +- src/i18n.js | 4 +++ src/index.css | 16 ++++++++++++ src/pages/LoginPage.jsx | 24 ++++++++++++++++++ src/pages/LoginPage.test.jsx | 47 +++++++++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 2 deletions(-) diff --git a/public/instalar/index.html b/public/instalar/index.html index aeec8fe..553c28f 100644 --- a/public/instalar/index.html +++ b/public/instalar/index.html @@ -121,7 +121,7 @@

Instalá la app
de Winbit

android: { label: 'Android · Chrome', items: [ ['img/android-1.webp', 'Abrí app.winbit.com.ar en Chrome e iniciá sesión con Google o con tu mail. Si el link se abrió dentro de WhatsApp, tocá ⋮ y elegí “Abrir en Chrome”.', true], ['img/android-2.webp', 'Tocá el menú ⋮ (tres puntos) arriba a la derecha.'], - ['img/android-3.webp', 'Elegí “Agregar a la pantalla principal” (o “Instalar app”) y confirmá.'], + ['img/android-3.webp', 'Elegí “Instalar y crear acceso directo” (o “Agregar a pantalla de inicio”) y confirmá.'], ['img/android-4.webp', '¡Listo! Winbit ya está en tu pantalla de inicio. Tocá el ícono para entrar, como en cualquier otra app.'] ], note: 'Los nombres pueden variar levemente según la versión de Android.' } }; diff --git a/src/i18n.js b/src/i18n.js index 5fa09e0..6894c06 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -112,6 +112,8 @@ const resources = { orSeparator: 'o', useEmailPassword: 'Ingresar con email y contraseña', backToGoogle: 'Volver a acceso con Google', + installPrompt: '¿Querés tenerla como app en tu celular?', + installLink: 'Ver cómo instalarla', tagline: 'Gestión activa en USD', securityTitle: 'Acceso seguro para inversores registrados.', securityDetail: 'Tu información está protegida con protocolos de seguridad avanzados.', @@ -830,6 +832,8 @@ const resources = { orSeparator: 'or', useEmailPassword: 'Sign in with email and password', backToGoogle: 'Back to Google sign-in', + installPrompt: 'Want it as an app on your phone?', + installLink: 'See how to install it', tagline: 'Active management in USD', securityTitle: 'Secure access for registered investors.', securityDetail: 'Your information is protected with advanced security protocols.', diff --git a/src/index.css b/src/index.css index b162075..6d189ab 100644 --- a/src/index.css +++ b/src/index.css @@ -1046,6 +1046,22 @@ color: var(--login-cream); } + .login-install-hint { + margin: 1rem 0 0; + color: var(--login-cream-soft); + font-family: var(--wb-font); + font-size: 0.8125rem; + line-height: 1.4; + text-align: center; + } + + .login-install-hint__link { + color: var(--login-cream); + font-weight: 600; + text-decoration: underline; + text-underline-offset: 3px; + } + .login-dev { margin-bottom: 1.5rem; padding-bottom: 1.25rem; diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 67f7f21..bdc7e48 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -87,6 +87,27 @@ const LoginBrand = ({ t }) => ( ); +/** Celular fuera de la app instalada: ahí tiene sentido ofrecer el instructivo de instalación. */ +const shouldShowInstallHint = () => { + if (typeof navigator === 'undefined') return false; + const isPhone = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || ''); + const isStandalone = + (typeof window.matchMedia === 'function' && + window.matchMedia('(display-mode: standalone)').matches) || + navigator.standalone === true; + return isPhone && !isStandalone; +}; + +// /instalar es una página estática fuera de la SPA: link común, no . +const LoginInstallHint = ({ t }) => ( +

+ {t('auth.login.installPrompt')}{' '} + + {t('auth.login.installLink')} + +

+); + const LoginTrustBlock = ({ t }) => (
@@ -113,6 +134,7 @@ export const LoginPage = () => { const [showPassword, setShowPassword] = useState(false); const { t } = useTranslation(); const devBypassActive = isDevBypassEnabled(); + const showInstallHint = shouldShowInstallHint(); useEffect(() => { setError(null); @@ -398,6 +420,8 @@ export const LoginPage = () => { )} + {showInstallHint ? : null} + {displayError ? (
{displayError} diff --git a/src/pages/LoginPage.test.jsx b/src/pages/LoginPage.test.jsx index 3717120..71cabd8 100644 --- a/src/pages/LoginPage.test.jsx +++ b/src/pages/LoginPage.test.jsx @@ -1,5 +1,5 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { LoginPage } from './LoginPage'; import { useAuth } from '../hooks/useAuth'; @@ -223,3 +223,48 @@ describe('LoginPage', () => { expect(screen.getByText(/Not an investor/)).toBeInTheDocument(); }); }); + +describe('LoginPage install hint', () => { + const ANDROID_UA = + 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/124.0.0.0 Mobile Safari/537.36'; + const originalUA = navigator.userAgent; + const originalMatchMedia = window.matchMedia; + + const setUserAgent = (ua) => + Object.defineProperty(navigator, 'userAgent', { value: ua, configurable: true }); + + beforeEach(() => { + useAuth.mockReturnValue(defaultMock); + }); + + afterEach(() => { + setUserAgent(originalUA); + window.matchMedia = originalMatchMedia; + }); + + it('shows the install link on a phone browser', () => { + setUserAgent(ANDROID_UA); + + renderAt('/login'); + expect(screen.getByRole('link', { name: 'Ver cómo instalarla' })).toHaveAttribute( + 'href', + '/instalar', + ); + }); + + it('hides the install link on desktop', () => { + renderAt('/login'); + expect(screen.queryByRole('link', { name: 'Ver cómo instalarla' })).not.toBeInTheDocument(); + }); + + it('hides the install link when the app is already installed', () => { + setUserAgent(ANDROID_UA); + window.matchMedia = vi.fn().mockImplementation((query) => ({ + matches: query === '(display-mode: standalone)', + media: query, + })); + + renderAt('/login'); + expect(screen.queryByRole('link', { name: 'Ver cómo instalarla' })).not.toBeInTheDocument(); + }); +});