diff --git a/src/languages/de.ts b/src/languages/de.ts index a6cf5c09960f..33e0a05271a0 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2955,6 +2955,8 @@ ${amount} für ${merchant} – ${date}`, phoneOrEmail: 'Telefon oder E-Mail', error: { invalidFormatEmailLogin: 'Die eingegebene E-Mail-Adresse ist ungültig. Bitte korrigiere das Format und versuche es erneut.', + agentSignInBlocked: + 'Agent-Konten können nicht direkt verwendet werden. Um ein Agent-Konto zu nutzen, melden Sie sich mit Ihrem eigenen Konto an und greifen Sie über Copilot darauf zu.', }, cannotGetAccountDetails: 'Kontodetails konnten nicht abgerufen werden. Bitte melde dich erneut an.', loginForm: 'Anmeldeformular', diff --git a/src/languages/en.ts b/src/languages/en.ts index 0b50e1b7b55e..3434047b12d3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3022,6 +3022,7 @@ const translations = { phoneOrEmail: 'Phone or email', error: { invalidFormatEmailLogin: 'The email entered is invalid. Please fix the format and try again.', + agentSignInBlocked: "Agent accounts can't be signed into directly. To use an agent, sign in with your own account and access it via Copilot.", }, cannotGetAccountDetails: "Couldn't retrieve account details. Please try to sign in again.", loginForm: 'Login form', diff --git a/src/languages/es.ts b/src/languages/es.ts index f84ec9f6a162..a7ec746835ec 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2831,6 +2831,8 @@ ${amount} para ${merchant} - ${date}`, phoneOrEmail: 'Número de teléfono o correo electrónico', error: { invalidFormatEmailLogin: 'El correo electrónico introducido no es válido. Corrígelo e inténtalo de nuevo.', + agentSignInBlocked: + 'No se puede iniciar sesión directamente en las cuentas de agente. Para usar un agente, inicia sesión con tu propia cuenta y accede a él a través de Copilot.', }, cannotGetAccountDetails: 'No se pudieron cargar los detalles de tu cuenta. Por favor, intenta iniciar sesión de nuevo.', loginForm: 'Formulario de inicio de sesión', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index eb8b2d3b96f3..faea6c2a765e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2962,6 +2962,8 @@ ${amount} pour ${merchant} - ${date}`, loginForm: { phoneOrEmail: 'Téléphone ou e-mail', error: { + agentSignInBlocked: + 'Les comptes d\u2019agent ne permettent pas de se connecter directement. Pour utiliser un agent, connectez-vous avec votre propre compte et accédez-y via Copilot.', invalidFormatEmailLogin: 'L’adresse e-mail saisie est invalide. Veuillez corriger le format et réessayer.', }, cannotGetAccountDetails: 'Impossible de récupérer les détails du compte. Veuillez essayer de vous reconnecter.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 8b55789d977a..3dc80464e652 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2950,6 +2950,7 @@ ${amount} per ${merchant} - ${date}`, loginForm: { phoneOrEmail: 'Telefono o email', error: { + agentSignInBlocked: 'Non puoi accedere direttamente agli account agente. Per usare un agente, accedi con il tuo account e raggiungilo tramite Copilot.', invalidFormatEmailLogin: 'L’email inserita non è valida. Correggi il formato e riprova.', }, cannotGetAccountDetails: 'Impossibile recuperare i dettagli dell’account. Prova ad accedere di nuovo.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7dadd52e6801..21f4138e59cd 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2923,6 +2923,8 @@ ${date} の ${merchant} への ${amount}`, phoneOrEmail: '電話番号またはメールアドレス', error: { invalidFormatEmailLogin: '入力されたメールアドレスが無効です。形式を修正して、もう一度お試しください。', + agentSignInBlocked: + 'エージェントアカウントには直接サインインすることはできません。エージェントを利用するには、ご自身のアカウントでサインインし、Copilot 経由でアクセスしてください。', }, cannotGetAccountDetails: 'アカウントの詳細を取得できませんでした。もう一度サインインしてください。', loginForm: 'ログインフォーム', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 9a7bb8d0ffb0..60c19f6baa0e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2947,6 +2947,7 @@ ${amount} voor ${merchant} - ${date}`, phoneOrEmail: 'Telefoon of e-mail', error: { invalidFormatEmailLogin: 'Het ingevoerde e-mailadres is ongeldig. Corrigeer de notatie en probeer het opnieuw.', + agentSignInBlocked: 'Je kunt niet rechtstreeks inloggen op agent-accounts. Log in met je eigen account en gebruik de agent via Copilot.', }, cannotGetAccountDetails: 'Accountgegevens konden niet worden opgehaald. Probeer opnieuw in te loggen.', loginForm: 'Aanmeldformulier', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f4011e201ee0..29bfc5e9248c 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2941,6 +2941,7 @@ ${amount} dla ${merchant} - ${date}`, phoneOrEmail: 'Telefon lub e-mail', error: { invalidFormatEmailLogin: 'Wprowadzony adres e-mail jest nieprawidłowy. Popraw jego format i spróbuj ponownie.', + agentSignInBlocked: 'Na konta agenta nie można logować się bezpośrednio. Żeby korzystać z agenta, zaloguj się na własne konto i uzyskaj do niego dostęp przez Copilota.', }, cannotGetAccountDetails: 'Nie można pobrać szczegółów konta. Spróbuj zalogować się ponownie.', loginForm: 'Formularz logowania', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index da031573508b..df41fba7aa7a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2941,6 +2941,7 @@ ${amount} para ${merchant} - ${date}`, phoneOrEmail: 'Telefone ou e-mail', error: { invalidFormatEmailLogin: 'O e-mail inserido é inválido. Corrija o formato e tente novamente.', + agentSignInBlocked: 'Contas de agente não podem ser acessadas diretamente. Para usar um agente, entre com a sua própria conta e acesse-o via Copilot.', }, cannotGetAccountDetails: 'Não foi possível recuperar os detalhes da conta. Tente entrar novamente.', loginForm: 'Formulário de login', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ea9c4e854831..b49ca205e666 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2866,6 +2866,7 @@ ${amount},商户:${merchant} - 日期:${date}`, phoneOrEmail: '电话或邮箱', error: { invalidFormatEmailLogin: '输入的邮箱无效。请修正格式后重试。', + agentSignInBlocked: '代理帐户无法直接登录。要使用代理,请先登录您自己的帐户,然后通过 Copilot 访问该代理。', }, cannotGetAccountDetails: '无法获取账户详情。请尝试重新登录。', loginForm: '登录表单', diff --git a/src/libs/SessionUtils.ts b/src/libs/SessionUtils.ts index 250dfdbd74d5..0687784aaf0c 100644 --- a/src/libs/SessionUtils.ts +++ b/src/libs/SessionUtils.ts @@ -108,7 +108,7 @@ function checkIfShouldUseNewPartnerName(partnerUserID?: string): boolean { return false; } -const AGENT_EMAIL_REGEX = /^agent_\d+@expensify\.ai$/; +const AGENT_EMAIL_REGEX = /^agent_\d+@expensify\.ai$/i; function isAgentEmail(email?: string): boolean { if (!email) { diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx index a156bd8ff94a..62437474629e 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx @@ -24,6 +24,7 @@ import {getLatestErrorMessage} from '@libs/ErrorUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; +import {isAgentEmail} from '@libs/SessionUtils'; import StringUtils from '@libs/StringUtils'; import {isNumericWithSpecialChars, isValidEmailWithTLD} from '@libs/ValidationUtils'; import Visibility from '@libs/Visibility'; @@ -140,6 +141,12 @@ function BaseLoginForm({submitBehavior = 'submit', isVisible, ref}: BaseLoginFor const loginTrim = StringUtils.removeInvisibleCharacters(login.trim()); + if (isAgentEmail(loginTrim)) { + setFormError('loginForm.error.agentSignInBlocked'); + isLoading.current = false; + return; + } + const phoneLogin = appendCountryCode(getPhoneNumberWithoutSpecialChars(loginTrim), countryCode); const parsedPhoneNumber = parsePhoneNumber(phoneLogin); diff --git a/tests/ui/BaseLoginFormTest.tsx b/tests/ui/BaseLoginFormTest.tsx new file mode 100644 index 000000000000..9b6070279043 --- /dev/null +++ b/tests/ui/BaseLoginFormTest.tsx @@ -0,0 +1,135 @@ +import type * as ReactNavigationNative from '@react-navigation/native'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import {LoginProvider} from '@pages/signin/SignInLoginContext'; +import {beginSignIn} from '@userActions/Session'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useIsFocused: () => true, + }; +}); + +const AGENT_ERROR = "Agent accounts can't be signed into directly. To use an agent, sign in with your own account and access it via Copilot."; +const INVALID_EMAIL_ERROR = 'The email entered is invalid. Please fix the format and try again.'; + +jest.mock('@hooks/useLocalize', () => + jest.fn(() => ({ + translate: jest.fn((key: string) => { + switch (key) { + case 'loginForm.error.agentSignInBlocked': + return AGENT_ERROR; + case 'loginForm.error.invalidFormatEmailLogin': + return INVALID_EMAIL_ERROR; + case 'loginForm.phoneOrEmail': + return 'Phone or email'; + case 'loginForm.loginForm': + return 'Login form'; + case 'common.continue': + return 'Continue'; + case 'common.signInWith': + return 'Sign in with'; + case 'common.pleaseEnterEmailOrPhoneNumber': + return 'Please enter an email or phone number'; + default: + return key; + } + }), + numberFormat: jest.fn(), + })), +); + +jest.mock('@hooks/useResponsiveLayout', () => + jest.fn(() => ({ + shouldUseNarrowLayout: false, + isInNarrowPaneModal: false, + })), +); + +jest.mock('@userActions/Session', () => ({ + beginSignIn: jest.fn(), + clearAccountMessages: jest.fn(), + clearSignInData: jest.fn(), +})); + +jest.mock('@userActions/CloseAccount', () => ({ + setDefaultData: jest.fn(), +})); + +// Use require to get the default export after all jest.mock calls are hoisted +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access +const BaseLoginForm = require('@pages/signin/LoginForm/BaseLoginForm').default; + +const mockBeginSignIn = beginSignIn as jest.MockedFunction; + +function renderForm() { + return render( + + + , + ); +} + +describe('BaseLoginForm', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await Onyx.set(ONYXKEYS.ACCOUNT, { + isLoading: false, + errors: null, + }); + await waitForBatchedUpdates(); + }); + + it('shows agent sign-in blocked error when an agent email is entered', async () => { + renderForm(); + + const input = screen.getByTestId('username'); + fireEvent.changeText(input, 'agent_123@expensify.ai'); + + const continueButton = screen.getByText('Continue'); + fireEvent.press(continueButton); + + await waitFor(() => { + expect(screen.getByText(AGENT_ERROR)).toBeTruthy(); + }); + expect(mockBeginSignIn).not.toHaveBeenCalled(); + }); + + it('blocks agent email regardless of case', async () => { + renderForm(); + + const input = screen.getByTestId('username'); + fireEvent.changeText(input, 'AGENT_123@EXPENSIFY.AI'); + + const continueButton = screen.getByText('Continue'); + fireEvent.press(continueButton); + + await waitFor(() => { + expect(screen.getByText(AGENT_ERROR)).toBeTruthy(); + }); + expect(mockBeginSignIn).not.toHaveBeenCalled(); + }); + + it('proceeds with sign-in for a normal email', async () => { + renderForm(); + + const input = screen.getByTestId('username'); + fireEvent.changeText(input, 'user@expensify.com'); + + const continueButton = screen.getByText('Continue'); + fireEvent.press(continueButton); + + await waitFor(() => { + expect(mockBeginSignIn).toHaveBeenCalledWith('user@expensify.com'); + }); + }); +});