client/components/mma/cancel/cancellationSaves/membership/MembershipSwitch.tsx (327 lines of code) (raw):
import { css } from '@emotion/react';
import { palette, space, textSans17 } from '@guardian/source/foundations';
import {
Button,
Stack,
SvgClock,
SvgCreditCard,
themeButtonReaderRevenueBrand,
} from '@guardian/source/react-components';
import { ErrorSummary } from '@guardian/source-development-kitchen/react-components';
import { useContext, useEffect, useState } from 'react';
import { Navigate, useLocation, useNavigate } from 'react-router';
import { dateString, parseDate } from '../../../../../../shared/dates';
import type {
PaidSubscriptionPlan,
Subscription} from '../../../../../../shared/productResponse';
import {
MDA_TEST_USER_HEADER
} from '../../../../../../shared/productResponse';
import { getMainPlan } from '../../../../../../shared/productResponse';
import type { ProductSwitchType } from '../../../../../../shared/productSwitchTypes';
import { getBillingPeriodAdjective } from '../../../../../../shared/productTypes';
import {
buttonCentredCss,
buttonMutedCss,
stackedButtonLayoutCss,
wideButtonCss,
} from '../../../../../styles/ButtonStyles';
import {
errorSummaryLinkCss,
errorSummaryOverrideCss,
} from '../../../../../styles/ErrorStyles';
import {
iconListCss,
listWithDividersCss,
productTitleCss,
sectionSpacing,
smallPrintCss,
} from '../../../../../styles/GenericStyles';
import { getOldMembershipPrice } from '../../../../../utilities/pricingConfig/membershipPriceRise';
import { JsonResponseHandler } from '../../../shared/asyncComponents/DefaultApiResponseHandler';
import { Card } from '../../../shared/Card';
import { Heading } from '../../../shared/Heading';
import { PaymentDetails } from '../../../shared/PaymentDetails';
import type {
CancellationContextInterface,
CancellationPageTitleInterface,
CancellationRouterState,
} from '../../CancellationContainer';
import {
CancellationContext,
CancellationPageTitleContext,
} from '../../CancellationContainer';
import { newAmountCss } from './SaveStyles';
const YourNewSupport = ({
contributionPriceDisplay,
billingPeriod,
monthlyOrAnnual,
}: {
contributionPriceDisplay: string;
billingPeriod: string;
monthlyOrAnnual: string;
}) => {
return (
<section css={sectionSpacing}>
<Heading
sansSerif
cssOverrides={css`
margin-bottom: ${space[3]}px;
`}
>
Your new support
</Heading>
<Card>
<Card.Header backgroundColor={palette.brand[400]}>
<h3 css={productTitleCss}>{monthlyOrAnnual}</h3>
</Card.Header>
<Card.Section>
<p
css={css`
${textSans17};
margin: 0;
`}
>
{monthlyOrAnnual} support with fewer funding asks and an
exclusive email from the newsroom
</p>
<p css={newAmountCss}>
{contributionPriceDisplay}/{billingPeriod}
</p>
</Card.Section>
</Card>
</section>
);
};
const WhatHappensNext = (props: {
contributionPriceDisplay: string;
subscription: Subscription;
}) => {
const nextPaymentDate = dateString(
parseDate(props.subscription.nextPaymentDate ?? '').date,
'd MMMM',
);
return (
<section css={sectionSpacing}>
<Stack space={4}>
<Heading sansSerif>What happens next?</Heading>
<ul css={[iconListCss, listWithDividersCss]}>
<li>
<SvgClock size="medium" />
<span>
<strong>
Your new support will start at the end of your
billing period
</strong>
<br />
You will be charged {
props.contributionPriceDisplay
}{' '}
from the {nextPaymentDate}. From that date, you will
continue to receive the supporter newsletter and see
fewer support asks but you will lose access to
premium features in the App and ad-free reading.
</span>
</li>
<li>
<SvgCreditCard size="medium" />
<span data-qm-masking="blocklist">
<strong>Your payment method</strong>
<br />
The payment will be taken from{' '}
<PaymentDetails subscription={props.subscription} />
</span>
</li>
</ul>
</Stack>
</section>
);
};
const TsAndCs = ({
contributionPriceDisplay,
paymentDay,
paymentMonth,
}: {
contributionPriceDisplay: string;
paymentDay: string;
paymentMonth: string;
}) => (
<section css={sectionSpacing}>
<p css={smallPrintCss}>
We will attempt to take payment of {contributionPriceDisplay}, on
the {paymentDay} day of {paymentMonth}, from now until you cancel
your payment. Payments may take up to 6 days to be recorded in your
bank account. You can change how much you give or cancel your
payment at any time.
</p>
<p css={smallPrintCss}>
By proceeding, you are agreeing to our{' '}
<a href="https://www.theguardian.com/info/2016/apr/04/contribution-terms-and-conditions">
Terms and Conditions
</a>
.
</p>
<p css={smallPrintCss}>
To find out what personal data we collect and how we use it, please
visit our{' '}
<a href="https://www.theguardian.com/help/privacy-policy">
Privacy Policy
</a>
.
</p>
</section>
);
export const MembershipSwitch = () => {
const navigate = useNavigate();
const location = useLocation();
const routerState = location.state as CancellationRouterState;
const cancellationContext = useContext(
CancellationContext,
) as CancellationContextInterface;
const membership = cancellationContext.productDetail;
const [isSwitching, setIsSwitching] = useState<boolean>(false);
const [switchingError, setSwitchingError] = useState<boolean>(false);
const pageTitleContext = useContext(
CancellationPageTitleContext,
) as CancellationPageTitleInterface;
useEffect(() => {
pageTitleContext.setPageTitle('Change your support');
}, [pageTitleContext]);
if (!membership) {
return <Navigate to="/" />;
}
const mainPlan = getMainPlan(
membership.subscription,
) as PaidSubscriptionPlan;
const contributionPriceDisplay = `${
mainPlan.currency
}${getOldMembershipPrice(mainPlan)}`;
const billingPeriod = mainPlan.billingPeriod;
const monthlyOrAnnual = getBillingPeriodAdjective(billingPeriod);
const indefiniteArticle = monthlyOrAnnual === 'Monthly' ? 'a' : 'an';
const paymentDay = parseDate(mainPlan.chargedThrough ?? undefined).dateStr(
'do',
);
const paymentMonth =
monthlyOrAnnual === 'Monthly'
? 'every month'
: parseDate(mainPlan.chargedThrough ?? undefined).dateStr('MMMM');
const productSwitchType: ProductSwitchType = 'to-recurring-contribution';
const productMoveFetch = () =>
fetch(
`/api/product-move/${productSwitchType}/${membership.subscription.subscriptionId}`,
{
method: 'POST',
body: JSON.stringify({
price: getOldMembershipPrice(mainPlan),
preview: false,
checkChargeAmountBeforeUpdate: false,
}),
headers: {
'Content-Type': 'application/json',
[MDA_TEST_USER_HEADER]: `${membership.isTestUser}`,
},
},
);
const confirmSwitch = async () => {
if (isSwitching) {
return;
}
try {
setIsSwitching(true);
const response = await productMoveFetch();
const data = await JsonResponseHandler(response);
if (data === null) {
setIsSwitching(false);
setSwitchingError(true);
} else {
navigate('../switch-thank-you', {
state: { ...routerState, journeyCompleted: true },
});
}
} catch {
setIsSwitching(false);
setSwitchingError(true);
}
};
return (
<>
<section css={sectionSpacing}>
<Heading sansSerif>Review and confirm change</Heading>
<p
css={css`
${textSans17};
margin: 0;
`}
>
Please confirm that you’re changing support type from a
Membership to {indefiniteArticle} {monthlyOrAnnual}{' '}
contribution.
</p>
</section>
<YourNewSupport
contributionPriceDisplay={contributionPriceDisplay}
billingPeriod={billingPeriod}
monthlyOrAnnual={monthlyOrAnnual}
/>
<WhatHappensNext
contributionPriceDisplay={contributionPriceDisplay}
subscription={membership.subscription}
/>
<section css={sectionSpacing}>
<p
css={css`
${textSans17};
border-top: 1px solid ${palette.neutral[86]};
padding-top: ${space[5]}px;
`}
>
Please note if you confirm the change you will not be able
to rejoin the Guardian Members scheme, as it’s now closed to
new members.
</p>
</section>
{switchingError && (
<section css={sectionSpacing} id="productSwitchErrorMessage">
<ErrorSummary
message={'We were unable to change your support'}
context={<SwitchErrorContext />}
cssOverrides={errorSummaryOverrideCss}
/>
</section>
)}
<section css={[sectionSpacing, stackedButtonLayoutCss]}>
<Button
theme={themeButtonReaderRevenueBrand}
cssOverrides={[buttonCentredCss, wideButtonCss]}
isLoading={isSwitching}
onClick={confirmSwitch}
>
Confirm change
</Button>
<Button
priority="tertiary"
cssOverrides={[buttonCentredCss, buttonMutedCss]}
onClick={() =>
navigate('../offers', { state: { ...routerState } })
}
>
Back
</Button>
</section>
<TsAndCs
contributionPriceDisplay={contributionPriceDisplay}
paymentDay={paymentDay}
paymentMonth={paymentMonth}
/>
</>
);
};
function SwitchErrorContext() {
return (
<>
Please ensure your payment details are correct. If the problem
persists get in touch at{' '}
<a
css={errorSummaryLinkCss}
href="mailto:customer.help@guardian.com"
>
customer.help@guardian.com
</a>
.
</>
);
}