client/components/mma/MMAPage.tsx (802 lines of code) (raw):

import { css, Global } from '@emotion/react'; import { ABProvider, useAB } from '@guardian/ab-react'; import { breakpoints, from, space } from '@guardian/source/foundations'; import type { ReactNode } from 'react'; import { lazy, Suspense, useEffect, useState } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { fonts } from '@/client/styles/fonts'; import { featureSwitches, initFeatureSwitchUrlParamOverride, } from '../../../shared/featureSwitches'; import type { ProductType, ProductTypeWithDeliveryRecordsProperties, ProductTypeWithHolidayStopsFlow, } from '../../../shared/productTypes'; import { PRODUCT_TYPES } from '../../../shared/productTypes'; import { abSwitches } from '../../experiments/abSwitches'; import { tests } from '../../experiments/abTests'; import { global } from '../../styles/global'; import { getCookie } from '../../utilities/cookies'; import { useAnalytics } from '../../utilities/hooks/useAnalytics'; import { useConsent } from '../../utilities/hooks/useConsent'; import { useScrollToTop } from '../../utilities/hooks/useScrollToTop'; import { hasDeliveryFlow, hasDeliveryRecordsFlow, shouldHaveHolidayStopsFlow, } from '../../utilities/productUtils'; import type { SignInStatus } from '../../utilities/signInStatus'; import { isSignedIn } from '../../utilities/signInStatus'; import { ErrorBoundary } from '../shared/ErrorBoundary'; import { GenericErrorScreen } from '../shared/GenericErrorScreen'; import { Main } from '../shared/Main'; import { DeliveryAddressUpdate } from './delivery/address/DeliveryAddressForm'; import { Maintenance } from './maintenance/Maintenance'; import { MMAPageSkeleton } from './MMAPageSkeleton'; import { SignInError } from './signInError/SignInError'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Ophan events are diverse (and unguessable?) const record = (event: any) => { if (window.guardian?.ophan?.record) { window.guardian.ophan.record(event); } }; initFeatureSwitchUrlParamOverride(); // The code below uses magic comments to instruct Webpack on // how to name the chunks these dynamic imports produce // More information: https://webpack.js.org/api/module-methods/#magic-comments const AccountOverview = lazy(() => import( /* webpackChunkName: "AccountOverview" */ './accountoverview/AccountOverview' ).then(({ AccountOverview }) => ({ default: AccountOverview })), ); const Billing = lazy(() => import(/* webpackChunkName: "Billing" */ './billing/Billing').then( ({ Billing }) => ({ default: Billing }), ), ); const DataPrivacy = lazy(() => import( /* webpackChunkName: "DataPrivacy" */ './dataPrivacy/DataPrivacy' ).then(({ DataPrivacy }) => ({ default: DataPrivacy })), ); const ManageProduct = lazy(() => import( /* webpackChunkName: "ManageProduct" */ './accountoverview/ManageProduct' ).then(({ ManageProduct }) => ({ default: ManageProduct })), ); const ManageProductV2 = lazy(() => import( /* webpackChunkName: "ManageProduct" */ './accountoverview/manageProducts/ManageProductV2' ).then(({ ManageProductV2 }) => ({ default: ManageProductV2 })), ); const CancellationContainer = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/CancellationContainer' ).then(({ CancellationContainer }) => ({ default: CancellationContainer })), ); const CancellationJourneyFunnel = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/CancellationJourneyFunnel' ).then(({ CancellationJourneyFunnel }) => ({ default: CancellationJourneyFunnel, })), ); const CancellationReasonReview = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/CancellationReasonReview' ).then(({ CancellationReasonReview }) => ({ default: CancellationReasonReview, })), ); const SavedCancellation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/stages/SavedCancellation' ).then(({ SavedCancellation }) => ({ default: SavedCancellation })), ); const ConfirmCancellation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/stages/ConfirmCancellation' ).then(({ ConfirmCancellation }) => ({ default: ConfirmCancellation })), ); const ExecuteCancellation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/stages/ExecuteCancellation' ).then(({ ExecuteCancellation }) => ({ default: ExecuteCancellation })), ); const MembershipCancellationLanding = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/CancellationLanding' ).then(({ CancellationLanding: MembershipCancellationLanding }) => ({ default: MembershipCancellationLanding, })), ); const ValueOfSupport = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/ValueOfSupport' ).then(({ ValueOfSupport }) => ({ default: ValueOfSupport, })), ); const CancelAlternativeOffer = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/CancelAlternativeOffer' ).then(({ CancelAlternativeOffer }) => ({ default: CancelAlternativeOffer, })), ); const CancelAlternativeReview = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/CancelAlternativeReview' ).then(({ CancelAlternativeReview }) => ({ default: CancelAlternativeReview, })), ); const CancelAlternativeConfirmed = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/CancelAlternativeConfirmed' ).then(({ CancelAlternativeConfirmed }) => ({ default: CancelAlternativeConfirmed, })), ); const SaveOptions = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/SaveOptions' ).then(({ SaveOptions }) => ({ default: SaveOptions, })), ); const MembershipSwitch = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/MembershipSwitch' ).then(({ MembershipSwitch }) => ({ default: MembershipSwitch, })), ); const SelectReason = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/SelectReason' ).then(({ SelectReason }) => ({ default: SelectReason, })), ); const ContinueMembershipConfirmation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/ContinueMembershipConfirmation' ).then(({ ContinueMembershipConfirmation }) => ({ default: ContinueMembershipConfirmation, })), ); const ConfirmMembershipCancellation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/ConfirmMembershipCancellation' ).then(({ ConfirmMembershipCancellation }) => ({ default: ConfirmMembershipCancellation, })), ); const SwitchThankYou = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/membership/SwitchThankYou' ).then(({ SwitchThankYou }) => ({ default: SwitchThankYou, })), ); const ConfirmDigiSubCancellation = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/digipack/ConfirmDigiSubCancellation' ).then(({ ConfirmDigiSubCancellation: ConfirmDigiSubCancellation }) => ({ default: ConfirmDigiSubCancellation, })), ); const DigiSubThankYouOffer = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/digipack/DigiSubThankYouOffer' ).then(({ DigiSubThankYouOffer: DigiSubThankYouOffer }) => ({ default: DigiSubThankYouOffer, })), ); const DigiSubDiscountConfirmed = lazy(() => import( /* webpackChunkName: "Cancellation" */ './cancel/cancellationSaves/digipack/DigiSubDiscountConfirmed' ).then(({ DigiSubDiscountConfirmed: DigiSubDiscountConfirmed }) => ({ default: DigiSubDiscountConfirmed, })), ); const PaymentDetailUpdateContainer = lazy(() => import( /* webpackChunkName: "PaymentDetailUpdate" */ './paymentUpdate/PaymentDetailUpdateContainer' ).then(({ PaymentDetailUpdateContainer }) => ({ default: PaymentDetailUpdateContainer, })), ); const PaymentDetailUpdate = lazy(() => import( /* webpackChunkName: "PaymentDetailUpdate" */ './paymentUpdate/PaymentDetailUpdate' ).then(({ PaymentDetailUpdate }) => ({ default: PaymentDetailUpdate })), ); const PaymentDetailUpdateCheckoutSessionReturn = lazy(() => import( /* webpackChunkName: "PaymentDetailUpdate" */ './paymentUpdate/PaymentDetailUpdateCheckoutSessionReturn' ).then(({ PaymentDetailUpdateCheckoutSessionReturn }) => ({ default: PaymentDetailUpdateCheckoutSessionReturn, })), ); const PaymentDetailUpdateConfirmation = lazy(() => import( /* webpackChunkName: "PaymentDetailUpdate" */ './paymentUpdate/PaymentDetailUpdateConfirmation' ).then(({ PaymentDetailUpdateConfirmation }) => ({ default: PaymentDetailUpdateConfirmation, })), ); const PaymentFailed = lazy(() => import( /* webpackChunkName: "PaymentDetailUpdate" */ './paymentUpdate/PaymentFailed' ).then(({ PaymentFailed }) => ({ default: PaymentFailed })), ); const HolidayStopsContainer = lazy(() => import( /* webpackChunkName: "HolidayStops" */ './holiday/HolidayStopsContainer' ).then(({ HolidayStopsContainer }) => ({ default: HolidayStopsContainer })), ); const HolidaysOverview = lazy(() => import( /* webpackChunkName: "HolidayStops" */ './holiday/HolidaysOverview' ).then(({ HolidaysOverview }) => ({ default: HolidaysOverview })), ); const HolidayDateChooser = lazy(() => import( /* webpackChunkName: "HolidayStops" */ './holiday/HolidayDateChooser' ).then(({ HolidayDateChooser }) => ({ default: HolidayDateChooser })), ); const HolidayReview = lazy(() => import( /* webpackChunkName: "HolidayStops" */ './holiday/HolidayReview' ).then(({ HolidayReview }) => ({ default: HolidayReview })), ); const HolidayConfirmed = lazy(() => import( /* webpackChunkName: "HolidayStops" */ './holiday/HolidayConfirmed' ).then(({ HolidayConfirmed }) => ({ default: HolidayConfirmed })), ); const DeliveryAddressChangeContainer = lazy(() => import( /* webpackChunkName: "DeliveryAddress" */ './delivery/address/DeliveryAddressChangeContainer' ).then(({ DeliveryAddressChangeContainer }) => ({ default: DeliveryAddressChangeContainer, })), ); const DeliveryAddressReview = lazy(() => import( /* webpackChunkName: "DeliveryAddress" */ './delivery/address/DeliveryAddressReview' ).then(({ DeliveryAddressReview }) => ({ default: DeliveryAddressReview })), ); const DeliveryAddressConfirmation = lazy(() => import( /* webpackChunkName: "DeliveryAddress" */ './delivery/address/DeliveryAddressConfirmation' ).then(({ DeliveryAddressConfirmation }) => ({ default: DeliveryAddressConfirmation, })), ); const DeliveryRecordsContainer = lazy(() => import( /* webpackChunkName: "DeliveryRecords" */ './delivery/records/DeliveryRecordsContainer' ).then(({ DeliveryRecordsContainer }) => ({ default: DeliveryRecordsContainer, })), ); const DeliveryRecords = lazy(() => import( /* webpackChunkName: "DeliveryRecords" */ './delivery/records/DeliveryRecords' ).then(({ DeliveryRecords }) => ({ default: DeliveryRecords })), ); const DeliveryRecordsProblemReview = lazy(() => import( /* webpackChunkName: "DeliveryRecords" */ './delivery/records/DeliveryRecordsProblemReview' ).then(({ DeliveryRecordsProblemReview }) => ({ default: DeliveryRecordsProblemReview, })), ); const DeliveryRecordsProblemConfirmation = lazy(() => import( /* webpackChunkName: "DeliveryRecords" */ './delivery/records/DeliveryRecordsProblemConfirmation' ).then(({ DeliveryRecordsProblemConfirmation }) => ({ default: DeliveryRecordsProblemConfirmation, })), ); const SwitchContainer = lazy(() => import(/* webpackChunkName: "Switch" */ './switch/SwitchContainer').then( ({ SwitchContainer }) => ({ default: SwitchContainer, }), ), ); const SwitchOptions = lazy(() => import( /* webpackChunkName: "Switch" */ './switch/options/SwitchOptions' ).then(({ SwitchOptions }) => ({ default: SwitchOptions, })), ); const SwitchReview = lazy(() => import( /* webpackChunkName: "Switch" */ './switch/review/SwitchReview' ).then(({ SwitchReview }) => ({ default: SwitchReview, })), ); const UpgradeSupportContainer = lazy(() => import( /* webpackChunkName: "UpgradeSupport" */ './upgrade/UpgradeSupportContainer' ).then(({ UpgradeSupportContainer }) => ({ default: UpgradeSupportContainer, })), ); const UpgradeSupport = lazy(() => import( /* webpackChunkName: "UpgradeSupport" */ './upgrade/UpgradeSupport' ).then(({ UpgradeSupport }) => ({ default: UpgradeSupport, })), ); const UpgradeSupportThankYou = lazy(() => import( /* webpackChunkName: "UpgradeSupport" */ './upgrade/UpgradeSupportThankYou' ).then(({ UpgradeSupportThankYou }) => ({ default: UpgradeSupportThankYou, })), ); const UpgradeSupportSwitchThankYou = lazy(() => import( /* webpackChunkName: "UpgradeSupport" */ './upgrade/UpgradeSupportSwitchThankYou' ).then(({ UpgradeSupportSwitchThankYou }) => ({ default: UpgradeSupportSwitchThankYou, })), ); const SwitchComplete = lazy(() => import( /* webpackChunkName: "Switch" */ './switch/complete/SwitchComplete' ).then(({ SwitchComplete }) => ({ default: SwitchComplete, })), ); const EmailAndMarketing = lazy(() => import( /* webpackChunkName: "EmailAndMarketing" */ './identity/emailAndMarketing/EmailAndMarketing' ).then(({ EmailAndMarketing }) => ({ default: EmailAndMarketing })), ); const PublicProfile = lazy(() => import( /* webpackChunkName: "PublicProfile" */ './identity/publicProfile/PublicProfile' ).then(({ PublicProfile }) => ({ default: PublicProfile })), ); const Settings = lazy(() => import( /* webpackChunkName: "Settings" */ './identity/settings/Settings' ).then(({ Settings }) => ({ default: Settings })), ); const Help = lazy(() => import(/* webpackChunkName: "Help" */ './help/Help').then(({ Help }) => ({ default: Help, })), ); const CancelReminders = lazy(() => import( /* webpackChunkName: "CancelReminders" */ './cancelReminders/CancelReminders' ).then(({ CancelReminders }) => ({ default: CancelReminders })), ); const CreateReminder = lazy(() => import( /* webpackChunkName: "CreateReminder" */ './reminders/CreateReminder' ).then(({ CreateReminder }) => ({ default: CreateReminder })), ); const GenericErrorContainer = (props: { children: ReactNode }) => ( <section css={css` padding: 0 ${space[3]}px; ${from.tablet} { padding-left: ${space[5]}px; padding-right: ${space[5]}px; } `} > <div css={css` margin: ${space[12]}px auto; max-width: ${breakpoints.wide}px; `} > {props.children} </div> </section> ); const MMARouter = () => { const [signInStatus, setSignInStatus] = useState<SignInStatus>('init'); const ABTestAPI = useAB(); useEffect(() => { setSignInStatus(isSignedIn() ? 'signedIn' : 'signedOut'); const allRunnableTests = ABTestAPI.allRunnableTests(tests); ABTestAPI.registerImpressionEvents(allRunnableTests); ABTestAPI.registerCompleteEvents(allRunnableTests); }, [ABTestAPI]); useAnalytics(); useConsent(); useScrollToTop(); return ( <Main signInStatus={signInStatus}> <Global styles={css(`${global}`)} /> <Global styles={css(`${fonts}`)} /> <Suspense fallback={<MMAPageSkeleton />}> <ErrorBoundary fallback={(error) => ( <GenericErrorContainer> <GenericErrorScreen loggingMessage={error} /> </GenericErrorContainer> )} > <Routes> <Route path="/" element={<AccountOverview />} /> <Route path="/app" element={<AccountOverview isFromApp />} /> <Route path="/billing" element={<Billing />} /> <Route path="/data-privacy" element={<DataPrivacy />} /> <Route path="/email-prefs" element={<EmailAndMarketing />} /> <Route path="/public-settings" element={<PublicProfile />} /> <Route path="/account-settings" element={<Settings />} /> <Route path="upgrade-support" element={<UpgradeSupportContainer />} > <Route index element={<UpgradeSupport />} /> <Route path={'switch-thank-you'} element={<UpgradeSupportSwitchThankYou />} /> <Route path={'thank-you'} element={<UpgradeSupportThankYou />} /> </Route> {[ { path: '/switch', fromApp: false }, { path: '/app/switch', fromApp: true }, ].map(({ path, fromApp }) => ( <Route key={path} path={path} element={ <SwitchContainer isFromApp={fromApp} /> } > <Route index element={<SwitchOptions />} /> <Route path="review" element={<SwitchReview />} /> <Route path="complete" element={<SwitchComplete />} /> </Route> ))} {Object.values(PRODUCT_TYPES).map( (productType: ProductType) => featureSwitches.digisubSave && productType.productType === 'digipack' ? ( <Route key={productType.urlPart} path={`/${productType.urlPart}`} element={ <ManageProductV2 productType={productType} /> } /> ) : ( <Route key={productType.urlPart} path={`/${productType.urlPart}`} element={ <ManageProduct productType={productType} /> } /> ), )} {Object.values(PRODUCT_TYPES) .filter(hasDeliveryFlow) .map((productType: ProductType) => ( <Route key={productType.urlPart} path={`/delivery/${productType.urlPart}/address`} element={ <DeliveryAddressChangeContainer productType={productType} /> } > <Route index element={ <DeliveryAddressUpdate productType={productType} /> } /> <Route path="review" element={ <DeliveryAddressReview productType={productType} /> } /> <Route path="confirmed" element={ <DeliveryAddressConfirmation productType={productType} /> } /> </Route> ))} {Object.values(PRODUCT_TYPES).map( (productType: ProductType) => ( <Route key={productType.urlPart} path={`/payment/${productType.urlPart}`} element={ <PaymentDetailUpdateContainer productType={productType} /> } > <Route index element={ <PaymentDetailUpdate productType={productType} /> } /> <Route path="checkout-session-return" element={ <PaymentDetailUpdateCheckoutSessionReturn /> } /> <Route path="updated" element={ <PaymentDetailUpdateConfirmation /> } /> <Route path="failed" element={<PaymentFailed />} /> </Route> ), )} {Object.values(PRODUCT_TYPES) .filter(hasDeliveryRecordsFlow) .map( ( productType: ProductTypeWithDeliveryRecordsProperties, ) => ( <Route key={productType.urlPart} path={`/delivery/${productType.urlPart}/records`} element={ <DeliveryRecordsContainer productType={productType} /> } > <Route index element={<DeliveryRecords />} /> <Route path="review" element={ <DeliveryRecordsProblemReview /> } /> <Route path="confirmed" element={ <DeliveryRecordsProblemConfirmation /> } /> </Route> ), )} {Object.values(PRODUCT_TYPES).map( (productType: ProductType) => ( <Route key={productType.urlPart} path={'/cancel/' + productType.urlPart} element={ <CancellationContainer productType={productType} /> } > <Route index element={<CancellationJourneyFunnel />} /> <Route path="review" element={<CancellationReasonReview />} /> <Route path="saved" element={<SavedCancellation />} /> <Route path="confirmed" element={<ExecuteCancellation />} /> <Route path="landing" element={ <MembershipCancellationLanding /> } /> <Route path="details" element={<ValueOfSupport />} /> <Route path="offer" element={<CancelAlternativeOffer />} /> <Route path="pause" element={<CancelAlternativeOffer />} /> <Route path="pause-review" element={<CancelAlternativeReview />} /> <Route path="offer-review" element={<CancelAlternativeReview />} /> <Route path="offer-confirmed" element={<CancelAlternativeConfirmed />} /> <Route path="pause-confirmed" element={<CancelAlternativeConfirmed />} /> <Route path="offers" element={<SaveOptions />} /> <Route path="reasons" element={<SelectReason />} /> <Route path="switch-offer" element={<MembershipSwitch />} /> <Route path="thank-you" element={ <ContinueMembershipConfirmation /> } /> <Route path="confirm" element={ productType.urlPart === 'membership' ? ( <ConfirmMembershipCancellation /> ) : ( <ConfirmCancellation /> ) } /> <Route path="switch-thank-you" element={<SwitchThankYou />} /> <Route path="confirm-cancel" element={<ConfirmDigiSubCancellation />} /> <Route path="discount-offer" element={<DigiSubThankYouOffer />} /> <Route path="discount-confirmed" element={<DigiSubDiscountConfirmed />} /> </Route> ), )} {Object.values(PRODUCT_TYPES) .filter(shouldHaveHolidayStopsFlow) .map( ( productType: ProductTypeWithHolidayStopsFlow, ) => ( <Route key={productType.urlPart} path={'/suspend/' + productType.urlPart} element={ <HolidayStopsContainer productType={productType} /> } > <Route index element={<HolidaysOverview />} /> <Route path="create" element={<HolidayDateChooser />} /> <Route path="amend" element={ <HolidayDateChooser isAmendJourney /> } /> <Route path="review" element={<HolidayReview />} /> <Route path="confirmed" element={<HolidayConfirmed />} /> </Route> ), )} <Route path="/help" element={<Help />} /> {/*Does not require sign in*/} <Route path="/cancel-reminders/:reminderCode" element={<CancelReminders />} /> {/*Does not require sign in*/} <Route path="/create-reminder/one-off" element={ <CreateReminder reminderType={'ONE_OFF'} /> } /> {/*Does not require sign in*/} <Route path="/create-reminder/recurring" element={ <CreateReminder reminderType={'RECURRING'} /> } /> {/*Does not require sign in*/} <Route path="/maintenance" element={<Maintenance />} /> {/*Does not require sign in*/} <Route path="/sign-in-error" element={<SignInError />} /> <Route path="*" element={<Navigate to="/" />} /> </Routes> </ErrorBoundary> </Suspense> </Main> ); }; const getMvtId = (): number => { const mvtId = getCookie('GU_mvt_id'); return mvtId ? parseInt(mvtId) : 0; }; export const MMAPage = ( <ABProvider arrayOfTestObjects={tests} abTestSwitches={abSwitches} pageIsSensitive={false} mvtMaxValue={1000000} mvtId={getMvtId()} ophanRecord={record} > <BrowserRouter> <MMARouter /> </BrowserRouter> </ABProvider> );