client/components/mma/cancel/cancellationSaves/digipack/DigiSubThankYouOffer.tsx (300 lines of code) (raw):
import { css } from '@emotion/react';
import {
from,
headlineBold24,
headlineBold34,
space,
textSans17,
textSansBold20,
} from '@guardian/source/foundations';
import {
Button,
Stack,
SvgTickRound,
themeButtonReaderRevenueBrand,
} from '@guardian/source/react-components';
import { ErrorSummary } from '@guardian/source-development-kitchen/react-components';
import { useContext, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import {
buttonCentredCss,
buttonContainerCss,
} from '@/client/styles/ButtonStyles';
import type { DiscountPreviewResponse } from '@/client/utilities/discountPreview';
import { fetchWithDefaultParameters } from '@/client/utilities/fetch';
import { formatAmount } from '@/client/utilities/utils';
import { appendCorrectPluralisation } from '@/shared/generalTypes';
import type { PaidSubscriptionPlan } from '@/shared/productResponse';
import { getMainPlan } from '@/shared/productResponse';
import { dateString } from '../../../../../../shared/dates';
import { DefaultLoadingView } from '../../../shared/asyncComponents/DefaultLoadingView';
import { benefitsCss } from '../../../shared/benefits/BenefitsStyles';
import { Heading } from '../../../shared/Heading';
import type {
CancellationContextInterface,
CancellationRouterState,
} from '../../CancellationContainer';
import { CancellationContext } from '../../CancellationContainer';
type DiscountOfferProps = {
currencySymbol: string;
discountPeriod: string;
discountedPrice: number;
isApplyDiscountLoading: boolean;
hasDiscountFailed: boolean;
handleDiscountOfferClick: () => void;
newPrice: number;
};
const DiscountOffer = ({
currencySymbol,
discountPeriod,
discountedPrice,
isApplyDiscountLoading: isDiscountLoading,
hasDiscountFailed,
handleDiscountOfferClick,
newPrice,
}: DiscountOfferProps) => (
<Stack
space={4}
cssOverrides={css`
background-color: #f3f7fe;
border-radius: 4px;
padding: ${space[4]}px;
`}
>
<div>
<div
css={css`
${textSansBold20};
margin-bottom: ${space[2]}px;
`}
>
A subscription offer just for you
</div>
<ul css={benefitsCss}>
<li>
<SvgTickRound isAnnouncedByScreenReader size="medium" />
<span
css={css`
padding-top: ${space[1]}px;
`}
>
Get a 25% discount for {discountPeriod} (
{currencySymbol}
{formatAmount(discountedPrice)}, then {currencySymbol}
{newPrice})
</span>
</li>
<li>
<SvgTickRound isAnnouncedByScreenReader size="medium" />{' '}
<span
css={css`
padding-top: ${space[1]}px;
`}
>
Keep all your supporter extras, including unlimited,
ad-free reading
</span>
</li>
<li>
<SvgTickRound isAnnouncedByScreenReader size="medium" />{' '}
<span
css={css`
padding-top: ${space[1]}px;
`}
>
Exclusive access to the Editions app (our daily digital
newspaper)
</span>
</li>
</ul>
</div>
<div css={buttonContainerCss}>
<Button
theme={themeButtonReaderRevenueBrand}
cssOverrides={buttonCentredCss}
onClick={handleDiscountOfferClick}
isLoading={isDiscountLoading}
>
Keep support with discount
</Button>
</div>
{hasDiscountFailed && (
<ErrorSummary
message={
'We were unable to apply your discount. Please try again'
}
/>
)}
</Stack>
);
function getDiscountPeriod(discountPreview: DiscountPreviewResponse): string {
return `${discountPreview.upToPeriods} ${appendCorrectPluralisation(
discountPreview.upToPeriodsType,
discountPreview.upToPeriods,
)}`;
}
export interface DigisubCancellationRouterState
extends CancellationRouterState {
eligibleForDiscount: boolean;
discountedPrice?: number;
discountPeriod?: string;
}
export const DigiSubThankYouOffer = () => {
const navigate = useNavigate();
const cancellationContext = useContext(
CancellationContext,
) as CancellationContextInterface;
const productDetail = cancellationContext.productDetail;
const location = useLocation();
const routerState = location.state as CancellationRouterState;
const [isApplyDiscountLoading, setIsApplyDiscountLoading] =
useState<boolean>(false);
const [hasDiscountFailed, setHasDiscountFailed] = useState<boolean>(false);
const [isPreviewDiscountLoading, setIsPreviewDiscountLoading] =
useState<boolean>(false);
const [discountPreview, setDiscountPreview] =
useState<DiscountPreviewResponse | null>(null);
useEffect(() => {
setIsPreviewDiscountLoading(true);
fetchWithDefaultParameters('/api/discounts/preview-discount', {
method: 'POST',
body: JSON.stringify({
subscriptionNumber: productDetail.subscription.subscriptionId,
}),
}).then((response) => {
if (response.ok) {
response.json().then((data) => {
setDiscountPreview(data);
});
}
setIsPreviewDiscountLoading(false);
});
}, [productDetail.subscription.subscriptionId]);
if (isPreviewDiscountLoading) {
return <DefaultLoadingView loadingMessage="Loading..." />;
}
if (!productDetail) {
navigate('/');
return null;
}
const supportStartYear = dateString(
new Date(productDetail.joinDate),
'yyyy',
);
const mainPlan = getMainPlan(
productDetail.subscription,
) as PaidSubscriptionPlan;
const newPrice =
(productDetail.subscription.nextPaymentPrice ?? mainPlan.price) / 100;
const newRouterState: DigisubCancellationRouterState = {
...routerState,
discountedPrice: discountPreview?.discountedPrice,
eligibleForDiscount: discountPreview !== null,
discountPeriod: discountPreview
? getDiscountPeriod(discountPreview)
: undefined,
};
const handleDiscountOfferClick = async () => {
if (isApplyDiscountLoading) {
return;
}
try {
setIsApplyDiscountLoading(true);
const response = await fetchWithDefaultParameters(
'/api/discounts/apply-discount',
{
method: 'POST',
body: JSON.stringify({
subscriptionNumber:
productDetail.subscription.subscriptionId,
}),
},
);
if (response.ok) {
setIsApplyDiscountLoading(false);
navigate('../discount-confirmed', {
state: {
...newRouterState,
journeyCompleted: true,
},
});
} else {
setIsApplyDiscountLoading(false);
setHasDiscountFailed(true);
}
} catch {
setIsApplyDiscountLoading(false);
setHasDiscountFailed(true);
}
};
return (
<section
css={css`
margin-top: ${space[4]}px;
`}
>
<Stack space={6}>
<h2
css={css`
${headlineBold24};
margin-top: 0;
margin-bottom: 0;
${from.tablet} {
${headlineBold34};
}
`}
>
Thank you for supporting the Guardian since{' '}
{supportStartYear}
</h2>
<Stack space={1}>
<Heading sansSerif borderless level="3">
Your funding has played a vital role in our progress
</Heading>
<p
css={css`
${textSans17};
`}
>
Since you first joined as a Guardian supporter, we've
lived through some of the most important news events of
our times. Without you, our fearless, independent
journalism wouldn't have reached millions around the
world. We're so grateful.
</p>
</Stack>
{discountPreview && (
<DiscountOffer
currencySymbol={mainPlan.currency}
discountPeriod={getDiscountPeriod(discountPreview)}
discountedPrice={discountPreview.discountedPrice}
isApplyDiscountLoading={isApplyDiscountLoading}
hasDiscountFailed={hasDiscountFailed}
handleDiscountOfferClick={handleDiscountOfferClick}
newPrice={newPrice}
/>
)}
<div>
<h3
css={css`
${textSansBold20};
margin: 0;
`}
>
Still want to cancel?
</h3>
<Button
priority="subdued"
onClick={() =>
navigate('../confirm-cancel', {
state: { ...newRouterState },
})
}
>
Continue to cancel
</Button>
</div>
</Stack>
</section>
);
};