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 public/instalar/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h1>Instalá <span>la app</span><br>de Winbit</h1>
android: { label: 'Android · Chrome', items: [
['img/android-1.webp', '<b>Abrí app.winbit.com.ar</b> en Chrome e iniciá sesión con Google o con tu mail. Si el link se abrió dentro de WhatsApp, tocá <b>⋮</b> y elegí <b>“Abrir en Chrome”</b>.', true],
['img/android-2.webp', 'Tocá el menú <b>⋮</b> (tres puntos) arriba a la derecha.'],
['img/android-3.webp', 'Elegí <b>“Agregar a la pantalla principal”</b> (o <b>“Instalar app”</b>) y confirmá.'],
['img/android-3.webp', 'Elegí <b>“Instalar y crear acceso directo”</b> (o <b>“Agregar a pantalla de inicio”</b>) y confirmá.'],
['img/android-4.webp', '<b>¡Listo!</b> 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.' }
};
Expand Down
4 changes: 4 additions & 0 deletions src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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.',
Expand Down
16 changes: 16 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ const LoginBrand = ({ t }) => (
</div>
);

/** 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 <Link>.
const LoginInstallHint = ({ t }) => (
<p className="login-install-hint">
{t('auth.login.installPrompt')}{' '}
<a href="/instalar" className="login-install-hint__link">
{t('auth.login.installLink')}
</a>
</p>
);

const LoginTrustBlock = ({ t }) => (
<div className="login-trust">
<SecurityShield className="login-trust__icon" />
Expand All @@ -113,6 +134,7 @@ export const LoginPage = () => {
const [showPassword, setShowPassword] = useState(false);
const { t } = useTranslation();
const devBypassActive = isDevBypassEnabled();
const showInstallHint = shouldShowInstallHint();

useEffect(() => {
setError(null);
Expand Down Expand Up @@ -398,6 +420,8 @@ export const LoginPage = () => {
</form>
)}

{showInstallHint ? <LoginInstallHint t={t} /> : null}

{displayError ? (
<div role="alert" className="login-panel__error">
{displayError}
Expand Down
47 changes: 46 additions & 1 deletion src/pages/LoginPage.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
});
});
Loading