Skip to content

Commit

Permalink
refactor(passcodes): move sign in with password instead option
Browse files Browse the repository at this point in the history
  • Loading branch information
coldlink committed Feb 10, 2025
1 parent 14ba445 commit d01ed27
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 22 deletions.
43 changes: 39 additions & 4 deletions cypress/integration/ete/sign_in_passcode.8.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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');
Expand All @@ -195,15 +230,15 @@ 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);
cy.get('[data-cy="main-form-submit-button"]').click();
// 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');
Expand Down
16 changes: 16 additions & 0 deletions src/client/components/EmailSentInformationBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ export const WithNoAccountInfo = () => {
};
WithNoAccountInfo.storyName = 'with noAccountInfo';

export const WithShowSignInWithPasswordOption = () => {
return (
<EmailSentInformationBox
setRecaptchaErrorContext={() => {}}
setRecaptchaErrorMessage={() => {}}
changeEmailPage="#"
email="[email protected]"
resendEmailAction="#"
noAccountInfo
showSignInWithPasswordOption
/>
);
};
WithShowSignInWithPasswordOption.storyName =
'with showSignInWithPasswordOption';

export const WithTimer = () => {
return (
<EmailSentInformationBox
Expand Down
12 changes: 11 additions & 1 deletion src/client/components/EmailSentInformationBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type EmailSentInformationBoxProps = Pick<
>;
setRecaptchaErrorMessage: React.Dispatch<React.SetStateAction<string>>;
sendAgainTimerInSeconds?: number;
showSignInWithPasswordOption?: boolean;
};

const sendAgainFormWrapperStyles = css`
Expand All @@ -50,6 +51,7 @@ export const EmailSentInformationBox = ({
queryString,
shortRequestId,
sendAgainTimerInSeconds,
showSignInWithPasswordOption,
}: EmailSentInformationBoxProps) => {
const timer = useCountdownTimer(sendAgainTimerInSeconds || 0);

Expand Down Expand Up @@ -89,12 +91,20 @@ export const EmailSentInformationBox = ({
)}
{changeEmailPage && (
<>
, or{' '}
,{!showSignInWithPasswordOption ? <> or </> : <> </>}
<ThemedLink href={`${changeEmailPage}${queryString}`}>
try another address
</ThemedLink>
</>
)}
{showSignInWithPasswordOption && (
<>
, or{' '}
<ThemedLink href={`${buildUrl('/signin/password')}${queryString}`}>
sign in with a password instead
</ThemedLink>
</>
)}
.
</InformationBoxText>
{noAccountInfo && (
Expand Down
10 changes: 1 addition & 9 deletions src/client/pages/PasscodeEmailSent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -183,13 +181,6 @@ export const PasscodeEmailSent = ({
formRef={formRef}
/>
</MainForm>
{showSignInWithPasswordOption && (
<MainBodyText>
<ThemedLink href={`${buildUrl('/signin/password')}${queryString}`}>
Sign in with password instead
</ThemedLink>
</MainBodyText>
)}
<EmailSentInformationBox
setRecaptchaErrorContext={setRecaptchaErrorContext}
setRecaptchaErrorMessage={setRecaptchaErrorMessage}
Expand All @@ -203,6 +194,7 @@ export const PasscodeEmailSent = ({
shortRequestId={shortRequestId}
noAccountInfo={noAccountInfo}
sendAgainTimerInSeconds={sendAgainTimerInSeconds}
showSignInWithPasswordOption={showSignInWithPasswordOption}
/>
</MinimalLayout>
);
Expand Down
45 changes: 43 additions & 2 deletions src/client/pages/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
extractMessage,
GatewayError,
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -193,7 +210,10 @@ export const SignIn = ({
hasGuardianTerms={!isJobs && socialSigninBlocked}
hasJobsTerms={isJobs && socialSigninBlocked}
>
<EmailInput defaultValue={email} />
<EmailInput
defaultValue={email}
onChange={(e) => setCurrentEmail(e.target.value)}
/>
{selectedView === 'password' && (
<>
<PasswordInput label="Password" autoComplete="current-password" />
Expand All @@ -212,6 +232,27 @@ export const SignIn = ({
)
}
</MainForm>
{
// Hidden input to determine whether passcode view is selected
selectedView === 'passcode' && (
<>
<MainBodyText>
<ThemedLink
href={buildUrlWithQueryParams(
'/signin/password',
{},
queryParams,
{
signInEmail: currentEmail,
},
)}
>
Sign in with a password instead
</ThemedLink>
</MainBodyText>
</>
)
}
{!isReauthenticate && (
<>
<Divider size="full" cssOverrides={divider} />
Expand Down
5 changes: 3 additions & 2 deletions src/client/pages/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -62,6 +62,7 @@ export const SignInPage = ({
shortRequestId={clientState.shortRequestId}
usePasscodeSignIn={usePasscodeSignIn}
hideSocialButtons={hideSocialButtons}
focusPasswordField={focusPasswordField}
/>
);
};
2 changes: 2 additions & 0 deletions src/server/lib/queryParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const parseExpressQueryParams = (
maxAge,
useOktaClassic,
usePasswordSignIn,
signInEmail,
}: Record<keyof QueryParams, string | undefined>, // parameters from req.query
// some parameters may be manually passed in req.body too,
// generally for tracking purposes
Expand All @@ -78,6 +79,7 @@ export const parseExpressQueryParams = (
maxAge: stringToNumber(maxAge),
useOktaClassic: isStringBoolean(useOktaClassic),
usePasswordSignIn: isStringBoolean(usePasswordSignIn),
signInEmail,
};
};

Expand Down
12 changes: 9 additions & 3 deletions src/server/routes/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,22 @@ 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 =
encryptedEmail && (await decrypt(encryptedEmail, req.ip));

// 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),
Expand Down Expand Up @@ -225,6 +227,7 @@ router.get(
requestState: mergeRequestState(state, {
pageData: {
email,
focusPasswordField: !!email,
},
globalMessage: {
error: getErrorMessageFromQueryParams(error, error_description),
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion src/shared/lib/routeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ export const buildUrlWithQueryParams = <P extends AllRoutes>(
path: P,
params: PathParams<P> = <PathParams<P>>{},
queryParams: QueryParams,
queryParamOverrides?: Partial<QueryParams>,
): string => {
const url = buildUrl(path, params);
return addQueryParamsToUntypedPath(url, queryParams);
return addQueryParamsToUntypedPath(url, queryParams, queryParamOverrides);
};

/**
Expand Down
3 changes: 3 additions & 0 deletions src/shared/model/ClientState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export interface PageData {

// passcode specific
passcodeSendAgainTimer?: number;

// sign in with password specific
focusPasswordField?: boolean;
}

export interface RecaptchaConfig {
Expand Down
3 changes: 3 additions & 0 deletions src/shared/model/QueryParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit d01ed27

Please sign in to comment.