diff --git a/dotcom-rendering/fixtures/manual/sign-in-gate.ts b/dotcom-rendering/fixtures/manual/sign-in-gate.ts index 037037060d4..6191939b74a 100644 --- a/dotcom-rendering/fixtures/manual/sign-in-gate.ts +++ b/dotcom-rendering/fixtures/manual/sign-in-gate.ts @@ -18,6 +18,25 @@ export const mockAuxiaResponseDismissible = { }, }; +export const mockAuxiaResponseDismissibleV2 = { + status: true, + data: { + userTreatment: { + treatmentId: 'auxia-treatment-001', + treatmentTrackingId: 'tracking-001', + surface: 'signin-gate', + treatmentContent: JSON.stringify({ + title: 'A small step for great Journalism. Sign in.', + subtitle: '', + body: "It's free and only takes 30 seconds.", + first_cta_name: 'Create account', + first_cta_link: 'https://profile.theguardian.com/register', + second_cta_name: "I'll do it later", + }), + }, + }, +}; + export const mockAuxiaResponseNonDismissible = { status: true, data: { @@ -38,6 +57,26 @@ export const mockAuxiaResponseNonDismissible = { }, }; +export const mockAuxiaResponseNonDismissibleV2 = { + status: true, + data: { + userTreatment: { + treatmentId: 'auxia-treatment-002', + treatmentTrackingId: 'tracking-002', + surface: 'signin-gate', + treatmentContent: JSON.stringify({ + title: 'Sorry for the interruption. We believe in free, independent journalism for all. Sign in or register to keep reading.', + subtitle: + "Once you are signed in, we'll bring you back here shortly.", + body: '', + first_cta_name: 'Create account', + first_cta_link: 'https://profile.theguardian.com/register', + second_cta_name: '', + }), + }, + }, +}; + export const mockAuxiaResponseLegacy = { status: true, data: { @@ -66,14 +105,25 @@ export const getAuxiaMock = (payload: string): unknown => { const body = JSON.parse(payload) as Record; const mocks = { - dismissable: mockAuxiaResponseDismissible, - 'non-dismissable': mockAuxiaResponseNonDismissible, - legacy: mockAuxiaResponseLegacy, - 'no-treatment': mockAuxiaResponseNoTreatment, + v1: { + dismissable: mockAuxiaResponseDismissible, + 'non-dismissable': mockAuxiaResponseNonDismissible, + legacy: mockAuxiaResponseLegacy, + 'no-treatment': mockAuxiaResponseNoTreatment, + }, + v2: { + dismissable: mockAuxiaResponseDismissibleV2, + 'non-dismissable': mockAuxiaResponseNonDismissibleV2, + legacy: mockAuxiaResponseLegacy, + 'no-treatment': mockAuxiaResponseNoTreatment, + }, }; - const key = body.sectionId as keyof typeof mocks; - const mock: unknown = mocks[key]; + const key = body.sectionId as keyof (typeof mocks)[keyof typeof mocks]; + const version = (body.articleIdentifier as string).includes('v2') + ? 'v2' + : 'v1'; + const mock: unknown = mocks[version][key]; return mock ?? mockAuxiaResponseNoTreatment; }; diff --git a/dotcom-rendering/src/components/AuthProviderButtons/AuthProviderButtons.tsx b/dotcom-rendering/src/components/AuthProviderButtons/AuthProviderButtons.tsx index 3053615b2c3..e2e07e4b7f8 100644 --- a/dotcom-rendering/src/components/AuthProviderButtons/AuthProviderButtons.tsx +++ b/dotcom-rendering/src/components/AuthProviderButtons/AuthProviderButtons.tsx @@ -13,6 +13,7 @@ import { } from '@guardian/source/react-components'; import React from 'react'; import { buildUrlWithQueryParams } from '../../lib/routeUtils'; +import type { AuxiaGateVersion } from '../SignInGate/types'; import type { IsNativeApp, QueryParams } from './types'; type AuthButtonProvider = 'social' | 'email'; @@ -22,6 +23,7 @@ type AuthProviderButtonsProps = { isNativeApp?: IsNativeApp; providers: AuthButtonProvider[]; onClick?: (provider: AuthButtonProvider) => void; + signInGateVersion?: AuxiaGateVersion; }; type AuthProviderButtonProps = { @@ -35,13 +37,13 @@ type AuthProviderButtonProps = { // The gap between elements in the main section of MinimalLayout. export const SECTION_GAP = remSpace[3]; // 12px -export const mainSectionStyles = css` +export const mainSectionStyles = (signInGateVersion: AuxiaGateVersion) => css` display: flex; flex-direction: column; gap: ${SECTION_GAP}; ${from.phablet} { - flex-direction: row; + flex-direction: ${signInGateVersion === 'v1' ? 'row' : 'column'}; } `; @@ -152,10 +154,11 @@ export const AuthProviderButtons = ({ isNativeApp, providers, onClick, + signInGateVersion = 'v1', }: AuthProviderButtonsProps) => { const buttonOrder = getButtonOrder(isNativeApp); return ( -
+
{providers.includes('social') && buttonOrder.map((socialProvider) => ( { - const tags: TagType[] = [ - { id: 'politics/politics', type: 'Keyword', title: 'Politics' }, - { id: 'world/europe-news', type: 'Keyword', title: 'Europe News' }, - ]; +const tags: TagType[] = [ + { id: 'politics/politics', type: 'Keyword', title: 'Politics' }, + { id: 'world/europe-news', type: 'Keyword', title: 'Europe News' }, +]; +export const signInGateSelectorStoryDismissable = () => { return (
{ - const tags: TagType[] = [ - { id: 'politics/politics', type: 'Keyword', title: 'Politics' }, - { id: 'world/europe-news', type: 'Keyword', title: 'Europe News' }, - ]; - return (
{ - const tags: TagType[] = [ - { id: 'politics/politics', type: 'Keyword', title: 'Politics' }, - { id: 'world/europe-news', type: 'Keyword', title: 'Europe News' }, - ]; - return (
{ signInGateSelectorStoryLegacy.storyName = 'sign_in_gate_selector_legacy'; export const signInGateSelectorStoryNoTreatment = () => { - const tags: TagType[] = [ - { id: 'politics/politics', type: 'Keyword', title: 'Politics' }, - { id: 'world/europe-news', type: 'Keyword', title: 'Europe News' }, - ]; - return (
{ signInGateSelectorStoryNoTreatment.storyName = 'sign_in_gate_selector_no_treatment'; + +export const auxiaV2DismissibleModal = () => { + return ( +
+ +
+ ); +}; + +auxiaV2DismissibleModal.storyName = 'sign_in_gate_auxia_v2_modal_dismissible'; + +export const auxiaV2NonDismissibleModal = () => { + return ( +
+ +
+ ); +}; + +auxiaV2NonDismissibleModal.storyName = + 'sign_in_gate_auxia_v2_modal_non_dismissible'; diff --git a/dotcom-rendering/src/components/SignInGate/gateDesigns/SignInGateAuxiaV2.tsx b/dotcom-rendering/src/components/SignInGate/gateDesigns/SignInGateAuxiaV2.tsx new file mode 100644 index 00000000000..0b9f9682377 --- /dev/null +++ b/dotcom-rendering/src/components/SignInGate/gateDesigns/SignInGateAuxiaV2.tsx @@ -0,0 +1,626 @@ +import { css } from '@emotion/react'; +import { + from, + headlineMedium20, + headlineMedium28, + headlineMedium34, + headlineMedium42, + palette, + space, + textSans14, + textSans15, + textSans17, + textSansBold15, +} from '@guardian/source/foundations'; +import { SvgCross, SvgGuardianLogo } from '@guardian/source/react-components'; +import { useEffect } from 'react'; +import { AuthProviderButtons } from '../../AuthProviderButtons/AuthProviderButtons'; +import { useConfig } from '../../ConfigContext'; +import { ExternalLink } from '../../ExternalLink/ExternalLink'; +import { InformationBox } from '../../InformationBox/InformationBox'; +import { GuardianTerms } from '../../Terms/Terms'; +import { trackLink } from '../componentEventTracking'; +import type { SignInGatePropsAuxia, TreatmentContentDecoded } from '../types'; + +const DividerWithOr = () => { + return ( +
+
+ or continue with +
+
+ ); +}; + +export const SignInGateAuxiaV2 = ({ + signInUrl, + dismissGate, + abTest, + ophanComponentId, + userTreatment, + logTreatmentInteractionCall, +}: SignInGatePropsAuxia) => { + const { renderingTarget } = useConfig(); + + const { + title, + subtitle, + body, + first_cta_name: firstCtaName, + first_cta_link: firstCtaLink, + second_cta_name: secondCtaName, + } = JSON.parse(userTreatment.treatmentContent) as TreatmentContentDecoded; + + const has = (s?: string) => !!s && s.trim() !== ''; + const isDismissible = has(secondCtaName); + const dismissStatusLabel = isDismissible + ? 'dismissible' + : 'non-dismissible'; + + // Prevent body scroll when modal is open + useEffect(() => { + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = ''; + }; + }, []); + + // Handle ESC key to close modal + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isDismissible) { + dismissGate(); + trackLink( + ophanComponentId, + 'escape-key-dismiss', + renderingTarget, + abTest, + ); + void logTreatmentInteractionCall('DISMISSED', ''); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [ + isDismissible, + dismissGate, + ophanComponentId, + renderingTarget, + abTest, + logTreatmentInteractionCall, + ]); + + const handleBackdropClick = ( + event: React.MouseEvent | React.KeyboardEvent, + ) => { + const isValid = + ((event.type === 'keyup' && + (event as React.KeyboardEvent).key === 'Escape') || + event.target === event.currentTarget) && + isDismissible; + + if (!isValid) { + return; + } + + dismissGate(); + trackLink( + ophanComponentId, + 'backdrop-dismiss', + renderingTarget, + abTest, + ); + void logTreatmentInteractionCall('DISMISSED', ''); + }; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- div needs click and keyup handlers for modal backdrop dismiss functionality +
+
+
+
+ + + {isDismissible && ( + + )} +
+ +
+
+

{title}

+ {has(subtitle) && ( +

{subtitle}

+ )} + {has(body) &&

{body}

} +
+ + The Guardian logo +
+
+
+
+
+

Sign in

+ + {isDismissible && ( + + )} +
+
+ { + trackLink( + ophanComponentId, + `sign-in-${provider}-${dismissStatusLabel}`, + renderingTarget, + abTest, + ); + void logTreatmentInteractionCall( + 'CLICKED', + 'SIGN-IN-LINK', + ); + }} + signInGateVersion="v2" + /> +
+ + + +
+ { + trackLink( + ophanComponentId, + `sign-in-${provider}-${dismissStatusLabel}`, + renderingTarget, + abTest, + ); + void logTreatmentInteractionCall( + 'CLICKED', + 'SIGN-IN-LINK', + ); + }} + signInGateVersion="v2" + /> +
+ +
+ + + +
+ + {has(firstCtaName) && has(firstCtaLink) && ( +

+ Not signed in before?{' '} + { + trackLink( + ophanComponentId, + `register-link-${dismissStatusLabel}`, + renderingTarget, + abTest, + ); + void logTreatmentInteractionCall( + 'CLICKED', + 'REGISTER-LINK', + ); + }} + data-testid="sign-in-gate-main_register" + data-ignore="global-link-styling" + > + {firstCtaName} + +

+ )} +
+
+
+
+ ); +}; + +// --- Modal Styling --- +const modalOverlay = css` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: ${space[4]}px; +`; + +const modalContainer = css` + background: white; + border-radius: ${space[4]}px; + display: flex; + flex-direction: column; + gap: ${space[4]}px; + max-width: 900px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + padding: ${space[3]}px; + position: relative; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + + ${from.phablet} { + gap: ${space[2]}px; + max-width: 460px; + padding: 0; + } + + ${from.desktop} { + flex-direction: row; + gap: 0; + height: 600px; + max-width: 940px; + } + + .non-dismissible & { + gap: 0; + padding: 0; + } +`; + +// --- New Layout Containers --- +const topContainer = css` + display: flex; + flex-direction: column; + + ${from.phablet} { + border-bottom: 0.5px solid ${palette.brand[400]}; + padding: ${space[4]}px ${space[4]}px 0 ${space[4]}px; + } + + ${from.desktop} { + border-bottom: 0; + border-right: 0.5px solid ${palette.brand[400]}; + flex-direction: column-reverse; + justify-content: space-between; + padding: ${space[6]}px ${space[4]}px ${space[4]}px ${space[8]}px; + width: 470px; + } + + .non-dismissible & { + background: ${palette.brand[400]}; + padding: ${space[3]}px; + + ${from.phablet} { + border: 0; + padding: ${space[4]}px; + } + + ${from.desktop} { + border: 0; + padding: ${space[6]}px ${space[4]}px ${space[4]}px ${space[8]}px; + width: 470px; + } + } +`; + +const contentContainer = css` + display: flex; + flex-direction: column; + gap: ${space[5]}px; + + ${from.phablet} { + padding: ${space[4]}px; + } + + ${from.desktop} { + flex-direction: row; + gap: ${space[6]}px; + padding: ${space[6]}px ${space[10]}px; + width: 470px; + } + + .non-dismissible & { + padding: ${space[3]}px; + + ${from.phablet} { + padding: ${space[4]}px; + } + + ${from.desktop} { + padding: ${space[6]}px ${space[10]}px; + width: 470px; + } + } +`; + +const headerSection = css` + display: flex; + flex-direction: row; + + ${from.desktop} { + flex-direction: column-reverse; + gap: ${space[6]}px; + padding-right: ${space[4]}px; + max-width: 100%; + } +`; + +const headerCopy = css` + display: flex; + flex-direction: column; + flex-grow: 1; + gap: ${space[4]}px; + ${from.desktop} { + gap: ${space[5]}px; + } +`; + +const headerImage = css` + display: none; + + ${from.phablet} { + align-self: flex-end; + display: block; + max-width: 158px; + width: 100%; + } + + ${from.desktop} { + align-self: center; + max-height: 276px; + max-width: 284px; + width: 100%; + object-fit: contain; + } + + .non-dismissible & { + display: none; + } +`; + +const actionsSection = css` + flex: 1; +`; + +// --- Existing Styling --- +const topBar = css` + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + padding: 0 0 ${space[6]}px; + + ${from.desktop} { + padding: 0; + } +`; + +const signInTopBar = css` + display: none; + + ${from.desktop} { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 0 ${space[4]}px; + + h2 { + ${headlineMedium34}; + } + } + + .non-dismissible & { + ${from.desktop} { + h2 { + ${headlineMedium34}; + color: ${palette.brand[400]}; + } + } + } +`; + +const dismissButtonStyles = css` + background: transparent; + border: none; + cursor: pointer; + padding: 0; + + ${from.desktop} { + display: none; + } +`; + +const signInDismissButtonStyles = css` + display: none; + + ${from.desktop} { + display: inline-flex; + background: transparent; + border: none; + cursor: pointer; + padding: 0; + } +`; + +const subHeadingStyles = css` + ${headlineMedium28}; + color: ${palette.brand[400]}; + white-space: pre-line; + margin-top: 0; /* Remove default margin for better spacing */ + + ${from.phablet} { + ${headlineMedium34}; + padding: ${space[2]}px 0 ${space[5]}px; + } + + ${from.desktop} { + ${headlineMedium42}; + padding: 0; + } + + .non-dismissible & { + ${headlineMedium20}; + color: ${palette.neutral[100]}; + + ${from.phablet} { + ${headlineMedium28}; + padding: ${space[1]}px 0 0; + } + + ${from.desktop} { + ${headlineMedium42}; + padding: 0; + } + } +`; + +const descriptionText = css` + ${textSans14}; + white-space: pre-line; + + ${from.phablet} { + ${textSans15}; + padding-bottom: ${space[4]}px; + } + + ${from.desktop} { + ${textSans17}; + } + + .non-dismissible & { + color: ${palette.neutral[100]}; + + ${from.phablet} { + padding: 0; + } + } +`; + +const socialContainer = css` + padding-bottom: ${space[3]}px; + + ${from.desktop} { + padding-bottom: ${space[5]}px; + } +`; + +const emailContainer = css` + padding-bottom: ${space[4]}px; + + ${from.phablet} { + padding-bottom: ${space[3]}px; + } + + ${from.desktop} { + padding-bottom: ${space[4]}px; + } +`; + +const termsBox = css` + padding-bottom: ${space[3]}px; + + ${from.phablet} { + margin-bottom: ${space[3]}px; + padding-bottom: ${space[4]}px; + border-bottom: 1px solid ${palette.neutral[86]}; + } +`; + +const createAccountText = css` + ${textSans15}; + color: ${palette.neutral[10]}; + margin-bottom: 0; + + a { + ${textSansBold15}; + } +`; + +const dividerContainer = css` + display: flex; + align-items: center; + justify-content: center; + padding-bottom: ${space[4]}px; +`; + +const line = css` + flex: 1; + border: none; + border-top: 1px solid ${palette.neutral[73]}; +`; + +const orText = css` + ${textSansBold15}; + color: ${palette.neutral[46]}; + padding: 0 8px; + white-space: nowrap; + line-height: 20px; + + ${from.phablet} { + line-height: 24px; + } +`; diff --git a/dotcom-rendering/src/components/SignInGate/types.ts b/dotcom-rendering/src/components/SignInGate/types.ts index 817b7e9ccd5..acc5dcff239 100644 --- a/dotcom-rendering/src/components/SignInGate/types.ts +++ b/dotcom-rendering/src/components/SignInGate/types.ts @@ -230,3 +230,9 @@ export type SignInGatePropsAuxia = { actionName: AuxiaInteractionActionName, ) => Promise; }; + +export type AuxiaGateVersion = 'v1' | 'v2'; + +export interface AuxiaGateVersionConfig { + version: AuxiaGateVersion; +} diff --git a/dotcom-rendering/src/components/SignInGateSelector.importable.tsx b/dotcom-rendering/src/components/SignInGateSelector.importable.tsx index 309f29bae3a..65de1356e66 100644 --- a/dotcom-rendering/src/components/SignInGateSelector.importable.tsx +++ b/dotcom-rendering/src/components/SignInGateSelector.importable.tsx @@ -20,11 +20,13 @@ import { submitComponentEventTracking } from './SignInGate/componentEventTrackin import { retrieveDismissedCount } from './SignInGate/dismissGate'; import { pageIdIsAllowedForGating } from './SignInGate/displayRules'; import { SignInGateAuxiaV1 } from './SignInGate/gateDesigns/SignInGateAuxiaV1'; +import { SignInGateAuxiaV2 } from './SignInGate/gateDesigns/SignInGateAuxiaV2'; import { signInGateComponent as gateLegacyComponent } from './SignInGate/gates/main-control'; import type { AuxiaAPIResponseDataUserTreatment, AuxiaGateDisplayData, AuxiaGateReaderPersonalData, + AuxiaGateVersion, AuxiaInteractionActionName, AuxiaInteractionInteractionType, AuxiaProxyGetTreatmentsPayload, @@ -50,6 +52,7 @@ type Props = { idUrl?: string; contributionsServiceUrl: string; editionId: EditionId; + signInGateVersion?: AuxiaGateVersion; }; // function to generate the profile.theguardian.com url with tracking params @@ -146,6 +149,7 @@ export const SignInGateSelector = ({ idUrl = 'https://profile.theguardian.com', contributionsServiceUrl, editionId, + signInGateVersion, }: Props) => { const abTestAPI = useAB()?.api; const userIsInAuxiaExperiment = !!abTestAPI?.isUserInVariant( @@ -170,6 +174,7 @@ export const SignInGateSelector = ({ sectionId={sectionId} tags={tags} isAuxiaAudience={userIsInAuxiaExperiment} + signInGateVersion={signInGateVersion} /> ); }; @@ -209,6 +214,7 @@ type PropsAuxia = { sectionId: string; tags: TagType[]; isAuxiaAudience: boolean; // [1] + signInGateVersion?: AuxiaGateVersion; }; // [1] If true, it indicates that we are using the component for the regular Auxia share of the Audience @@ -228,6 +234,7 @@ interface ShowSignInGateAuxiaProps { interactionType: AuxiaInteractionInteractionType, actionName: AuxiaInteractionActionName, ) => Promise; + signInGateVersion?: AuxiaGateVersion; } const decideIsSupporter = (): boolean => { @@ -512,6 +519,24 @@ const buildAbTestTrackingAuxiaVariant = ( }; }; +const getAuxiaGateVersion = ( + signInGateVersion?: AuxiaGateVersion, +): AuxiaGateVersion => { + if (signInGateVersion) { + return signInGateVersion; + } + + const params = new URLSearchParams(window.location.search); + const version = params.get('auxia_gate_version'); + + if (version === 'v2') { + return 'v2'; + } + + // Default to v1 + return 'v1'; +}; + const SignInGateSelectorAuxia = ({ host = 'https://theguardian.com/', pageId, @@ -524,6 +549,7 @@ const SignInGateSelectorAuxia = ({ sectionId, tags, isAuxiaAudience, + signInGateVersion, }: PropsAuxia) => { const [isGateDismissed, setIsGateDismissed] = useState( undefined, @@ -671,6 +697,7 @@ const SignInGateSelectorAuxia = ({ ); }); }} + signInGateVersion={signInGateVersion} /> )} @@ -688,11 +715,15 @@ const ShowSignInGateAuxia = ({ treatmentId, renderingTarget, logTreatmentInteractionCall, + signInGateVersion, }: ShowSignInGateAuxiaProps) => { const componentId = 'main_variant_5'; const checkoutCompleteCookieData = undefined; const personaliseSignInGateAfterCheckoutSwitch = undefined; + // Get the gate version configuration + const gateVersion = getAuxiaGateVersion(signInGateVersion); + useOnce(() => { void auxiaLogTreatmentInteraction( contributionsServiceUrl, @@ -717,32 +748,36 @@ const ShowSignInGateAuxia = ({ { component: { componentType: 'SIGN_IN_GATE', - id: treatmentId, + id: `${treatmentId}_${gateVersion}`, // Include version in tracking }, action: 'VIEW', abTest: buildAbTestTrackingAuxiaVariant(treatmentId), }, renderingTarget, ); - }, [componentId]); + }, [componentId, gateVersion]); + + const commonProps = { + guUrl: host, + signInUrl, + dismissGate: () => { + setShowGate(false); + }, + abTest, + ophanComponentId: componentId, + checkoutCompleteCookieData, + personaliseSignInGateAfterCheckoutSwitch, + userTreatment, + logTreatmentInteractionCall, + }; return ( <> - { - setShowGate(false); - }} - abTest={abTest} - ophanComponentId={componentId} - checkoutCompleteCookieData={checkoutCompleteCookieData} - personaliseSignInGateAfterCheckoutSwitch={ - personaliseSignInGateAfterCheckoutSwitch - } - userTreatment={userTreatment} - logTreatmentInteractionCall={logTreatmentInteractionCall} - /> + {gateVersion === 'v2' ? ( + + ) : ( + + )} ); };