client/components/mma/upgrade/UpgradeSupportAmountForm.tsx (216 lines of code) (raw):

import { css } from '@emotion/react'; import { space } from '@guardian/source/foundations'; import { Button, ChoiceCard, ChoiceCardGroup, Stack, TextInput, themeButtonReaderRevenueBrand, } from '@guardian/source/react-components'; import type { Dispatch, SetStateAction } from 'react'; import { useContext, useEffect, useState } from 'react'; import { buttonCentredCss, buttonContainerCss, } from '../../../styles/ButtonStyles'; import { twoColumnChoiceCardMobile } from '../../../styles/GenericStyles'; import type { ContributionInterval } from '../../../utilities/pricingConfig/contributionsAmount'; import { contributionAmountsLookup } from '../../../utilities/pricingConfig/contributionsAmount'; import { formatAmount, waitForElement } from '../../../utilities/utils'; import { UpgradeBenefitsCard } from '../shared/benefits/BenefitsCard'; import { getUpgradeBenefits } from '../shared/benefits/BenefitsConfiguration'; import { Heading } from '../shared/Heading'; import type { UpgradeSupportInterface } from './UpgradeSupportContainer'; import { UpgradeSupportContext } from './UpgradeSupportContainer'; async function scrollToConfirmChange() { const confirmElement = await waitForElement('#confirm-change'); confirmElement?.scrollIntoView({ behavior: 'smooth', }); parent.location.hash = 'confirm-change'; } function validateChoice( currentAmount: number, chosenAmount: number | null, minAmount: number, maxAmount: number, isOtherAmountSelected: boolean, ): string | null { if (!chosenAmount && !isOtherAmountSelected) { return 'Please make a selection'; } else if (chosenAmount === currentAmount) { return 'This is the same amount as your current support. Please enter a new amount.'; } else if ( !chosenAmount || chosenAmount < minAmount || chosenAmount > maxAmount ) { return `Enter a number between ${minAmount} and ${maxAmount}.`; } return null; } function BenefitsDisplay({ chosenAmount, chosenAmountDisplay, threshold, }: { chosenAmount: number; chosenAmountDisplay: string; threshold: number; }) { const benefitsList = chosenAmount < threshold ? getUpgradeBenefits('contributions') : getUpgradeBenefits('supporterplus'); return ( <UpgradeBenefitsCard chosenAmountDisplay={chosenAmountDisplay} benefits={benefitsList} /> ); } interface UpgradeSupportAmountFormProps { chosenAmount: number | null; setChosenAmount: Dispatch<SetStateAction<number | null>>; threshold: number; setContinuedToConfirmation: Dispatch<SetStateAction<boolean>>; continuedToConfirmation: boolean; suggestedAmounts: number[]; } export const UpgradeSupportAmountForm = ({ chosenAmount, setChosenAmount, threshold, setContinuedToConfirmation, continuedToConfirmation, suggestedAmounts, }: UpgradeSupportAmountFormProps) => { const { mainPlan } = useContext( UpgradeSupportContext, ) as UpgradeSupportInterface; const priceConfig = (contributionAmountsLookup[mainPlan.currencyISO] || contributionAmountsLookup.international)[ mainPlan.billingPeriod as ContributionInterval ]; const currencySymbol = mainPlan.currency; const amountLabel = (amount: number) => { return `${currencySymbol}${formatAmount(amount)} per ${ mainPlan.billingPeriod }`; }; const currentAmount = mainPlan.price / 100; const otherAmountLabel = `Enter an amount (${currencySymbol} per ${mainPlan.billingPeriod})`; const [isOtherAmountSelected, setIsOtherAmountSelected] = useState<boolean>(false); const [otherAmountSelected, setOtherAmountSelected] = useState< number | null >(null); const [hasInteractedWithOtherAmount, setHasInteractedWithOtherAmount] = useState<boolean>(false); const [errorMessage, setErrorMessage] = useState<string | null>(null); useEffect(() => { if (otherAmountSelected !== null) { setHasInteractedWithOtherAmount(true); } }, [otherAmountSelected]); useEffect(() => { const newErrorMessage = validateChoice( currentAmount, chosenAmount, priceConfig.minAmount, priceConfig.maxAmount, isOtherAmountSelected, ); setIsOtherAmountSelected(chosenAmount === otherAmountSelected); setErrorMessage(newErrorMessage); }, [ otherAmountSelected, chosenAmount, currentAmount, isOtherAmountSelected, priceConfig.maxAmount, priceConfig.minAmount, ]); return ( <> <Stack space={3}> <Heading sansSerif level="3" borderless> 1. Choose your new amount </Heading> <Stack space={4}> <ChoiceCardGroup cssOverrides={twoColumnChoiceCardMobile} name="amounts" data-cy="contribution-amount-choices" > <> {suggestedAmounts.map((amount) => ( <ChoiceCard id={`amount-${amount}`} key={amount} value={amount.toString()} label={amountLabel(amount)} checked={ chosenAmount === amount && !isOtherAmountSelected } onChange={() => { setChosenAmount(amount); setIsOtherAmountSelected(false); setContinuedToConfirmation(false); }} /> ))} <ChoiceCard id={`amount-other`} value="Other" label="Other" checked={isOtherAmountSelected} onChange={() => { setIsOtherAmountSelected(true); setChosenAmount(otherAmountSelected); setContinuedToConfirmation(false); }} /> </> </ChoiceCardGroup> {isOtherAmountSelected && ( <div css={css` margin-top: ${space[3]}px; `} > <TextInput label={otherAmountLabel} supporting={`Support ${currencySymbol}${threshold}/${mainPlan.billingPeriod} or more to unlock extras.`} error={ (hasInteractedWithOtherAmount && errorMessage) || undefined } type="number" width={30} value={otherAmountSelected?.toString() || ''} onWheel={(event) => event.currentTarget.blur()} onChange={(event) => { setChosenAmount( event.target.value ? Number(event.target.value) : null, ); setOtherAmountSelected( event.target.value ? Number(event.target.value) : null, ); setContinuedToConfirmation(false); }} /> </div> )} {!errorMessage && !!chosenAmount && ( <BenefitsDisplay chosenAmountDisplay={`${currencySymbol}${formatAmount( chosenAmount, )} per ${mainPlan.billingPeriod}`} chosenAmount={chosenAmount} threshold={threshold} /> )} {!continuedToConfirmation && !errorMessage && !!chosenAmount && ( <section css={buttonContainerCss}> <Button theme={themeButtonReaderRevenueBrand} cssOverrides={buttonCentredCss} onClick={() => { setContinuedToConfirmation(true); scrollToConfirmChange(); }} > Continue with {currencySymbol} {formatAmount(chosenAmount)}/ {mainPlan.billingPeriod} </Button> </section> )} </Stack> </Stack> </> ); };