client/components/mma/cancel/contributions/ContributionsCancellationFlowFinancialSaveAttempt.tsx (223 lines of code) (raw):
import { css } from '@emotion/react';
import { from, space } from '@guardian/source/foundations';
import { Button, LinkButton, Spinner } from '@guardian/source/react-components';
import * as Sentry from '@sentry/browser';
import { useContext, useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview';
import { fetchWithDefaultParameters } from '@/client/utilities/fetch';
import { cancelAlternativeUrlPartLookup } from '@/shared/cancellationUtilsAndTypes';
import { featureSwitches } from '@/shared/featureSwitches';
import type { TrueFalsePending } from '@/shared/generalTypes';
import {
getMainPlan,
isPaidSubscriptionPlan,
} from '../../../../../shared/productResponse';
import { PRODUCT_TYPES } from '../../../../../shared/productTypes';
import { trackEventInOphanOnly } from '../../../../utilities/analytics';
import { ContributionUpdateAmountForm } from '../../accountoverview/updateAmount/ContributionUpdateAmountForm';
import { GenericErrorMessage } from '../../identity/GenericErrorMessage';
import type {
CancellationContextInterface,
CancellationRouterState,
} from '../CancellationContainer';
import { CancellationContext } from '../CancellationContainer';
import type { SaveBodyProps } from '../cancellationReason';
import { getIsPayingMinAmount } from './utils';
const container = css`
& > * + * {
margin-top: ${space[6]}px;
}
`;
const buttonsCss = css`
display: flex;
flex-direction: column;
gap: ${space[5]}px;
${from.tablet} {
flex-direction: row;
}
`;
const buttonCss = css`
justify-content: center;
${from.tablet} {
&:last-child {
margin-left: auto;
}
}
`;
export const ContributionsCancellationFlowFinancialSaveAttempt: React.FC<
SaveBodyProps
> = ({ caseId, holidayStops, deliveryCredits }: SaveBodyProps) => {
const [showAmountUpdateForm, setShowUpdateForm] = useState(false);
const location = useLocation();
const routerState = location.state as CancellationRouterState;
const navigate = useNavigate();
const { productDetail, productType } = useContext(
CancellationContext,
) as CancellationContextInterface;
const isSupporterPlusAndFreePeriodOfferIsActive =
featureSwitches.supporterplusCancellationOffer &&
productType.productType === 'supporterplus';
const isContributionAndBreakFeatureIsActive =
featureSwitches.contributionCancellationPause &&
productType.productType === 'contributions';
const [
showAlternativeBeforeCancelling,
setShowAlternativeBeforeCancelling,
] = useState<TrueFalsePending>(
isSupporterPlusAndFreePeriodOfferIsActive ||
isContributionAndBreakFeatureIsActive
? 'pending'
: false,
);
const [discountPreviewDetails, setDiscountPreviewDetails] =
useState<DiscountPreviewResponse | null>(null);
useEffect(() => {
if (
isSupporterPlusAndFreePeriodOfferIsActive ||
isContributionAndBreakFeatureIsActive
) {
(async () => {
try {
const response = await fetchWithDefaultParameters(
'/api/discounts/preview-discount',
{
method: 'POST',
body: JSON.stringify({
subscriptionNumber:
productDetail.subscription.subscriptionId,
}),
},
);
if (response.ok) {
// api returns a 400 response if the user is not eligible
setShowAlternativeBeforeCancelling(true);
const offerData = await response.json();
setDiscountPreviewDetails(offerData);
} else {
setShowAlternativeBeforeCancelling(false);
}
} catch {
setShowAlternativeBeforeCancelling(false);
}
})();
}
}, [
isContributionAndBreakFeatureIsActive,
isSupporterPlusAndFreePeriodOfferIsActive,
productDetail.subscription.subscriptionId,
]);
if (!productType || !productDetail || !routerState.selectedReasonId) {
return <Navigate to="../" />;
}
const onUpdateConfirmed = (updatedAmount: number) => {
trackEventInOphanOnly({
eventCategory: 'cancellation_flow_financial_circumstances',
eventAction: 'click',
eventLabel: 'change',
});
navigate('../saved', {
state: { ...routerState, updatedContributionAmount: updatedAmount },
});
};
const onReduceClicked = () => {
trackEventInOphanOnly({
eventCategory: 'cancellation_flow_financial_circumstances',
eventAction: 'click',
eventLabel: 'reduce',
});
setShowUpdateForm(true);
};
const onCancelClicked = () => {
if (showAlternativeBeforeCancelling) {
const cancelAlternativeUrlPart =
cancelAlternativeUrlPartLookup[productType.productType] || '';
navigate(`../${cancelAlternativeUrlPart}`, {
state: {
...routerState,
...discountPreviewDetails,
caseId,
holidayStops,
deliveryCredits,
},
});
} else {
trackEventInOphanOnly({
eventCategory: 'cancellation_flow_financial_circumstances',
eventAction: 'click',
eventLabel: 'cancel',
});
navigate('../confirmed', {
state: { ...routerState, caseId },
});
}
};
const onReturnClicked = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
navigate('/');
};
const mainPlan = getMainPlan(productDetail.subscription);
if (!isPaidSubscriptionPlan(mainPlan)) {
Sentry.captureMessage(
'mainPlan is not a PaidSubscriptionPlan in ContributionsCancellationFlowFinancialSaveAttempt',
);
return <GenericErrorMessage />;
}
const isPayingMinAmount = getIsPayingMinAmount(mainPlan);
return (
<div css={container}>
{isPayingMinAmount ? (
<>
<div>
We understand that financial circumstances change, and
your current contribution might not suit you right now.
</div>
<Button onClick={onCancelClicked}>
Confirm cancellation
</Button>
</>
) : (
<>
<div>
We understand that financial circumstances change. If
you can, we hope you’ll consider reducing the size of
your contribution today rather than cancelling it.
Simply pick a new amount and we’ll do the rest.
</div>
{showAmountUpdateForm ? (
<ContributionUpdateAmountForm
currentAmount={mainPlan.price / 100}
subscriptionId={
productDetail.subscription.subscriptionId
}
mainPlan={mainPlan}
productType={PRODUCT_TYPES.contributions}
nextPaymentDate={
productDetail.subscription.nextPaymentDate
}
mode="CANCELLATION_SAVE"
onUpdateConfirmed={onUpdateConfirmed}
withReturnToAccountOverviewButton
/>
) : (
<div css={buttonsCss}>
<Button
onClick={onReduceClicked}
cssOverrides={buttonCss}
>
Reduce amount
</Button>
<Button
icon={
showAlternativeBeforeCancelling ===
'pending' ? (
<Spinner size="xsmall" />
) : undefined
}
iconSide="right"
disabled={
showAlternativeBeforeCancelling ===
'pending'
}
aria-disabled={
showAlternativeBeforeCancelling ===
'pending'
}
onClick={onCancelClicked}
priority="tertiary"
cssOverrides={buttonCss}
>
I still want to cancel
</Button>
<LinkButton
href="/"
onClick={onReturnClicked}
priority="subdued"
cssOverrides={buttonCss}
>
Return to your account
</LinkButton>
</div>
)}
</>
)}
</div>
);
};