client/components/mma/cancel/CancellationReasonSelection.tsx (371 lines of code) (raw):

import { css } from '@emotion/react'; import { from, palette, space, textSansBold17, until, } from '@guardian/source/foundations'; import { Button, InlineError, Radio, RadioGroup, SvgArrowRightStraight, } from '@guardian/source/react-components'; import type { FormEvent } from 'react'; import { useContext, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { featureSwitches } from '@/shared/featureSwitches'; import { DATE_FNS_LONG_OUTPUT_FORMAT, parseDate, } from '../../../../shared/dates'; import type { ProductDetail } from '../../../../shared/productResponse'; import type { ProductTypeWithCancellationFlow } from '../../../../shared/productTypes'; import { LoadingState, useAsyncLoader, } from '../../../utilities/hooks/useAsyncLoader'; import { GenericErrorScreen } from '../../shared/GenericErrorScreen'; import { WithStandardTopMargin } from '../../shared/WithStandardTopMargin'; import { JsonResponseHandler } from '../shared/asyncComponents/DefaultApiResponseHandler'; import { DefaultLoadingView } from '../shared/asyncComponents/DefaultLoadingView'; import { ProgressIndicator } from '../shared/ProgressIndicator'; import { ProgressStepper } from '../shared/ProgressStepper'; import type { CancellationContextInterface } from './CancellationContainer'; import { CancellationContext } from './CancellationContainer'; import { cancellationEffectiveEndOfLastInvoicePeriod, cancellationEffectiveToday, } from './cancellationContexts'; import type { CancellationDateResponse } from './cancellationDateResponse'; import { cancellationDateFetcher } from './cancellationDateResponse'; import type { CancellationReason } from './cancellationReason'; interface ReasonPickerProps { productType: ProductTypeWithCancellationFlow; productDetail: ProductDetail; chargedThroughDateStr?: string; } const ReasonPicker = ({ productType, productDetail, chargedThroughDateStr, }: ReasonPickerProps) => { const [selectedReasonId, setSelectedReasonId] = useState<string>(''); const [cancellationPolicy, setCancellationPolicy] = useState<string>(''); const [inValidationErrorState, setInValidationErrorState] = useState<boolean>(false); const navigate = useNavigate(); const location = useLocation(); const routerState = location.state as { productDetail: ProductDetail; productType: ProductTypeWithCancellationFlow; }; const shouldUseProgressStepper = (featureSwitches.supporterplusCancellationOffer && productType.productType === 'supporterplus') || (featureSwitches.contributionCancellationPause && productType.productType === 'contributions'); if (!productType.cancellation.reasons) { return ( <GenericErrorScreen loggingMessage="Got to the cancellation reasons selection page with a productType that doesn't have any cancellation reasons." /> ); } return ( <> {shouldUseProgressStepper ? ( <ProgressStepper steps={[{ isCurrentStep: true }, {}, {}, {}]} additionalCSS={css` margin: ${space[5]}px 0 ${space[12]}px; `} /> ) : ( <ProgressIndicator steps={[ { title: 'Reason', isCurrentStep: true }, { title: 'Review' }, { title: 'Confirmation' }, ]} additionalCSS={css` margin: ${space[5]}px 0 ${space[12]}px; `} /> )} {productType.cancellation.startPageBody(productDetail)} <WithStandardTopMargin> <fieldset onChange={(event: FormEvent<HTMLFieldSetElement>) => { const target: HTMLInputElement = event.target as HTMLInputElement; setSelectedReasonId(target.value); }} css={css` border: 1px solid ${palette.neutral[86]}; margin: 0 0 ${space[5]}px; padding: 0; `} > <legend css={css` display: block; width: 100%; margin: 0; padding: ${space[3]}px; float: left; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${textSansBold17} ${from.tablet} { padding: ${space[3]}px ${space[5]}px; } `} > Please select a reason </legend> <RadioGroup name="issue_type" orientation="vertical" cssOverrides={css` display: block; padding: ${space[5]}px; `} > {productType.cancellation.reasons.map( (reason: CancellationReason) => ( <Radio key={reason.reasonId} name="cancellation-reason" value={reason.reasonId} label={reason.linkLabel} cssOverrides={css` vertical-align: top; text-transform: lowercase; :checked + div label:first-of-type { font-weight: bold; } `} /> ), )} </RadioGroup> </fieldset> {inValidationErrorState && !selectedReasonId.length && ( <InlineError cssOverrides={css` padding: ${space[5]}px; margin-bottom: ${space[4]}px; border: 4px solid ${palette.error[400]}; text-align: left; `} > Please select a reason </InlineError> )} {chargedThroughDateStr && ( <> <fieldset onChange={( event: FormEvent<HTMLFieldSetElement>, ) => { const target: HTMLInputElement = event.target as HTMLInputElement; if (target.value === 'EndOfLastInvoicePeriod') { setCancellationPolicy( cancellationEffectiveEndOfLastInvoicePeriod, ); } else { setCancellationPolicy( cancellationEffectiveToday, ); } }} css={css` border: 1px solid ${palette.neutral[86]}; margin: 0 0 ${space[5]}px; padding: 0; `} > <legend css={css` display: block; width: 100%; margin: 0; padding: ${space[3]}px; float: left; background-color: ${palette.neutral[97]}; border-bottom: 1px solid ${palette.neutral[86]}; ${textSansBold17} ${from.tablet} { padding: ${space[3]}px ${space[5]}px; } `} > When would you like your cancellation to become effective? </legend> <RadioGroup name="issue_type" orientation="vertical" cssOverrides={css` display: block; padding: ${space[5]}px; `} > <Radio name="effective-date" value="EndOfLastInvoicePeriod" label={`On ${chargedThroughDateStr}, which is the end of your current billing period (you should not be charged again)`} /> <Radio name="effective-date" value="Today" label="Today" /> </RadioGroup> </fieldset> {inValidationErrorState && !cancellationPolicy.length && ( <InlineError cssOverrides={css` padding: ${space[5]}px; margin-bottom: ${space[4]}px; border: 4px solid ${palette.error[400]}; text-align: left; `} > Please select When would you like your cancellation to become effective? </InlineError> )} </> )} <div data-cy="cta_container" css={{ display: 'flex', justifyContent: 'space-between', flexDirection: 'row-reverse', [until.mobileLandscape]: { flexDirection: 'column', }, }} > <div css={{ textAlign: 'right', marginBottom: '10px', }} > <Button icon={<SvgArrowRightStraight />} iconSide="right" onClick={() => { const canContinue = !!selectedReasonId.length && (chargedThroughDateStr ? !!cancellationPolicy.length : true); if (canContinue) { navigate('review', { state: { ...routerState, selectedReasonId, cancellationPolicy, }, }); } setInValidationErrorState(!canContinue); }} > Continue </Button> </div> <div> <Button priority="tertiary" onClick={() => { navigate('/'); }} > Return to your account </Button> </div> </div> </WithStandardTopMargin> </> ); }; interface ReasonPickerWithCancellationDateProps { productType: ProductTypeWithCancellationFlow; productDetail: ProductDetail; } function getChargedThroughDateStr( cancellationDateResponse: CancellationDateResponse, ) { if ( cancellationDateResponse.cancellationEffectiveDate === 'now' || cancellationDateResponse.cancellationEffectiveDate === undefined || cancellationDateResponse.cancellationEffectiveDate === null ) { return undefined; } return parseDate( cancellationDateResponse.cancellationEffectiveDate, ).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT); } const ReasonPickerWithCancellationDate = ({ productType, productDetail, }: ReasonPickerWithCancellationDateProps) => { const { data: cancellationDateResponse, loadingState, }: { data: CancellationDateResponse | null; loadingState: LoadingState; } = useAsyncLoader( cancellationDateFetcher(productDetail.subscription.subscriptionId), JsonResponseHandler, ); if (loadingState == LoadingState.HasError) { return <GenericErrorScreen />; } if (loadingState == LoadingState.IsLoading) { return ( <DefaultLoadingView loadingMessage={`Checking your ${ productType.shortFriendlyName || productType.friendlyName } details...`} /> ); } if (cancellationDateResponse === null) { return <GenericErrorScreen />; } const chargedThroughDateStr = getChargedThroughDateStr( cancellationDateResponse, ); return ( <ReasonPicker productType={productType} productDetail={productDetail} chargedThroughDateStr={chargedThroughDateStr} /> ); }; export const CancellationReasonSelection = () => { const { productDetail, productType } = useContext( CancellationContext, ) as CancellationContextInterface; if (productType.cancellation.startPageOfferEffectiveDateOptions) { return ( <ReasonPickerWithCancellationDate productType={productType} productDetail={productDetail} /> ); } return ( <ReasonPicker productType={productType} productDetail={productDetail} /> ); };