client/components/mma/cancel/cancellationSaves/SelectReason.tsx (309 lines of code) (raw):
import { css } from '@emotion/react';
import {
from,
palette,
space,
textSans17,
textSansBold17,
} from '@guardian/source/foundations';
import {
Button,
InlineError,
Radio,
RadioGroup,
Stack,
} from '@guardian/source/react-components';
import type { FormEvent } from 'react';
import { useContext, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import {
DATE_FNS_LONG_OUTPUT_FORMAT,
parseDate,
} from '../../../../../shared/dates';
import type {
PaidSubscriptionPlan,
ProductDetail,
} from '../../../../../shared/productResponse';
import {
getMainPlan,
MDA_TEST_USER_HEADER,
} from '../../../../../shared/productResponse';
import {
GROUPED_PRODUCT_TYPES,
type ProductTypeWithCancellationFlow,
} from '../../../../../shared/productTypes';
import {
buttonCentredCss,
stackedButtonLayoutCss,
wideButtonCss,
} from '../../../../styles/ButtonStyles';
import { headingCss, sectionSpacing } from '../../../../styles/GenericStyles';
import { GenericErrorScreen } from '../../../shared/GenericErrorScreen';
import { JsonResponseHandler } from '../../shared/asyncComponents/DefaultApiResponseHandler';
import type {
CancellationContextInterface,
CancellationRouterState,
} from '../CancellationContainer';
import { CancellationContext } from '../CancellationContainer';
import type { CancellationReason } from '../cancellationReason';
const paragraphListCss = css`
${textSans17};
${from.tablet} {
span {
display: block;
}
}
`;
const reasonLegendCss = css`
display: block;
width: 100%;
float: left;
margin-top: ${space[2]}px;
${textSansBold17};
`;
const CancellationInfo = ({
userEmailAddress,
benefitsEndDate,
}: {
userEmailAddress: string;
benefitsEndDate: string;
}) => (
<ul
css={css`
padding-inline-start: 0;
`}
>
<Stack space={1}>
<p css={paragraphListCss} data-qm-masking="blocklist">
We will send a confirmation email to you at {userEmailAddress}.{' '}
<span>
You will have access to all of your benefits until{' '}
{benefitsEndDate}
</span>
</p>
</Stack>
</ul>
);
const ReasonSelection = ({
groupedProductFriendlyName,
cancellationReasons,
setSelectedReasonId,
}: {
groupedProductFriendlyName: string;
cancellationReasons: CancellationReason[];
setSelectedReasonId: React.Dispatch<React.SetStateAction<string>>;
}) => {
return (
<fieldset
onChange={(event: FormEvent<HTMLFieldSetElement>) => {
const target: HTMLInputElement =
event.target as HTMLInputElement;
setSelectedReasonId(target.value);
}}
css={css`
margin: 0 0 ${space[5]}px;
padding: 0;
border: 0;
`}
>
<legend css={reasonLegendCss}>
Why did you cancel your {groupedProductFriendlyName} today?
</legend>
<RadioGroup
name="issue_type"
orientation="vertical"
cssOverrides={css`
display: block;
padding-top: ${space[4]}px;
`}
>
{cancellationReasons.map((reason: CancellationReason) => (
<div
key={reason.reasonId}
css={css`
border: 1px solid ${palette.neutral[86]};
border-radius: 4px;
padding: ${space[1]}px ${space[3]}px;
margin-bottom: ${space[3]}px;
`}
>
<Radio
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;
}
`}
/>
</div>
))}
</RadioGroup>
</fieldset>
);
};
function cancellationCaseFetch(
selectedReasonId: string,
productType: ProductTypeWithCancellationFlow,
productDetail: ProductDetail,
) {
return fetch('/api/case', {
method: 'POST',
body: JSON.stringify({
reason: selectedReasonId,
product: productType.cancellation.sfCaseProduct,
subscriptionName: productDetail.subscription.subscriptionId,
gaData: '',
}),
headers: {
'Content-Type': 'application/json',
[MDA_TEST_USER_HEADER]: `${productDetail.isTestUser}`,
},
});
}
function updateZuoraCancellationReason(
selectedReasonId: string,
productDetail: ProductDetail,
) {
return fetch(
'/api/update-cancellation-reason/' +
productDetail.subscription.subscriptionId,
{
method: 'POST',
body: JSON.stringify({ reason: selectedReasonId }),
headers: {
'Content-Type': 'application/json',
[MDA_TEST_USER_HEADER]: `${productDetail.isTestUser}`,
},
},
);
}
export const SelectReason = () => {
const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [loadingFailed, setLoadingFailed] = useState<boolean>(false);
const [selectedReasonId, setSelectedReasonId] = useState<string>('');
const [inValidationErrorState, setInValidationErrorState] =
useState<boolean>(false);
const { productDetail, productType } = useContext(
CancellationContext,
) as CancellationContextInterface;
const mainPlan = getMainPlan(
productDetail.subscription,
) as PaidSubscriptionPlan;
const location = useLocation();
const routerState = location.state as CancellationRouterState;
const userEmailAddress = routerState?.user?.email ?? '';
const benefitsEndDate = parseDate(
mainPlan.chargedThrough ?? undefined,
).dateStr(DATE_FNS_LONG_OUTPUT_FORMAT);
const navigateToReminder = productType.productType === 'membership';
const submitReason = async () => {
{
const canContinue = !!selectedReasonId.length;
if (canContinue) {
await postReason();
navigate(navigateToReminder ? '../reminder' : '/', {
state: {
selectedReasonId,
},
});
}
setInValidationErrorState(!canContinue);
}
};
const postReason = async () => {
if (isSubmitting) {
return;
}
try {
setIsSubmitting(true);
const response = await Promise.all([
cancellationCaseFetch(
selectedReasonId,
productType,
productDetail,
),
updateZuoraCancellationReason(selectedReasonId, productDetail),
]);
const data = await JsonResponseHandler(response);
if (data === null) {
setIsSubmitting(false);
setLoadingFailed(true);
}
} catch {
setIsSubmitting(false);
setLoadingFailed(true);
}
};
if (loadingFailed) {
return (
<GenericErrorScreen loggingMessage="Cancel journey case id api call failed during the cancellation process" />
);
}
if (!productType.cancellation.reasons) {
return (
<GenericErrorScreen loggingMessage="Got to the cancellation /reasons page with a productType that doesn't have any cancellation reasons." />
);
}
return (
<section css={sectionSpacing}>
<h2 css={headingCss}>
Your{' '}
{
GROUPED_PRODUCT_TYPES[productType.groupedProductType]
.friendlyName
}{' '}
has been cancelled
</h2>
<CancellationInfo
userEmailAddress={userEmailAddress}
benefitsEndDate={benefitsEndDate}
/>
<p
css={css`
${paragraphListCss};
border-top: 1px solid ${palette.neutral[86]};
padding-top: ${space[5]}px;
`}
>
We're always keen to improve, and welcome your feedback.{' '}
<span>
Please take a moment to tell us more about your decision.
</span>
</p>
{!!productType.cancellation.reasons && (
<ReasonSelection
groupedProductFriendlyName={
GROUPED_PRODUCT_TYPES[productType.groupedProductType]
.friendlyName
}
cancellationReasons={productType.cancellation.reasons}
setSelectedReasonId={setSelectedReasonId}
/>
)}
{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>
)}
<section css={stackedButtonLayoutCss}>
<Button
isLoading={isSubmitting}
onClick={() => submitReason()}
cssOverrides={[buttonCentredCss, wideButtonCss]}
>
Submit
</Button>
<Button
priority="tertiary"
cssOverrides={buttonCentredCss}
onClick={() => navigate('/')}
>
Skip
</Button>
</section>
</section>
);
};