From 0ae787849935a3326603e1a2989603ec8debb0de Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Wed, 31 Dec 2025 17:39:49 +0200 Subject: [PATCH 1/9] wip: load idcta config --- .../AccountContainer/hooks/useIdctaConfig.ts | 89 +++++++++++++++++++ .../idcta/appendCtaQueryParams.ts | 25 ++++++ .../AccountContainer/idcta/getIdctaBaseUrl.ts | 10 +++ src/app/components/AccountContainer/index.tsx | 34 +++++++ src/app/legacy/containers/Header/index.jsx | 3 + 5 files changed, 161 insertions(+) create mode 100644 src/app/components/AccountContainer/hooks/useIdctaConfig.ts create mode 100644 src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts create mode 100644 src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts create mode 100644 src/app/components/AccountContainer/index.tsx diff --git a/src/app/components/AccountContainer/hooks/useIdctaConfig.ts b/src/app/components/AccountContainer/hooks/useIdctaConfig.ts new file mode 100644 index 00000000000..5f688ddc76f --- /dev/null +++ b/src/app/components/AccountContainer/hooks/useIdctaConfig.ts @@ -0,0 +1,89 @@ +import { useEffect, useState } from 'react'; +import { appendCtaQueryParams } from '../idcta/appendCtaQueryParams'; +import { getIdctaConfigUrl } from '../idcta/getIdctaBaseUrl'; + +type UseIdctaConfigArgs = { + ptrt?: string; + userOrigin?: string; + sequenceId?: string; +}; + +type UseIdctaConfigState = { + signInUrl: string | null; + registerUrl: string | null; + isSignInAvailable: boolean; + availability: { signin: string; refresh: string }; +}; + +const initialState: UseIdctaConfigState = { + signInUrl: null, + registerUrl: null, + isSignInAvailable: false, + availability: { signin: '', refresh: '' }, +}; + +export default function useIdctaConfig({ + ptrt, + userOrigin = 'simorgh', + sequenceId, +}: UseIdctaConfigArgs = {}) { + const [state, setState] = useState(initialState); + const [error, setError] = useState(null); + + const idctaConfigUrl = getIdctaConfigUrl(); + + useEffect(() => { + if (!idctaConfigUrl) return; + + const controller = new AbortController(); + + const run = async () => { + try { + setError(null); + + const response = await fetch(idctaConfigUrl, { + cache: 'no-store', + credentials: 'include', + signal: controller.signal, + }); + if (!response.ok) { + throw new Error( + `Failed to fetch IDCTA config: ${response.status} ${response.statusText}`, + ); + } + + const body = await response.json(); + + const signInAvailable = body?.availability?.signin === 'GREEN'; + const unavailable = body?.unavailable_url; + + const signInHref = signInAvailable ? body?.signin_url : unavailable; + const registerHref = signInAvailable ? body?.register_url : unavailable; + + setState({ + signInUrl: signInHref + ? appendCtaQueryParams(signInHref, { ptrt, userOrigin, sequenceId }) + : null, + registerUrl: registerHref + ? appendCtaQueryParams(registerHref, { + ptrt, + userOrigin, + sequenceId, + }) + : null, + isSignInAvailable: signInAvailable, + availability: body?.availability ?? { signin: '', refresh: '' }, + }); + } catch (e) { + if ((e as Error)?.name === 'AbortError') return; + setError(e as Error); + } + }; + + run(); + // eslint-disable-next-line consistent-return + return () => controller.abort(); + }, [idctaConfigUrl, ptrt, userOrigin, sequenceId]); + + return { ...state, error }; +} diff --git a/src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts b/src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts new file mode 100644 index 00000000000..0bd9d6463be --- /dev/null +++ b/src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts @@ -0,0 +1,25 @@ +type Params = { + ptrt?: string; + userOrigin?: string; + sequenceId?: string; +}; + +// eslint-disable-next-line import/prefer-default-export +export const appendCtaQueryParams = ( + url: string, + { ptrt, userOrigin, sequenceId }: Params = {}, +): string => { + const ctaUrl = new URL(url); + + const ptrtQuery = + ptrt || + (typeof window !== 'undefined' && window.location?.href + ? window.location.href + : ''); + + if (ptrtQuery) ctaUrl.searchParams.set('ptrt', ptrtQuery); + if (sequenceId) ctaUrl.searchParams.set('sequenceId', sequenceId); + if (userOrigin) ctaUrl.searchParams.set('userOrigin', userOrigin); + + return ctaUrl.toString(); +}; diff --git a/src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts b/src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts new file mode 100644 index 00000000000..ac81c21a67b --- /dev/null +++ b/src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts @@ -0,0 +1,10 @@ +import isLive from '../../../lib/utilities/isLive'; + +export const getIdctaBaseUrl = (): string => { + return isLive() + ? 'https://idcta.api.bbc.com/idcta' + : 'https://idcta.test.api.bbc.com/idcta'; +}; + +export const getIdctaConfigUrl = (): string => `${getIdctaBaseUrl()}/config`; +export const getIdctaInitUrl = (): string => `${getIdctaBaseUrl()}/init`; diff --git a/src/app/components/AccountContainer/index.tsx b/src/app/components/AccountContainer/index.tsx new file mode 100644 index 00000000000..92c8550fbc1 --- /dev/null +++ b/src/app/components/AccountContainer/index.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import useIdctaConfig from './hooks/useIdctaConfig'; +import MessageBanner from '../MessageBanner'; + +export default function AccountContainer() { + // might not be needed? + const ptrt = useMemo(() => { + if (typeof window === 'undefined') return ''; + const base = window.location.href; + return base; + }, []); + + const config = useIdctaConfig({ + ptrt, + userOrigin: 'simorgh', + }); + + const { signInUrl, isSignInAvailable, error, registerUrl } = config; + + if (error || !isSignInAvailable || !signInUrl || !registerUrl) { + return null; + } + + console.log({ config, ptrt }); + + return ( + + ); +} diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index c46187c025e..f40fe5eb956 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -11,6 +11,7 @@ import { LIVE_PAGE, } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; +import AccountContainer from '#app/components/AccountContainer'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import NavigationContainer from '../Navigation'; @@ -91,6 +92,7 @@ const HeaderContainer = ({ propsForTopBarOJComponent }) => { if (isApp) return null; + // TODO: might add an account container (sign-in/out) as part of the header return (
{isAmp ? ( @@ -110,6 +112,7 @@ const HeaderContainer = ({ propsForTopBarOJComponent }) => { +
); }; From c3a8018f33b3247911e8c0fa7c7b18601d52180e Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Mon, 5 Jan 2026 21:39:44 +0200 Subject: [PATCH 2/9] feat: header account --- .../AccountContainer/HeaderAccount.tsx | 13 +++++++++ .../AccountContainer/idcta/isSignedIn.ts | 8 ++++++ src/app/components/AccountContainer/index.tsx | 28 +++++++++++++------ src/app/legacy/containers/Header/index.jsx | 12 ++++++-- .../containers/Navigation/index.canonical.jsx | 2 ++ .../legacy/containers/Navigation/index.jsx | 6 ++-- .../psammead/psammead-brand/src/index.jsx | 2 ++ 7 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 src/app/components/AccountContainer/HeaderAccount.tsx create mode 100644 src/app/components/AccountContainer/idcta/isSignedIn.ts diff --git a/src/app/components/AccountContainer/HeaderAccount.tsx b/src/app/components/AccountContainer/HeaderAccount.tsx new file mode 100644 index 00000000000..0380b29b04f --- /dev/null +++ b/src/app/components/AccountContainer/HeaderAccount.tsx @@ -0,0 +1,13 @@ +import { isSignedIn } from './idcta/isSignedIn'; + +// TODO: Render an icon with different state +// 1. Render Sign in icon with URL +// 2. Render My Account button +// 3. Render Sign out button (as a test?) +const HeaderAccount = () => { + const isUserSignedIn = isSignedIn(); + + return
Sign In: {String(isUserSignedIn)}
; +}; + +export default HeaderAccount; diff --git a/src/app/components/AccountContainer/idcta/isSignedIn.ts b/src/app/components/AccountContainer/idcta/isSignedIn.ts new file mode 100644 index 00000000000..cf461c52b0f --- /dev/null +++ b/src/app/components/AccountContainer/idcta/isSignedIn.ts @@ -0,0 +1,8 @@ +import Cookie from 'js-cookie'; + +export const MARKER_COOKIE_NAME = 'ckns_id'; + +export const isSignedIn = () => { + if (typeof window === 'undefined') return false; + return Cookie.get(MARKER_COOKIE_NAME) !== undefined; +}; diff --git a/src/app/components/AccountContainer/index.tsx b/src/app/components/AccountContainer/index.tsx index 92c8550fbc1..114e5a9bfaf 100644 --- a/src/app/components/AccountContainer/index.tsx +++ b/src/app/components/AccountContainer/index.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import useIdctaConfig from './hooks/useIdctaConfig'; import MessageBanner from '../MessageBanner'; +import { isSignedIn } from './idcta/isSignedIn'; export default function AccountContainer() { // might not be needed? @@ -11,7 +12,8 @@ export default function AccountContainer() { }, []); const config = useIdctaConfig({ - ptrt, + // TODO: Temp - used for testing + ptrt: 'https://www.bbc.com/hindi?test=true', userOrigin: 'simorgh', }); @@ -21,14 +23,24 @@ export default function AccountContainer() { return null; } - console.log({ config, ptrt }); + const isUserSignedIn = isSignedIn(); + + console.log({ config, ptrt, isUserSignedIn }); return ( - +
+ + +
); } diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index f40fe5eb956..2bafb5eccc8 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -12,6 +12,7 @@ import { } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; import AccountContainer from '#app/components/AccountContainer'; +import HeaderAccount from '#app/components/AccountContainer/HeaderAccount'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import NavigationContainer from '../Navigation'; @@ -35,6 +36,8 @@ const Header = ({ brandRef, borderBottom, skipLink, scriptLink, linkId }) => { }; // linkId={linkId || 'topPage'} is a temporary fix for the a11y nested span's bug experienced in TalkBack, refer to the following issue: https://github.com/bbc/simorgh/issues/9652 + + // TODO: render Header config if global Config is available return (
{showConsentBanner && } @@ -44,7 +47,9 @@ const Header = ({ brandRef, borderBottom, skipLink, scriptLink, linkId }) => { scriptLink={scriptLink} brandRef={brandRef} linkId={linkId || 'topPage'} - /> + > + +
); }; @@ -111,8 +116,9 @@ const HeaderContainer = ({ propsForTopBarOJComponent }) => { {isLite && } - + > + + ); }; diff --git a/src/app/legacy/containers/Navigation/index.canonical.jsx b/src/app/legacy/containers/Navigation/index.canonical.jsx index 046977006b1..49768c4665d 100644 --- a/src/app/legacy/containers/Navigation/index.canonical.jsx +++ b/src/app/legacy/containers/Navigation/index.canonical.jsx @@ -43,6 +43,7 @@ const CanonicalNavigationContainer = ({ scrollableListItems, dropdownListItems, blocks, + children, }) => { const { isLite } = use(RequestContext); const { enabled } = useToggle('topBarOJs'); @@ -73,6 +74,7 @@ const CanonicalNavigationContainer = ({ {dropdownListItems} {enabled && } + {children} ); }; diff --git a/src/app/legacy/containers/Navigation/index.jsx b/src/app/legacy/containers/Navigation/index.jsx index 83966bf7354..e4d63c895d6 100644 --- a/src/app/legacy/containers/Navigation/index.jsx +++ b/src/app/legacy/containers/Navigation/index.jsx @@ -49,7 +49,7 @@ const renderListItems = ( return [...listAcc, listItem]; }, []); -const NavigationContainer = ({ propsForTopBarOJComponent }) => { +const NavigationContainer = ({ propsForTopBarOJComponent, children }) => { const { isAmp, isLite } = use(RequestContext); const { blocks = [] } = propsForTopBarOJComponent || {}; const { @@ -144,7 +144,9 @@ const NavigationContainer = ({ propsForTopBarOJComponent }) => { script={script} service={service} blocks={blocks} - /> + > + {children} + ); }; diff --git a/src/app/legacy/psammead/psammead-brand/src/index.jsx b/src/app/legacy/psammead/psammead-brand/src/index.jsx index ca41762c46d..de283aeb27e 100644 --- a/src/app/legacy/psammead/psammead-brand/src/index.jsx +++ b/src/app/legacy/psammead/psammead-brand/src/index.jsx @@ -188,6 +188,7 @@ const Brand = forwardRef((props, ref) => { isLongBrand = false, skipLink = null, linkId = null, + children, ...rest } = props; @@ -216,6 +217,7 @@ const Brand = forwardRef((props, ref) => { )} {skipLink} {scriptLink &&
{scriptLink}
} + {children} ); From 7eea8e85ee5d4c0f0bb4ec4d9d37c73adc7b1880 Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Tue, 6 Jan 2026 23:25:20 +0200 Subject: [PATCH 3/9] refactor: account provider --- .../components/Account/AccountContainer.tsx | 26 +++++++++ src/app/components/Account/AccountContext.tsx | 58 +++++++++++++++++++ .../Account/HeaderAccount/index.styles.tsx | 33 +++++++++++ .../Account/HeaderAccount/index.tsx | 49 ++++++++++++++++ .../hooks/useIdctaConfig.ts | 1 + .../idcta/appendCtaQueryParams.ts | 0 .../Account/idcta/fetchIdctaConfig.ts | 36 ++++++++++++ .../idcta/getIdctaBaseUrl.ts | 0 .../idcta/isSignedIn.ts | 1 + .../AccountContainer/HeaderAccount.tsx | 13 ----- src/app/components/AccountContainer/index.tsx | 46 --------------- src/app/legacy/containers/Header/index.jsx | 4 +- ws-nextjs-app/pages/_app.page.tsx | 37 +++++++----- 13 files changed, 229 insertions(+), 75 deletions(-) create mode 100644 src/app/components/Account/AccountContainer.tsx create mode 100644 src/app/components/Account/AccountContext.tsx create mode 100644 src/app/components/Account/HeaderAccount/index.styles.tsx create mode 100644 src/app/components/Account/HeaderAccount/index.tsx rename src/app/components/{AccountContainer => Account}/hooks/useIdctaConfig.ts (97%) rename src/app/components/{AccountContainer => Account}/idcta/appendCtaQueryParams.ts (100%) create mode 100644 src/app/components/Account/idcta/fetchIdctaConfig.ts rename src/app/components/{AccountContainer => Account}/idcta/getIdctaBaseUrl.ts (100%) rename src/app/components/{AccountContainer => Account}/idcta/isSignedIn.ts (80%) delete mode 100644 src/app/components/AccountContainer/HeaderAccount.tsx delete mode 100644 src/app/components/AccountContainer/index.tsx diff --git a/src/app/components/Account/AccountContainer.tsx b/src/app/components/Account/AccountContainer.tsx new file mode 100644 index 00000000000..30234640b17 --- /dev/null +++ b/src/app/components/Account/AccountContainer.tsx @@ -0,0 +1,26 @@ +import { use } from 'react'; +import MessageBanner from '../MessageBanner'; +import { AccountContext } from './AccountContext'; + +export default function AccountContainer() { + const { isSignInAvailable, signInUrl, registerUrl } = use(AccountContext); + + if (!isSignInAvailable || !signInUrl || !registerUrl) { + return null; + } + + return ( +
+ + +
+ ); +} diff --git a/src/app/components/Account/AccountContext.tsx b/src/app/components/Account/AccountContext.tsx new file mode 100644 index 00000000000..1a49c064de5 --- /dev/null +++ b/src/app/components/Account/AccountContext.tsx @@ -0,0 +1,58 @@ +import React, { createContext, useMemo } from 'react'; +import { isSignedIn } from './idcta/isSignedIn'; + +export type AccountContextProps = { + isSignInAvailable: boolean; + accountUrl: string; + signInUrl: string; + registerUrl: string; + isSignedIn: boolean | null; +}; + +export const AccountContext = createContext( + {} as AccountContextProps, +); + +export const AccountProvider = ({ + children, + initialConfig = null, +}: { + children: React.ReactNode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initialConfig: any; +}) => { + const signInAvailability = initialConfig.availability.signin === 'GREEN'; + const unavailableUrl = initialConfig?.unavailable_url; + + console.log({ initialConfig }); + // TODO: Consider using `ptrt` if not working by default + const signInUrl = signInAvailability + ? initialConfig?.signin_url + : unavailableUrl; + + const registerUrl = signInAvailability + ? initialConfig?.register_url + : unavailableUrl; + + const accountUrl = + initialConfig?.['foryou-flagpole'] === 'GREEN' + ? initialConfig.foryou_url + : unavailableUrl; + + const isUserSignedIn = signInAvailability ? isSignedIn() : null; + + const value = useMemo( + () => ({ + isSignInAvailable: signInAvailability, + signInUrl, + registerUrl, + accountUrl, + isSignedIn: isUserSignedIn, + }), + [accountUrl, isUserSignedIn, registerUrl, signInAvailability, signInUrl], + ); + + return ( + {children} + ); +}; diff --git a/src/app/components/Account/HeaderAccount/index.styles.tsx b/src/app/components/Account/HeaderAccount/index.styles.tsx new file mode 100644 index 00000000000..0362628c0c5 --- /dev/null +++ b/src/app/components/Account/HeaderAccount/index.styles.tsx @@ -0,0 +1,33 @@ +import { Theme, css } from '@emotion/react'; +import pixelsToRem from '#app/utilities/pixelsToRem'; + +export default { + icon: ({ palette }: Theme) => + css({ + height: `${pixelsToRem(24)}rem`, + width: `${pixelsToRem(24)}rem`, + color: palette.WHITE, + }), + + linkWrapper: ({ palette }) => + css({ + display: 'flex', + gap: `${pixelsToRem(8)}rem`, + justifyContent: 'center', + alignItems: 'center', + textDecoration: 'none', + '&:hover, &:focus': { + textDecoration: 'underline', + textDecorationColor: palette.WHITE, + }, + }), + + linkText: ({ palette, mq }: Theme) => + css({ + color: palette.WHITE, + + [mq.GROUP_1_MAX_WIDTH]: { + display: 'none', + }, + }), +}; diff --git a/src/app/components/Account/HeaderAccount/index.tsx b/src/app/components/Account/HeaderAccount/index.tsx new file mode 100644 index 00000000000..0ef6268eaca --- /dev/null +++ b/src/app/components/Account/HeaderAccount/index.tsx @@ -0,0 +1,49 @@ +import { use } from 'react'; +import Text from '#app/components/Text'; +import { AccountContext } from '../AccountContext'; +import styles from './index.styles'; + +const AccountIcon = () => { + return ( + + ); +}; + +const HeaderAccount = () => { + const { isSignedIn, signInUrl, accountUrl, isSignInAvailable } = + use(AccountContext); + + // TODO: fix hydration error + if (!isSignInAvailable || isSignedIn === null) { + return null; + } + + return ( +
+ + + {isSignedIn ? 'For you' : 'Sign In'}{' '} + + + +
+ ); +}; + +export default HeaderAccount; diff --git a/src/app/components/AccountContainer/hooks/useIdctaConfig.ts b/src/app/components/Account/hooks/useIdctaConfig.ts similarity index 97% rename from src/app/components/AccountContainer/hooks/useIdctaConfig.ts rename to src/app/components/Account/hooks/useIdctaConfig.ts index 5f688ddc76f..e511d1d04ba 100644 --- a/src/app/components/AccountContainer/hooks/useIdctaConfig.ts +++ b/src/app/components/Account/hooks/useIdctaConfig.ts @@ -22,6 +22,7 @@ const initialState: UseIdctaConfigState = { availability: { signin: '', refresh: '' }, }; +// Should be used if config is only needed on the client side export default function useIdctaConfig({ ptrt, userOrigin = 'simorgh', diff --git a/src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts b/src/app/components/Account/idcta/appendCtaQueryParams.ts similarity index 100% rename from src/app/components/AccountContainer/idcta/appendCtaQueryParams.ts rename to src/app/components/Account/idcta/appendCtaQueryParams.ts diff --git a/src/app/components/Account/idcta/fetchIdctaConfig.ts b/src/app/components/Account/idcta/fetchIdctaConfig.ts new file mode 100644 index 00000000000..0788969a704 --- /dev/null +++ b/src/app/components/Account/idcta/fetchIdctaConfig.ts @@ -0,0 +1,36 @@ +import { getIdctaConfigUrl } from './getIdctaBaseUrl'; + +export type IdctaConfigFetchResult = { + // TODO: Extra fields temp for testing + ok: boolean; + status: number; + statusText: string; + body: unknown | null; +}; + +export default async function fetchIdctaConfig(): Promise { + const idctaConfigUrl = getIdctaConfigUrl(); + + try { + const response = await fetch(idctaConfigUrl, { + cache: 'no-store', + }); + + const body = await response.json(); + + return { + ok: response.ok, + status: response.status, + statusText: response.statusText, + body, + }; + } catch (error) { + return { + ok: false, + status: 500, + statusText: + error instanceof Error ? error.message : 'Failed to fetch IDCTA config', + body: null, + }; + } +} diff --git a/src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts b/src/app/components/Account/idcta/getIdctaBaseUrl.ts similarity index 100% rename from src/app/components/AccountContainer/idcta/getIdctaBaseUrl.ts rename to src/app/components/Account/idcta/getIdctaBaseUrl.ts diff --git a/src/app/components/AccountContainer/idcta/isSignedIn.ts b/src/app/components/Account/idcta/isSignedIn.ts similarity index 80% rename from src/app/components/AccountContainer/idcta/isSignedIn.ts rename to src/app/components/Account/idcta/isSignedIn.ts index cf461c52b0f..ea81a5ac24d 100644 --- a/src/app/components/AccountContainer/idcta/isSignedIn.ts +++ b/src/app/components/Account/idcta/isSignedIn.ts @@ -2,6 +2,7 @@ import Cookie from 'js-cookie'; export const MARKER_COOKIE_NAME = 'ckns_id'; +// TODO: Might be moved to the AccountContext instead export const isSignedIn = () => { if (typeof window === 'undefined') return false; return Cookie.get(MARKER_COOKIE_NAME) !== undefined; diff --git a/src/app/components/AccountContainer/HeaderAccount.tsx b/src/app/components/AccountContainer/HeaderAccount.tsx deleted file mode 100644 index 0380b29b04f..00000000000 --- a/src/app/components/AccountContainer/HeaderAccount.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { isSignedIn } from './idcta/isSignedIn'; - -// TODO: Render an icon with different state -// 1. Render Sign in icon with URL -// 2. Render My Account button -// 3. Render Sign out button (as a test?) -const HeaderAccount = () => { - const isUserSignedIn = isSignedIn(); - - return
Sign In: {String(isUserSignedIn)}
; -}; - -export default HeaderAccount; diff --git a/src/app/components/AccountContainer/index.tsx b/src/app/components/AccountContainer/index.tsx deleted file mode 100644 index 114e5a9bfaf..00000000000 --- a/src/app/components/AccountContainer/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useMemo } from 'react'; -import useIdctaConfig from './hooks/useIdctaConfig'; -import MessageBanner from '../MessageBanner'; -import { isSignedIn } from './idcta/isSignedIn'; - -export default function AccountContainer() { - // might not be needed? - const ptrt = useMemo(() => { - if (typeof window === 'undefined') return ''; - const base = window.location.href; - return base; - }, []); - - const config = useIdctaConfig({ - // TODO: Temp - used for testing - ptrt: 'https://www.bbc.com/hindi?test=true', - userOrigin: 'simorgh', - }); - - const { signInUrl, isSignInAvailable, error, registerUrl } = config; - - if (error || !isSignInAvailable || !signInUrl || !registerUrl) { - return null; - } - - const isUserSignedIn = isSignedIn(); - - console.log({ config, ptrt, isUserSignedIn }); - - return ( -
- - -
- ); -} diff --git a/src/app/legacy/containers/Header/index.jsx b/src/app/legacy/containers/Header/index.jsx index 2bafb5eccc8..415d8d61ba2 100644 --- a/src/app/legacy/containers/Header/index.jsx +++ b/src/app/legacy/containers/Header/index.jsx @@ -11,8 +11,8 @@ import { LIVE_PAGE, } from '#app/routes/utils/pageTypes'; import LiteSiteSummary from '#app/components/LiteSiteSummary'; -import AccountContainer from '#app/components/AccountContainer'; -import HeaderAccount from '#app/components/AccountContainer/HeaderAccount'; +import AccountContainer from '#app/components/Account/AccountContainer'; +import HeaderAccount from '#app/components/Account/HeaderAccount/index'; import { ServiceContext } from '../../../contexts/ServiceContext'; import ConsentBanner from '../ConsentBanner'; import NavigationContainer from '../Navigation'; diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index f7a0032b2fd..2fc22f0aba8 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -23,6 +23,8 @@ import cspHeaderResponse, { CspHeaderResponseProps, } from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; +import fetchIdctaConfig from '#app/components/Account/idcta/fetchIdctaConfig'; +import { AccountProvider } from '#app/components/Account/AccountContext'; interface Props extends AppProps { pageProps: { @@ -52,6 +54,7 @@ interface Props extends AppProps { variant?: Variants; isUK?: boolean; country?: string | null; + idctaConfig?: unknown | null; }; } @@ -78,6 +81,7 @@ export default function App({ Component, pageProps }: Props) { variant, isUK, country, + idctaConfig = null, } = pageProps; const { metadata: { atiAnalytics = undefined } = {} } = pageData ?? {}; @@ -115,21 +119,23 @@ export default function App({ Component, pageProps }: Props) { isNextJs={isNextJs} isUK={isUK ?? false} > - - {isAvEmbeds ? ( - - {RenderChildrenOrError} - - ) : ( - + + + {isAvEmbeds ? ( - - {RenderChildrenOrError} - + {RenderChildrenOrError} - - )} - + ) : ( + + + + {RenderChildrenOrError} + + + + )} + + @@ -170,12 +176,14 @@ App.getInitialProps = async ({ ctx }: AppContext) => { const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); const routeSegments = asPath?.split('/')?.filter(Boolean); - const [service] = (routeSegments || []) as [Services]; const toggles = await getToggles(service); addServiceChainAndCspHeaders({ ctx, service, toggles }); + // TODO: Only fetch for the Hindi service? + const idctaConfigResult = await fetchIdctaConfig(); + const idctaConfig = idctaConfigResult.ok ? idctaConfigResult.body : null; return { pageProps: { @@ -185,6 +193,7 @@ App.getInitialProps = async ({ ctx }: AppContext) => { isLite, isNextJs: true, toggles, + idctaConfig, }, }; }; From 43a3d5b8cceef288f05e3bab34146262f380995a Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Tue, 6 Jan 2026 23:34:33 +0200 Subject: [PATCH 4/9] fix: remove suppression --- src/app/components/Account/HeaderAccount/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/app/components/Account/HeaderAccount/index.tsx b/src/app/components/Account/HeaderAccount/index.tsx index 0ef6268eaca..81caf4d8ac1 100644 --- a/src/app/components/Account/HeaderAccount/index.tsx +++ b/src/app/components/Account/HeaderAccount/index.tsx @@ -32,12 +32,7 @@ const HeaderAccount = () => { href={isSignedIn ? accountUrl : signInUrl} css={styles.linkWrapper} > - + {isSignedIn ? 'For you' : 'Sign In'}{' '} From 6744d6f209b60e3bd6456a2b2fbd02aecda146e4 Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Wed, 7 Jan 2026 10:31:34 +0200 Subject: [PATCH 5/9] feat: pass ptrt --- src/app/components/Account/AccountContext.tsx | 24 +++++++++++++------ .../Account/HeaderAccount/index.tsx | 6 +++-- .../Account/hooks/useIdctaConfig.ts | 20 +++++++--------- .../Account/idcta/appendCtaQueryParams.ts | 20 ++++------------ 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/app/components/Account/AccountContext.tsx b/src/app/components/Account/AccountContext.tsx index 1a49c064de5..fd71ec28afa 100644 --- a/src/app/components/Account/AccountContext.tsx +++ b/src/app/components/Account/AccountContext.tsx @@ -1,12 +1,14 @@ -import React, { createContext, useMemo } from 'react'; +import React, { createContext, use, useMemo } from 'react'; +import { ServiceContext } from '#app/contexts/ServiceContext'; import { isSignedIn } from './idcta/isSignedIn'; +import appendCtaQueryParams from './idcta/appendCtaQueryParams'; export type AccountContextProps = { isSignInAvailable: boolean; accountUrl: string; signInUrl: string; registerUrl: string; - isSignedIn: boolean | null; + isSignedIn: boolean; }; export const AccountContext = createContext( @@ -21,17 +23,25 @@ export const AccountProvider = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any initialConfig: any; }) => { + const { locale } = use(ServiceContext); + const signInAvailability = initialConfig.availability.signin === 'GREEN'; const unavailableUrl = initialConfig?.unavailable_url; - console.log({ initialConfig }); - // TODO: Consider using `ptrt` if not working by default const signInUrl = signInAvailability - ? initialConfig?.signin_url + ? appendCtaQueryParams(initialConfig?.signin_url, { + // TEMP: Used for testing. Use window.location.href in production + ptrt: 'https://www.bbc.com/ws/languages', + lang: locale, + }) : unavailableUrl; const registerUrl = signInAvailability - ? initialConfig?.register_url + ? appendCtaQueryParams(initialConfig?.register_url, { + // TEMP: Used for testing. Use window.location.href in production + ptrt: 'https://www.bbc.com/ws/languages', + lang: locale, + }) : unavailableUrl; const accountUrl = @@ -39,7 +49,7 @@ export const AccountProvider = ({ ? initialConfig.foryou_url : unavailableUrl; - const isUserSignedIn = signInAvailability ? isSignedIn() : null; + const isUserSignedIn = signInAvailability ? isSignedIn() : false; const value = useMemo( () => ({ diff --git a/src/app/components/Account/HeaderAccount/index.tsx b/src/app/components/Account/HeaderAccount/index.tsx index 81caf4d8ac1..f3a46704181 100644 --- a/src/app/components/Account/HeaderAccount/index.tsx +++ b/src/app/components/Account/HeaderAccount/index.tsx @@ -1,5 +1,6 @@ import { use } from 'react'; import Text from '#app/components/Text'; +import useHydrationDetection from '#app/hooks/useHydrationDetection'; import { AccountContext } from '../AccountContext'; import styles from './index.styles'; @@ -17,11 +18,12 @@ const AccountIcon = () => { }; const HeaderAccount = () => { + const isHydrated = useHydrationDetection(); + const { isSignedIn, signInUrl, accountUrl, isSignInAvailable } = use(AccountContext); - // TODO: fix hydration error - if (!isSignInAvailable || isSignedIn === null) { + if (!isSignInAvailable || !isHydrated) { return null; } diff --git a/src/app/components/Account/hooks/useIdctaConfig.ts b/src/app/components/Account/hooks/useIdctaConfig.ts index e511d1d04ba..34cf156a334 100644 --- a/src/app/components/Account/hooks/useIdctaConfig.ts +++ b/src/app/components/Account/hooks/useIdctaConfig.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { appendCtaQueryParams } from '../idcta/appendCtaQueryParams'; +import appendCtaQueryParams from '../idcta/appendCtaQueryParams'; import { getIdctaConfigUrl } from '../idcta/getIdctaBaseUrl'; type UseIdctaConfigArgs = { @@ -22,11 +22,11 @@ const initialState: UseIdctaConfigState = { availability: { signin: '', refresh: '' }, }; -// Should be used if config is only needed on the client side +// Can be used if configuration needs to be fetched on the client side. +// Currently not used in this implementation. export default function useIdctaConfig({ ptrt, userOrigin = 'simorgh', - sequenceId, }: UseIdctaConfigArgs = {}) { const [state, setState] = useState(initialState); const [error, setError] = useState(null); @@ -38,7 +38,7 @@ export default function useIdctaConfig({ const controller = new AbortController(); - const run = async () => { + const getIdctaConfig = async () => { try { setError(null); @@ -63,14 +63,10 @@ export default function useIdctaConfig({ setState({ signInUrl: signInHref - ? appendCtaQueryParams(signInHref, { ptrt, userOrigin, sequenceId }) + ? appendCtaQueryParams(signInHref, { ptrt }) : null, registerUrl: registerHref - ? appendCtaQueryParams(registerHref, { - ptrt, - userOrigin, - sequenceId, - }) + ? appendCtaQueryParams(registerHref, { ptrt }) : null, isSignInAvailable: signInAvailable, availability: body?.availability ?? { signin: '', refresh: '' }, @@ -81,10 +77,10 @@ export default function useIdctaConfig({ } }; - run(); + getIdctaConfig(); // eslint-disable-next-line consistent-return return () => controller.abort(); - }, [idctaConfigUrl, ptrt, userOrigin, sequenceId]); + }, [idctaConfigUrl, ptrt, userOrigin]); return { ...state, error }; } diff --git a/src/app/components/Account/idcta/appendCtaQueryParams.ts b/src/app/components/Account/idcta/appendCtaQueryParams.ts index 0bd9d6463be..a2469391d4b 100644 --- a/src/app/components/Account/idcta/appendCtaQueryParams.ts +++ b/src/app/components/Account/idcta/appendCtaQueryParams.ts @@ -1,25 +1,13 @@ type Params = { ptrt?: string; - userOrigin?: string; - sequenceId?: string; + lang?: string; }; -// eslint-disable-next-line import/prefer-default-export -export const appendCtaQueryParams = ( - url: string, - { ptrt, userOrigin, sequenceId }: Params = {}, -): string => { +export default (url: string, { ptrt, lang }: Params = {}): string => { const ctaUrl = new URL(url); - const ptrtQuery = - ptrt || - (typeof window !== 'undefined' && window.location?.href - ? window.location.href - : ''); - - if (ptrtQuery) ctaUrl.searchParams.set('ptrt', ptrtQuery); - if (sequenceId) ctaUrl.searchParams.set('sequenceId', sequenceId); - if (userOrigin) ctaUrl.searchParams.set('userOrigin', userOrigin); + if (ptrt) ctaUrl.searchParams.set('ptrt', ptrt); + if (lang) ctaUrl.searchParams.set('lang', lang); return ctaUrl.toString(); }; From 5796e2bdf9d4d9f001c03c45d059c41a3e1782e7 Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Wed, 7 Jan 2026 11:18:52 +0200 Subject: [PATCH 6/9] feat: Account container --- .../components/Account/AccountContainer.tsx | 26 -------- .../Account/AccountContainer/index.styles.tsx | 43 ++++++++++++ .../Account/AccountContainer/index.tsx | 55 ++++++++++++++++ .../components/Account/idcta/isSignedIn.ts | 2 +- .../components/PromotionalBanner/index.tsx | 65 ++++++++++--------- .../PromotionalBanner/index.types.ts | 10 +-- src/app/legacy/containers/Header/index.jsx | 2 +- 7 files changed, 141 insertions(+), 62 deletions(-) delete mode 100644 src/app/components/Account/AccountContainer.tsx create mode 100644 src/app/components/Account/AccountContainer/index.styles.tsx create mode 100644 src/app/components/Account/AccountContainer/index.tsx diff --git a/src/app/components/Account/AccountContainer.tsx b/src/app/components/Account/AccountContainer.tsx deleted file mode 100644 index 30234640b17..00000000000 --- a/src/app/components/Account/AccountContainer.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { use } from 'react'; -import MessageBanner from '../MessageBanner'; -import { AccountContext } from './AccountContext'; - -export default function AccountContainer() { - const { isSignInAvailable, signInUrl, registerUrl } = use(AccountContext); - - if (!isSignInAvailable || !signInUrl || !registerUrl) { - return null; - } - - return ( -
- - -
- ); -} diff --git a/src/app/components/Account/AccountContainer/index.styles.tsx b/src/app/components/Account/AccountContainer/index.styles.tsx new file mode 100644 index 00000000000..ed4181b002b --- /dev/null +++ b/src/app/components/Account/AccountContainer/index.styles.tsx @@ -0,0 +1,43 @@ +import { Theme, css } from '@emotion/react'; + +export default { + actionLinkWrapper: ({ mq }) => + css({ + display: 'flex', + flexDirection: 'column', + flex: '1 1 auto', + alignItems: 'center', + gap: '1rem', + [mq.GROUP_2_MIN_WIDTH]: { + flexDirection: 'row', + }, + }), + callToActionLink: () => + css({ + padding: '1rem', + }), + + orText: ({ palette }: Theme) => + css({ + color: palette.WHITE, + }), + + signInLnk: ({ palette }: Theme) => + css({ + color: palette.WHITE, + backgroundColor: '#0071F1', + '&:hover, &:focus': { + backgroundColor: '#0051AD', + color: palette.WHITE, + }, + }), + registerLink: ({ palette }: Theme) => + css({ + color: palette.BLACK, + backgroundColor: palette.WHITE, + '&:hover, &:focus': { + backgroundColor: '#F6F6F6', + color: palette.BLACK, + }, + }), +}; diff --git a/src/app/components/Account/AccountContainer/index.tsx b/src/app/components/Account/AccountContainer/index.tsx new file mode 100644 index 00000000000..840c6f3ae58 --- /dev/null +++ b/src/app/components/Account/AccountContainer/index.tsx @@ -0,0 +1,55 @@ +import { use } from 'react'; +import Paragraph from '#app/components/Paragraph'; +import { AccountContext } from '../AccountContext'; +import PromotionalBanner from '../../PromotionalBanner'; +import CallToActionLink from '../../CallToActionLink'; +import styles from './index.styles'; + +export default function AccountContainer() { + const { isSignInAvailable, signInUrl, registerUrl } = use(AccountContext); + + if (!isSignInAvailable || !signInUrl || !registerUrl) { + return null; + } + + return ( +
+ +
+ + + + Sign in + + + + + + + Or + + + + + + Register + + + +
+
+
+ ); +} diff --git a/src/app/components/Account/idcta/isSignedIn.ts b/src/app/components/Account/idcta/isSignedIn.ts index ea81a5ac24d..16a7d6d44d2 100644 --- a/src/app/components/Account/idcta/isSignedIn.ts +++ b/src/app/components/Account/idcta/isSignedIn.ts @@ -2,7 +2,7 @@ import Cookie from 'js-cookie'; export const MARKER_COOKIE_NAME = 'ckns_id'; -// TODO: Might be moved to the AccountContext instead +// TODO: Use cookie name returned by IDCTA export const isSignedIn = () => { if (typeof window === 'undefined') return false; return Cookie.get(MARKER_COOKIE_NAME) !== undefined; diff --git a/src/app/components/PromotionalBanner/index.tsx b/src/app/components/PromotionalBanner/index.tsx index 134f5fd7a4d..9b89b5f3f7d 100644 --- a/src/app/components/PromotionalBanner/index.tsx +++ b/src/app/components/PromotionalBanner/index.tsx @@ -1,3 +1,4 @@ +import { PropsWithChildren } from 'react'; import type { PromotionalBannerProps } from './index.types'; import styles from './index.styles'; import { Close } from '../icons'; @@ -18,8 +19,9 @@ const PromotionalBanner = ({ onSecondaryClick, bannerLabel, closeLabel, + children, id = 'promotional-banner', -}: PromotionalBannerProps) => { +}: PropsWithChildren) => { return (