Skip to content
43 changes: 43 additions & 0 deletions src/app/components/Account/AccountContainer/index.styles.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
}),
};
55 changes: 55 additions & 0 deletions src/app/components/Account/AccountContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<PromotionalBanner
title="Discover your BBC"
description="Sign in or create an account to watch, listen and join in"
bannerLabel="Discover your BBC"
>
<div css={styles.actionLinkWrapper}>
<CallToActionLink
url={signInUrl}
className="focusIndicatorInvert"
css={[styles.callToActionLink, styles.signInLnk]}
>
<CallToActionLink.ButtonLikeWrapper>
<CallToActionLink.Text shouldUnderlineOnHoverFocus>
Sign in
<CallToActionLink.Chevron />
</CallToActionLink.Text>
</CallToActionLink.ButtonLikeWrapper>
</CallToActionLink>

<Paragraph size="bodyCopy" css={styles.orText}>
Or
</Paragraph>

<CallToActionLink
url={registerUrl}
className="focusIndicatorInvert"
css={[styles.callToActionLink, styles.registerLink]}
>
<CallToActionLink.ButtonLikeWrapper>
<CallToActionLink.Text shouldUnderlineOnHoverFocus>
Register
</CallToActionLink.Text>
</CallToActionLink.ButtonLikeWrapper>
</CallToActionLink>
</div>
</PromotionalBanner>
</div>
);
}
66 changes: 66 additions & 0 deletions src/app/components/Account/AccountContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { createContext, use, useEffect, useMemo, useState } 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;
};

export const AccountContext = createContext<AccountContextProps>(
{} as AccountContextProps,
);

export const AccountProvider = ({
children,
initialConfig = null,
}: {
children: React.ReactNode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialConfig: any;
}) => {
const { locale } = use(ServiceContext);

const [ptrt, setPtrt] = useState<string | undefined>(undefined);

useEffect(() => {
setPtrt(window.location.href);
}, []);

const signInAvailability = initialConfig.availability.signin === 'GREEN';
const unavailableUrl = initialConfig?.unavailable_url;

const signInUrl = signInAvailability
? appendCtaQueryParams(initialConfig?.signin_url, { ptrt, lang: locale })
: unavailableUrl;

const registerUrl = signInAvailability
? appendCtaQueryParams(initialConfig?.register_url, { ptrt, lang: locale })
: unavailableUrl;

const accountUrl =
initialConfig?.['foryou-flagpole'] === 'GREEN'
? initialConfig.foryou_url
: unavailableUrl;

const isUserSignedIn = signInAvailability ? isSignedIn() : false;

const value = useMemo(
() => ({
isSignInAvailable: signInAvailability,
signInUrl,
registerUrl,
accountUrl,
isSignedIn: isUserSignedIn,
}),
[accountUrl, isUserSignedIn, registerUrl, signInAvailability, signInUrl],
);

return (
<AccountContext.Provider value={value}>{children}</AccountContext.Provider>
);
};
33 changes: 33 additions & 0 deletions src/app/components/Account/HeaderAccount/index.styles.tsx
Original file line number Diff line number Diff line change
@@ -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',
},
}),
};
46 changes: 46 additions & 0 deletions src/app/components/Account/HeaderAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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';

const AccountIcon = () => {
return (
<svg
viewBox="0 0 32 32"
focusable="false"
aria-hidden="true"
css={styles.icon}
>
<path d="M16 23c-3.3 0-5.8-2.5-5.8-5.8s2.5-5.8 5.8-5.8c3.2 0 5.8 2.5 5.8 5.8S19.2 23 16 23m15-5.7c0-8.4-6.6-15-15-15-8.5 0-15 6.6-15 15 0 5.2 2.5 9.8 6.4 12.4 1.9-2.8 5-4.6 8.6-4.6s6.7 1.8 8.6 4.6C28.5 27 31 22.5 31 17.3" />
</svg>
);
};

const HeaderAccount = () => {
const isHydrated = useHydrationDetection();

const { isSignedIn, signInUrl, accountUrl, isSignInAvailable } =
use(AccountContext);

if (!isSignInAvailable || !isHydrated) {
return null;
}

return (
<div>
<Text
as="a"
href={isSignedIn ? accountUrl : signInUrl}
css={styles.linkWrapper}
>
<Text as="span" css={styles.linkText} size="pica">
{isSignedIn ? 'For you' : 'Sign In'}{' '}
</Text>
<AccountIcon />
</Text>
</div>
);
};

export default HeaderAccount;
86 changes: 86 additions & 0 deletions src/app/components/Account/hooks/useIdctaConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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: '' },
};

// 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',
}: UseIdctaConfigArgs = {}) {
const [state, setState] = useState<UseIdctaConfigState>(initialState);
const [error, setError] = useState<Error | null>(null);

const idctaConfigUrl = getIdctaConfigUrl();

useEffect(() => {
if (!idctaConfigUrl) return;

const controller = new AbortController();

const getIdctaConfig = 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 })
: null,
registerUrl: registerHref
? appendCtaQueryParams(registerHref, { ptrt })
: null,
isSignInAvailable: signInAvailable,
availability: body?.availability ?? { signin: '', refresh: '' },
});
} catch (e) {
if ((e as Error)?.name === 'AbortError') return;
setError(e as Error);
}
};

getIdctaConfig();
// eslint-disable-next-line consistent-return
return () => controller.abort();
}, [idctaConfigUrl, ptrt, userOrigin]);

return { ...state, error };
}
13 changes: 13 additions & 0 deletions src/app/components/Account/idcta/appendCtaQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Params = {
ptrt?: string;
lang?: string;
};

export default (url: string, { ptrt, lang }: Params = {}): string => {
const ctaUrl = new URL(url);

if (ptrt) ctaUrl.searchParams.set('ptrt', ptrt);
if (lang) ctaUrl.searchParams.set('lang', lang);

return ctaUrl.toString();
};
36 changes: 36 additions & 0 deletions src/app/components/Account/idcta/fetchIdctaConfig.ts
Original file line number Diff line number Diff line change
@@ -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<IdctaConfigFetchResult> {
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,
};
}
}
10 changes: 10 additions & 0 deletions src/app/components/Account/idcta/getIdctaBaseUrl.ts
Original file line number Diff line number Diff line change
@@ -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`;
Loading
Loading