diff --git a/cypress/integration/ete/sign_in_passcode.8.cy.ts b/cypress/integration/ete/sign_in_passcode.8.cy.ts index 2733f8968..685f5bb0b 100644 --- a/cypress/integration/ete/sign_in_passcode.8.cy.ts +++ b/cypress/integration/ete/sign_in_passcode.8.cy.ts @@ -171,7 +171,42 @@ describe('Sign In flow, with passcode', () => { }); }); - it('selects password option to sign in', () => { + it('selects password option to sign in from initial sign in page', () => { + cy + .createTestUser({ + isUserEmailValidated: true, + }) + ?.then(({ emailAddress, finalPassword }) => { + cy.visit(`/signin`); + cy.get('input[name=email]').type(emailAddress); + + cy.contains('Sign in with a password instead').click(); + + // password page + cy.url().should('include', '/signin/password'); + cy.get('input[name=email]').should('have.value', emailAddress); + cy.get('input[name=password]').type(finalPassword); + cy.get('[data-cy="main-form-submit-button"]').click(); + cy.url().should('include', 'https://m.code.dev-theguardian.com/'); + }); + }); + + it('selects password option to sign in from the initial sign in page and show correct error page on incorrect password', () => { + const emailAddress = randomMailosaurEmail(); + cy.visit(`/signin`); + cy.get('input[name=email]').type(emailAddress); + cy.contains('Sign in with a password instead').click(); + + // password page + cy.url().should('include', '/signin/password'); + cy.get('input[name=email]').should('have.value', emailAddress); + cy.get('input[name=password]').type(randomPassword()); + cy.get('[data-cy="main-form-submit-button"]').click(); + cy.url().should('include', '/signin/password'); + cy.contains('Email and password don’t match'); + }); + + it('selects password option to sign in from passcode page', () => { cy .createTestUser({ isUserEmailValidated: true, @@ -184,7 +219,7 @@ describe('Sign In flow, with passcode', () => { // passcode page cy.url().should('include', '/signin/code'); cy.contains('Enter your one-time code'); - cy.contains('Sign in with password instead').click(); + cy.contains('sign in with a password instead').click(); // password page cy.url().should('include', '/signin/password'); @@ -195,7 +230,7 @@ describe('Sign In flow, with passcode', () => { }); }); - it('selects password option to sign in and show correct error page on incorrect password', () => { + it('selects password option to sign in from passcode page and show correct error page on incorrect password', () => { const emailAddress = randomMailosaurEmail(); cy.visit(`/signin?usePasscodeSignIn=true`); cy.get('input[name=email]').type(emailAddress); @@ -203,7 +238,7 @@ describe('Sign In flow, with passcode', () => { // passcode page cy.url().should('include', '/signin/code'); cy.contains('Enter your one-time code'); - cy.contains('Sign in with password instead').click(); + cy.contains('sign in with a password instead').click(); // password page cy.url().should('include', '/signin/password'); diff --git a/src/client/components/EmailSentInformationBox.stories.tsx b/src/client/components/EmailSentInformationBox.stories.tsx index 941a0f230..97066a958 100644 --- a/src/client/components/EmailSentInformationBox.stories.tsx +++ b/src/client/components/EmailSentInformationBox.stories.tsx @@ -83,6 +83,22 @@ export const WithNoAccountInfo = () => { }; WithNoAccountInfo.storyName = 'with noAccountInfo'; +export const WithShowSignInWithPasswordOption = () => { + return ( + {}} + setRecaptchaErrorMessage={() => {}} + changeEmailPage="#" + email="test@example.com" + resendEmailAction="#" + noAccountInfo + showSignInWithPasswordOption + /> + ); +}; +WithShowSignInWithPasswordOption.storyName = + 'with showSignInWithPasswordOption'; + export const WithTimer = () => { return ( ; setRecaptchaErrorMessage: React.Dispatch>; sendAgainTimerInSeconds?: number; + showSignInWithPasswordOption?: boolean; }; const sendAgainFormWrapperStyles = css` @@ -50,6 +51,7 @@ export const EmailSentInformationBox = ({ queryString, shortRequestId, sendAgainTimerInSeconds, + showSignInWithPasswordOption, }: EmailSentInformationBoxProps) => { const timer = useCountdownTimer(sendAgainTimerInSeconds || 0); @@ -89,12 +91,20 @@ export const EmailSentInformationBox = ({ )} {changeEmailPage && ( <> - , or{' '} + ,{!showSignInWithPasswordOption ? <> or : <> } try another address )} + {showSignInWithPasswordOption && ( + <> + , or{' '} + + sign in with a password instead + + + )} . {noAccountInfo && ( diff --git a/src/client/pages/PasscodeEmailSent.tsx b/src/client/pages/PasscodeEmailSent.tsx index 312b54200..f23cb23d6 100644 --- a/src/client/pages/PasscodeEmailSent.tsx +++ b/src/client/pages/PasscodeEmailSent.tsx @@ -7,8 +7,6 @@ import { MinimalLayout } from '@/client/layouts/MinimalLayout'; import { PasscodeInput } from '@/client/components/PasscodeInput'; import { EmailSentInformationBox } from '@/client/components/EmailSentInformationBox'; import { EmailSentProps } from '@/client/pages/EmailSent'; -import { buildUrl } from '@/shared/lib/routeUtils'; -import ThemedLink from '@/client/components/ThemedLink'; type TextType = 'verification' | 'security' | 'generic' | 'signin'; @@ -183,13 +181,6 @@ export const PasscodeEmailSent = ({ formRef={formRef} /> - {showSignInWithPasswordOption && ( - - - Sign in with password instead - - - )} ); diff --git a/src/client/pages/SignIn.tsx b/src/client/pages/SignIn.tsx index dcc7968f0..2dfdf20d4 100644 --- a/src/client/pages/SignIn.tsx +++ b/src/client/pages/SignIn.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { extractMessage, GatewayError, @@ -38,6 +38,7 @@ export type SignInProps = { // flag to determine whether to show the passcode view or password view usePasscodeSignIn?: boolean; hideSocialButtons?: boolean; + focusPasswordField?: boolean; }; const resetPassword = css` @@ -145,7 +146,23 @@ export const SignIn = ({ shortRequestId, usePasscodeSignIn = false, hideSocialButtons = false, + focusPasswordField = false, }: SignInProps) => { + const [currentEmail, setCurrentEmail] = React.useState(email); + + // autofocus the password input field when the page loads if it exists + // and the focusPasswordField flag is set to true and a default email exists + useEffect(() => { + if (typeof window !== 'undefined' && focusPasswordField && email) { + const passwordInput: HTMLInputElement | null = + window.document.querySelector('input[name="password"]'); + + if (passwordInput) { + passwordInput.focus(); + } + } + }, [focusPasswordField, email]); + // status of the OTP checkbox const selectedView = usePasscodeSignIn ? 'passcode' : 'password'; @@ -193,7 +210,10 @@ export const SignIn = ({ hasGuardianTerms={!isJobs && socialSigninBlocked} hasJobsTerms={isJobs && socialSigninBlocked} > - + setCurrentEmail(e.target.value)} + /> {selectedView === 'password' && ( <> @@ -212,6 +232,27 @@ export const SignIn = ({ ) } + { + // Hidden input to determine whether passcode view is selected + selectedView === 'passcode' && ( + <> + + + Sign in with a password instead + + + + ) + } {!isReauthenticate && ( <> diff --git a/src/client/pages/SignInPage.tsx b/src/client/pages/SignInPage.tsx index 035560d19..ffc59c735 100644 --- a/src/client/pages/SignInPage.tsx +++ b/src/client/pages/SignInPage.tsx @@ -21,7 +21,7 @@ export const SignInPage = ({ queryParams, recaptchaConfig, } = clientState; - const { email, formError } = pageData; + const { email, formError, focusPasswordField } = pageData; const { error: pageError } = globalMessage; const { recaptchaSiteKey } = recaptchaConfig; @@ -31,7 +31,7 @@ export const SignInPage = ({ // determines if the passcode view of the sign in page should be shown const usePasscodeSignIn: boolean = (() => { // if the forcePasswordPage flag is set, we should always show the password view - // for example when the user clicks "sign in with password instead" + // for example when the user clicks "sign in with a password instead" if (forcePasswordPage) { return false; } @@ -62,6 +62,7 @@ export const SignInPage = ({ shortRequestId={clientState.shortRequestId} usePasscodeSignIn={usePasscodeSignIn} hideSocialButtons={hideSocialButtons} + focusPasswordField={focusPasswordField} /> ); }; diff --git a/src/server/lib/queryParams.ts b/src/server/lib/queryParams.ts index bfa899ac1..1474198b2 100644 --- a/src/server/lib/queryParams.ts +++ b/src/server/lib/queryParams.ts @@ -53,6 +53,7 @@ export const parseExpressQueryParams = ( maxAge, useOktaClassic, usePasswordSignIn, + signInEmail, }: Record, // parameters from req.query // some parameters may be manually passed in req.body too, // generally for tracking purposes @@ -78,6 +79,7 @@ export const parseExpressQueryParams = ( maxAge: stringToNumber(maxAge), useOktaClassic: isStringBoolean(useOktaClassic), usePasswordSignIn: isStringBoolean(usePasswordSignIn), + signInEmail, }; }; diff --git a/src/server/routes/signIn.ts b/src/server/routes/signIn.ts index fbf9b3de2..2a866f108 100644 --- a/src/server/routes/signIn.ts +++ b/src/server/routes/signIn.ts @@ -98,7 +98,8 @@ router.get( redirectIfLoggedIn, handleAsyncErrors(async (req: Request, res: ResponseWithRequestState) => { const state = res.locals; - const { encryptedEmail, error, error_description } = state.queryParams; + const { encryptedEmail, error, error_description, signInEmail } = + state.queryParams; // first attempt to get email from IDAPI encryptedEmail if it exists const decryptedEmail = @@ -106,12 +107,13 @@ router.get( // followed by the gateway EncryptedState // if it exists - const email = decryptedEmail || readEmailCookie(req); + const email = decryptedEmail || signInEmail || readEmailCookie(req); const html = renderer('/signin', { requestState: mergeRequestState(state, { pageData: { email, + focusPasswordField: !!email, }, globalMessage: { error: getErrorMessageFromQueryParams(error, error_description), @@ -225,6 +227,7 @@ router.get( requestState: mergeRequestState(state, { pageData: { email, + focusPasswordField: !!email, }, globalMessage: { error: getErrorMessageFromQueryParams(error, error_description), @@ -552,10 +555,13 @@ router.get( '/signin/password', (req: Request, res: ResponseWithRequestState) => { const state = res.locals; + const email = + state.queryParams.signInEmail || readEncryptedStateCookie(req)?.email; const html = renderer('/signin/password', { requestState: mergeRequestState(state, { pageData: { - email: readEncryptedStateCookie(req)?.email, + email, + focusPasswordField: !!email, }, }), pageTitle: 'Sign in', diff --git a/src/shared/lib/routeUtils.ts b/src/shared/lib/routeUtils.ts index 317d817a1..160a22af4 100644 --- a/src/shared/lib/routeUtils.ts +++ b/src/shared/lib/routeUtils.ts @@ -69,9 +69,10 @@ export const buildUrlWithQueryParams =

( path: P, params: PathParams

= >{}, queryParams: QueryParams, + queryParamOverrides?: Partial, ): string => { const url = buildUrl(path, params); - return addQueryParamsToUntypedPath(url, queryParams); + return addQueryParamsToUntypedPath(url, queryParams, queryParamOverrides); }; /** diff --git a/src/shared/model/ClientState.ts b/src/shared/model/ClientState.ts index 14fe20006..76b146c40 100644 --- a/src/shared/model/ClientState.ts +++ b/src/shared/model/ClientState.ts @@ -76,6 +76,9 @@ export interface PageData { // passcode specific passcodeSendAgainTimer?: number; + + // sign in with password specific + focusPasswordField?: boolean; } export interface RecaptchaConfig { diff --git a/src/shared/model/QueryParams.ts b/src/shared/model/QueryParams.ts index fcbd505e6..55be1b565 100644 --- a/src/shared/model/QueryParams.ts +++ b/src/shared/model/QueryParams.ts @@ -70,4 +70,7 @@ export interface QueryParams error?: string; error_description?: string; maxAge?: number; + // only use this to prefill the email input on either sign in page, for passcode or password + // don't rely on this for any other purpose, or to be a valid email + signInEmail?: string; }