client/components/mma/paymentUpdate/PaymentDetailUpdateConfirmation.tsx (432 lines of code) (raw):

import { css } from '@emotion/react'; import { from, headlineBold28, palette, space, textSans17, textSansBold17, until, } from '@guardian/source/foundations'; import { Button, LinkButton, themeButtonReaderRevenueBrand, } from '@guardian/source/react-components'; import { InfoSummary } from '@guardian/source-development-kitchen/react-components'; import { useContext } from 'react'; import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { buttonCentredCss } from '@/client/styles/ButtonStyles'; import type { ProductDetail, Subscription, WithSubscription, } from '../../../../shared/productResponse'; import { formatDate, getMainPlan, getSpecificProductType, isPaidSubscriptionPlan, } from '../../../../shared/productResponse'; import { GenericErrorScreen } from '../../shared/GenericErrorScreen'; import { ArrowIcon } from '../shared/assets/ArrowIcon'; import { LinkButton as MMALinkButton } from '../shared/Buttons'; import { CardDisplay } from '../shared/CardDisplay'; import { DirectDebitDisplay } from '../shared/DirectDebitDisplay'; import type { IsFromAppProps } from '../shared/IsFromAppProps'; import { PaypalDisplay } from '../shared/PaypalDisplay'; import { SepaDisplay } from '../shared/SepaDisplay'; import type { PaymentUpdateContextInterface } from './PaymentDetailUpdateContainer'; import { PaymentUpdateContext } from './PaymentDetailUpdateContainer'; interface ConfirmedNewPaymentDetailsRendererProps { subscription: Subscription; subHasExpectedPaymentType: boolean; previousProductDetail: ProductDetail; } const keyValuePairCss = css` list-style: none; margin: 0; padding: 0; `; const keyCss = css` ${textSansBold17}; padding: 0 ${space[2]}px 0 0; display: inline-block; vertical-align: top; width: 50%; ${from.tablet} { width: 14ch; } `; const valueCss = css` ${textSans17}; padding: 0; display: inline-block; vertical-align: top; width: 50%; ${from.tablet} { width: calc(100% - 15ch); } `; const subHeadingCss = ` border-top: 1px solid ${palette.neutral['86']}; ${headlineBold28}; margin-top: ${space[9]}px; ${until.tablet} { font-size: 1.25rem; line-height: 1.6; }; `; function getPaymentInterval(interval: string) { if (interval === 'year') { return 'annual'; } else if (interval === 'month') { return 'monthly'; } else if (interval === 'quarter') { return 'quarterly'; } } export const ConfirmedNewPaymentDetailsRenderer = ({ subscription, subHasExpectedPaymentType, previousProductDetail, }: ConfirmedNewPaymentDetailsRendererProps) => { const mainPlan = getMainPlan(subscription); const specificProductType = getSpecificProductType( previousProductDetail.tier, ); if (subHasExpectedPaymentType && isPaidSubscriptionPlan(mainPlan)) { return ( <div css={css` border: 1px solid ${palette.neutral[86]}; margin-bottom: ${space[6]}px; `} > <div css={css` display: flex; justify-content: space-between; align-items: start; background-color: ${palette.brand[400]}; ${from.mobileLandscape} { align-items: center; } `} > <h2 css={css` font-size: 17px; font-weight: bold; margin: 0; padding: ${space[3]}px; color: ${palette.neutral[100]}; ${until.mobileLandscape} { padding: ${space[3]}px; } ${from.tablet} { font-size: 20px; padding: ${space[3]}px ${space[5]}px; } `} > {specificProductType.productTitle(mainPlan)} </h2> </div> <div css={css` padding: ${space[3]}px; ${from.tablet} { padding: ${space[5]}px; display: flex; } `} > <div css={css` ${from.tablet} { flex: 1; display: flex; flex-flow: column nowrap; } `} > {previousProductDetail.isPaidTier && ( <> <ul css={keyValuePairCss}> <li css={keyCss}>Payment method</li> <li css={valueCss}> {subscription.card && ( <CardDisplay inErrorState={false} cssOverrides={css` margin: 0; `} {...subscription.card} /> )} {subscription.payPalEmail && ( <PaypalDisplay payPalId={ subscription.payPalEmail } /> )} {subscription.sepaMandate && ( <SepaDisplay accountName={ subscription.sepaMandate .accountName } iban={ subscription.sepaMandate .iban } /> )} {subscription.mandate && ( <DirectDebitDisplay inErrorState={false} {...subscription.mandate} /> )} {subscription.stripePublicKeyForCardAddition && ( <span>No Payment Method</span> )} </li> </ul> </> )} </div> {subscription.card && ( <div css={css` padding: ${space[3]}px 0 0 0; ${from.tablet} { flex: 1; display: inline-block; flex-flow: column nowrap; margin: 0; padding: 0 0 0 ${space[5]}px; } ul:last-of-type { margin-bottom: ${space[5]}px; } `} > {subscription.card.expiry && ( <> <span css={css` ${keyCss}; ${from.tablet} { text-align: right; } `} > Expiry </span> <span css={css` ${valueCss}; color: ${palette.neutral[7]}; `} > {subscription.card.expiry.month} /{' '} {subscription.card.expiry.year} </span> </> )} </div> )} </div> {subscription.nextPaymentPrice && subscription.nextPaymentDate && ( <div css={css` padding: ${space[3]}px; border-top: 1px solid ${palette.neutral[86]}; ${from.tablet} { padding: ${space[5]}px; display: flex; } `} > <div css={css` ${from.tablet} { margin: ${space[6]}px 0 0 0; padding: ${space[6]}px 0 0 0; flex: 1; display: flex; flex-flow: column nowrap; padding: 0; margin: 0; } `} > <> <ul css={keyValuePairCss}> <li css={keyCss}>Next Payment</li> <li css={valueCss}> {mainPlan.currency} {( subscription.nextPaymentPrice / 100.0 ).toFixed(2)}{' '} /{' '} {getPaymentInterval( mainPlan.billingPeriod, )} {subscription.stripePublicKeyForCardAddition && ( <span>No Payment Method</span> )} </li> </ul> </> </div> <div css={css` padding: ${space[3]}px 0 0 0; ${from.tablet} { margin: ${space[6]}px 0 0 0; flex: 1; display: inline-block; flex-flow: column nowrap; padding: 0 0 0 ${space[5]}px; margin: 0; padding: 0 0 0 ${space[5]}px; } ul:last-of-type { margin-bottom: ${space[5]}px; } `} > <> <span css={keyCss}>Next payment date</span> <span css={valueCss}> {formatDate(subscription.nextPaymentDate)} </span> </> </div> </div> )} </div> ); } return ( <GenericErrorScreen loggingMessage="Unsupported new payment method" /> ); // unsupported operation currently }; interface PaymentMethodUpdatedProps extends IsFromAppProps { subs: WithSubscription[] | object; paymentFailureRecoveryMessage: string; subHasExpectedPaymentType: boolean; previousProductDetail: ProductDetail; } export const PaymentMethodUpdated = ({ isFromApp, subs, paymentFailureRecoveryMessage, subHasExpectedPaymentType, previousProductDetail, }: PaymentMethodUpdatedProps) => { const navigate = useNavigate(); return Array.isArray(subs) && subs.length === 1 ? ( <> <h1 css={css` ${subHeadingCss} `} > Your payment details were updated successfully </h1> {previousProductDetail.alertText && paymentFailureRecoveryMessage && ( <InfoSummary context="" message={paymentFailureRecoveryMessage} cssOverrides={css` margin-bottom: ${space[6]}px; `} /> )} <ConfirmedNewPaymentDetailsRenderer subscription={subs[0].subscription} subHasExpectedPaymentType={subHasExpectedPaymentType} previousProductDetail={previousProductDetail} /> <h2 css={css` margin-top: ${space[9]}px; margin-bottom: ${space[1]}px; line-height: 1; font-weight: bold; font-size: 1.75rem; `} > Thank you </h2> <span> You are helping to support independent journalism.</span> <div css={{ marginTop: `${space[9]}px` }}> {isFromApp ? ( <LinkButton theme={themeButtonReaderRevenueBrand} href="x-gu://mma/payment-update?success" cssOverrides={buttonCentredCss} > Return to the app </LinkButton> ) : ( <MMALinkButton to="/" text="Back to Account overview" colour={palette.brand[400]} textColour={palette.neutral[100]} fontWeight="bold" right /> )} </div> </> ) : ( <> <GenericErrorScreen loggingMessage={`${ Array.isArray(subs) && subs.length } subs returned when one was expected`} /> <Button priority="tertiary" icon={<ArrowIcon pointingLeft />} iconSide="left" onClick={() => { navigate('/'); }} > Return to your account </Button> </> ); }; export const PaymentDetailUpdateConfirmation = () => { const { productDetail: previousProductDetail, isFromApp } = useContext( PaymentUpdateContext, ) as PaymentUpdateContextInterface; const location = useLocation(); const state = location.state as { paymentFailureRecoveryMessage: string; subHasExpectedPaymentType: boolean; newSubscriptionData: WithSubscription[]; }; const newSubscriptionData = state.newSubscriptionData; return state ? ( <PaymentMethodUpdated subs={newSubscriptionData} paymentFailureRecoveryMessage={state.paymentFailureRecoveryMessage} subHasExpectedPaymentType={state.subHasExpectedPaymentType} previousProductDetail={previousProductDetail} isFromApp={isFromApp} /> ) : ( <Navigate to="/" /> ); };