diff --git a/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx b/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
index b378978e73..b7048f7e08 100644
--- a/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
+++ b/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
@@ -1,27 +1,18 @@
import { css } from '@emotion/react';
import { palette, space } from '@guardian/source/foundations';
+import { Checkbox, Label, TextInput } from '@guardian/source/react-components';
import {
- Checkbox,
- Label,
- Radio,
- RadioGroup,
- TextInput,
-} from '@guardian/source/react-components';
-import {
- Divider,
ErrorSummary,
InfoSummary,
} from '@guardian/source-development-kitchen/react-components';
import {
CardNumberElement,
- ExpressCheckoutElement,
useElements,
useStripe,
} from '@stripe/react-stripe-js';
import type { ExpressPaymentType } from '@stripe/stripe-js';
-import { useEffect, useRef, useState } from 'react';
+import { lazy, Suspense, useEffect, useRef, useState } from 'react';
import { Box, BoxContents } from 'components/checkoutBox/checkoutBox';
-import DirectDebitForm from 'components/directDebit/directDebitForm/directDebitForm';
import { LoadingOverlay } from 'components/loadingOverlay/loadingOverlay';
import { ContributionsOrderSummary } from 'components/orderSummary/contributionsOrderSummary';
import {
@@ -29,13 +20,9 @@ import {
getTermsStartDateTier3,
} from 'components/orderSummary/contributionsOrderSummaryContainer';
import { DefaultPaymentButton } from 'components/paymentButton/defaultPaymentButton';
-import { paymentMethodData } from 'components/paymentMethodSelector/paymentMethodData';
import { PayPalButton } from 'components/payPalPaymentButton/payPalButton';
import { StateSelect } from 'components/personalDetails/stateSelect';
-import { Recaptcha } from 'components/recaptcha/recaptcha';
-import { SecureTransactionIndicator } from 'components/secureTransactionIndicator/secureTransactionIndicator';
import Signout from 'components/signout/signout';
-import { StripeCardForm } from 'components/stripeCardForm/stripeCardForm';
import { AddressFields } from 'components/subscriptionCheckouts/address/addressFields';
import type { PostcodeFinderResult } from 'components/subscriptionCheckouts/address/postcodeLookup';
import { findAddressesForPostcode } from 'components/subscriptionCheckouts/address/postcodeLookup';
@@ -58,13 +45,10 @@ import type {
} from 'helpers/forms/paymentIntegrations/readerRevenueApis';
import {
DirectDebit,
- isPaymentMethod,
type PaymentMethod as LegacyPaymentMethod,
PayPal,
Stripe,
- toPaymentMethodSwitchNaming,
} from 'helpers/forms/paymentMethods';
-import { isSwitchOn } from 'helpers/globalsAndSwitches/globals';
import type { AppConfig } from 'helpers/globalsAndSwitches/window';
import type { IsoCountry } from 'helpers/internationalisation/country';
import { countryGroups } from 'helpers/internationalisation/countryGroup';
@@ -85,9 +69,7 @@ import {
getSupportAbTests,
} from 'helpers/tracking/acquisitions';
import { trackComponentClick } from 'helpers/tracking/behaviour';
-import { sendEventPaymentMethodSelected } from 'helpers/tracking/quantumMetric';
import { isProd } from 'helpers/urls/url';
-import { logException } from 'helpers/utilities/logger';
import type { GeoId } from 'pages/geoIdConfig';
import { getGeoIdConfig } from 'pages/geoIdConfig';
import { CheckoutDivider } from 'pages/supporter-plus-landing/components/checkoutDivider';
@@ -97,6 +79,7 @@ import {
PaymentTsAndCs,
SummaryTsAndCs,
} from 'pages/supporter-plus-landing/components/paymentTsAndCs';
+import { LoadingDots } from '../../../../stories/animations/LoadingDots.stories';
import {
formatMachineDate,
formatUserDate,
@@ -113,10 +96,7 @@ import {
paypalOneClickCheckout,
setupPayPalPayment,
} from '../checkout/helpers/paypal';
-import {
- stripeCreateSetupIntentPrb,
- stripeCreateSetupIntentRecaptcha,
-} from '../checkout/helpers/stripe';
+import { stripeCreateSetupIntentPrb } from '../checkout/helpers/stripe';
import {
doesNotContainExtendedEmojiOrLeadingSpace,
preventDefaultValidityMessage,
@@ -124,16 +104,11 @@ import {
import { BackButton } from './backButton';
import { CheckoutLayout } from './checkoutLayout';
import { FormSection, Legend, shorterBoxMargin } from './form';
-import {
- checkedRadioLabelColour,
- defaultRadioLabelColour,
- paymentMethodBody,
- PaymentMethodRadio,
- PaymentMethodSelector,
-} from './paymentMethod';
import { retryPaymentStatus } from './retryPaymentStatus';
import { setThankYouOrder, unsetThankYouOrder } from './thankYouComponent';
+const PaymentSection = lazy(() => import('./paymentSection'));
+const ExpressCheckout = lazy(() => import('./expressCheckout'));
/**
* We have not added StripeExpressCheckoutElement to the old PaymentMethod
* as it is heavily coupled through the code base and would require adding
@@ -142,12 +117,6 @@ import { setThankYouOrder, unsetThankYouOrder } from './thankYouComponent';
type PaymentMethod = LegacyPaymentMethod | 'StripeExpressCheckoutElement';
const countriesRequiringBillingState = ['US', 'CA', 'AU'];
-function paymentMethodIsActive(paymentMethod: LegacyPaymentMethod) {
- return isSwitchOn(
- `recurringPaymentMethods.${toPaymentMethodSwitchNaming(paymentMethod)}`,
- );
-}
-
/**
/**
* Attempt to submit a payment to the server. The response will be either `success`, `failure` or `pending`.
@@ -288,18 +257,6 @@ export function CheckoutComponent({
return
Invalid Amount {originalAmount}
;
}
- const validPaymentMethods = [
- /* NOT YET IMPLEMENTED
- countryGroupId === 'EURCountries' && Sepa,
- countryId === 'US' && AmazonPay,
- */
- countryId === 'GB' && DirectDebit,
- Stripe,
- PayPal,
- ]
- .filter(isPaymentMethod)
- .filter(paymentMethodIsActive);
-
const [paymentMethod, setPaymentMethod] = useState();
const [paymentMethodError, setPaymentMethodError] = useState();
@@ -314,8 +271,6 @@ export function CheckoutComponent({
] = useState();
const [stripeExpressCheckoutSuccessful, setStripeExpressCheckoutSuccessful] =
useState(false);
- const [stripeExpressCheckoutReady, setStripeExpressCheckoutReady] =
- useState(false);
useEffect(() => {
if (stripeExpressCheckoutSuccessful) {
formRef.current?.requestSubmit();
@@ -400,22 +355,12 @@ export function CheckoutComponent({
const formRef = useRef(null);
- /** Direct debit details */
- const [accountHolderName, setAccountHolderName] = useState('');
- const [accountNumber, setAccountNumber] = useState('');
- const [sortCode, setSortCode] = useState('');
- const [accountHolderConfirmation, setAccountHolderConfirmation] =
- useState(false);
-
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
/** General error that can occur via fetch validations */
const [errorMessage, setErrorMessage] = useState();
const [errorContext, setErrorContext] = useState();
- const useLinkExpressCheckout =
- abParticipations.linkExpressCheckout === 'variant';
-
const formOnSubmit = async (formData: FormData) => {
setIsProcessingPayment(true);
/**
@@ -777,133 +722,28 @@ export function CheckoutComponent({
{useStripeExpressCheckout && (
-
-
{
- /**
- * This is use to show UI needed besides this Element
- * i.e. The "or" divider
- */
- if (availablePaymentMethods) {
- setStripeExpressCheckoutReady(true);
- }
- }}
- onClick={({ resolve }) => {
- /** @see https://docs.stripe.com/elements/express-checkout-element/accept-a-payment?locale=en-GB#handle-click-event */
- const options = {
- emailRequired: true,
- };
-
- // Track payment method selection with QM
- sendEventPaymentMethodSelected(
- 'StripeExpressCheckoutElement',
- );
-
- resolve(options);
- }}
- onConfirm={async (event) => {
- if (!(stripe && elements)) {
- console.error('Stripe not loaded');
- return;
- }
-
- const { error: submitError } = await elements.submit();
-
- if (submitError) {
- setErrorMessage(submitError.message);
- return;
- }
-
- const name = event.billingDetails?.name ?? '';
-
- /**
- * splits by the last space, and uses the head as firstName
- * and tail as lastName
- */
- const firstName = name
- .substring(0, name.lastIndexOf(' ') + 1)
- .trim();
- const lastName = name
- .substring(name.lastIndexOf(' ') + 1, name.length)
- .trim();
- setFirstName(firstName);
- setLastName(lastName);
-
- event.billingDetails?.address.postal_code &&
- setBillingPostcode(
- event.billingDetails.address.postal_code,
- );
-
- if (
- !event.billingDetails?.address.state &&
- countriesRequiringBillingState.includes(countryId)
- ) {
- logException(
- "Could not find state from Stripe's billingDetails",
- { geoId, countryGroupId, countryId },
- );
- }
- event.billingDetails?.address.state &&
- setBillingState(event.billingDetails.address.state);
-
- event.billingDetails?.email &&
- setEmail(event.billingDetails.email);
-
- setPaymentMethod('StripeExpressCheckoutElement');
- setStripeExpressCheckoutPaymentType(
- event.expressPaymentType,
- );
- /**
- * There is a useEffect that listens to this and submits the form
- * when true
- */
- setStripeExpressCheckoutSuccessful(true);
- }}
- options={{
- paymentMethods: {
- applePay: 'auto',
- googlePay: 'auto',
- link: useLinkExpressCheckout ? 'auto' : 'never',
- },
- }}
+ >}>
+
-
- {stripeExpressCheckoutReady && (
-
- )}
-
+
)}
@@ -1141,143 +981,25 @@ export function CheckoutComponent({
>
)}
-
-
-
-
- {validPaymentMethods.map((validPaymentMethod) => {
- const selected = paymentMethod === validPaymentMethod;
- const { label, icon } = paymentMethodData[validPaymentMethod];
- return (
-
-
-
- {label}
- {icon}
- >
- }
- name="paymentMethod"
- value={validPaymentMethod}
- cssOverrides={
- selected
- ? checkedRadioLabelColour
- : defaultRadioLabelColour
- }
- onChange={() => {
- setPaymentMethod(validPaymentMethod);
- setPaymentMethodError(undefined);
- // Track payment method selection with QM
- sendEventPaymentMethodSelected(validPaymentMethod);
- }}
- />
-
- {validPaymentMethod === 'Stripe' && selected && (
-
-
- {
- // no-op
- }}
- onExpiryChange={() => {
- // no-op
- }}
- onCvcChange={() => {
- // no-op
- }}
- errors={{}}
- recaptcha={
- {
- setStripeClientSecretInProgress(true);
- setRecaptchaToken(token);
- void stripeCreateSetupIntentRecaptcha(
- isTestUser,
- stripePublicKey,
- token,
- ).then((client_secret) => {
- setStripeClientSecret(client_secret);
- setStripeClientSecretInProgress(false);
- });
- }}
- onRecaptchaExpired={() => {
- setRecaptchaToken(undefined);
- }}
- />
- }
- />
-
- )}
-
- {validPaymentMethod === 'DirectDebit' && selected && (
-
- {
- setAccountHolderName(name);
- }}
- updateAccountNumber={(number: string) => {
- setAccountNumber(number);
- }}
- updateSortCode={(sortCode: string) => {
- setSortCode(sortCode);
- }}
- updateAccountHolderConfirmation={(
- confirmation: boolean,
- ) => {
- setAccountHolderConfirmation(confirmation);
- }}
- recaptcha={
- {
- setRecaptchaToken(token);
- }}
- onRecaptchaExpired={() => {
- setRecaptchaToken(undefined);
- }}
- />
- }
- formError={''}
- errors={{}}
- />
-
- )}
-
- );
- })}
-
-
+ }>
+
+
void;
+ stripe: Stripe | null;
+ elements: StripeElements | null;
+ setErrorMessage: (message: string | undefined) => void;
+ setFirstName: (firstName: string) => void;
+ setLastName: (lastName: string) => void;
+ setBillingPostcode: (postcode: string) => void;
+ setBillingState: (state: string) => void;
+ setEmail: (email: string) => void;
+ setPaymentMethod: (
+ paymentMethod: PaymentMethod | 'StripeExpressCheckoutElement',
+ ) => void;
+ setStripeExpressCheckoutSuccessful: (successful: boolean) => void;
+ countryId: IsoCountry;
+ geoId: GeoId;
+ countryGroupId: CountryGroupId;
+};
+
+export default function ExpressCheckout({
+ setStripeExpressCheckoutPaymentType,
+ stripe,
+ elements,
+ setErrorMessage,
+ setFirstName,
+ setLastName,
+ setBillingPostcode,
+ setBillingState,
+ setEmail,
+ setPaymentMethod,
+ setStripeExpressCheckoutSuccessful,
+ countryId,
+ geoId,
+ countryGroupId,
+}: ExpressCheckoutProps) {
+ const [stripeExpressCheckoutReady, setStripeExpressCheckoutReady] =
+ useState(false);
+
+ return (
+
+
{
+ /**
+ * This is use to show UI needed besides this Element
+ * i.e. The "or" divider
+ */
+ if (availablePaymentMethods) {
+ setStripeExpressCheckoutReady(true);
+ }
+ }}
+ onClick={({ resolve }) => {
+ /** @see https://docs.stripe.com/elements/express-checkout-element/accept-a-payment?locale=en-GB#handle-click-event */
+ const options = {
+ emailRequired: true,
+ };
+
+ // Track payment method selection with QM
+ sendEventPaymentMethodSelected('StripeExpressCheckoutElement');
+
+ resolve(options);
+ }}
+ onConfirm={async (event) => {
+ if (!(stripe && elements)) {
+ console.error('Stripe not loaded');
+ return;
+ }
+
+ const { error: submitError } = await elements.submit();
+
+ if (submitError) {
+ setErrorMessage(submitError.message);
+ return;
+ }
+
+ const name = event.billingDetails?.name ?? '';
+
+ /**
+ * splits by the last space, and uses the head as firstName
+ * and tail as lastName
+ */
+ const firstName = name.substring(0, name.lastIndexOf(' ') + 1).trim();
+ const lastName = name
+ .substring(name.lastIndexOf(' ') + 1, name.length)
+ .trim();
+ setFirstName(firstName);
+ setLastName(lastName);
+
+ event.billingDetails?.address.postal_code &&
+ setBillingPostcode(event.billingDetails.address.postal_code);
+
+ if (
+ !event.billingDetails?.address.state &&
+ countriesRequiringBillingState.includes(countryId)
+ ) {
+ logException("Could not find state from Stripe's billingDetails", {
+ geoId,
+ countryGroupId,
+ countryId,
+ });
+ }
+ event.billingDetails?.address.state &&
+ setBillingState(event.billingDetails.address.state);
+
+ event.billingDetails?.email && setEmail(event.billingDetails.email);
+
+ setPaymentMethod('StripeExpressCheckoutElement');
+ setStripeExpressCheckoutPaymentType(event.expressPaymentType);
+ /**
+ * There is a useEffect that listens to this and submits the form
+ * when true
+ */
+ setStripeExpressCheckoutSuccessful(true);
+ }}
+ options={{
+ paymentMethods: {
+ applePay: 'auto',
+ googlePay: 'auto',
+ link: 'never',
+ },
+ }}
+ />
+
+ {stripeExpressCheckoutReady && (
+
+ )}
+
+ );
+}
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/paymentSection.tsx b/support-frontend/assets/pages/[countryGroupId]/components/paymentSection.tsx
new file mode 100644
index 0000000000..d370f11ae3
--- /dev/null
+++ b/support-frontend/assets/pages/[countryGroupId]/components/paymentSection.tsx
@@ -0,0 +1,220 @@
+import { css } from '@emotion/react';
+import { space } from '@guardian/source/foundations';
+import { Radio, RadioGroup } from '@guardian/source/react-components';
+import { useState } from 'react';
+import DirectDebitForm from 'components/directDebit/directDebitForm/directDebitForm';
+import { paymentMethodData } from 'components/paymentMethodSelector/paymentMethodData';
+import { Recaptcha } from 'components/recaptcha/recaptcha';
+import { SecureTransactionIndicator } from 'components/secureTransactionIndicator/secureTransactionIndicator';
+import { StripeCardForm } from 'components/stripeCardForm/stripeCardForm';
+import {
+ DirectDebit,
+ isPaymentMethod,
+ type PaymentMethod,
+ PayPal,
+ Stripe,
+ toPaymentMethodSwitchNaming,
+} from 'helpers/forms/paymentMethods';
+import { isSwitchOn } from 'helpers/globalsAndSwitches/globals';
+import type { IsoCountry } from 'helpers/internationalisation/country';
+import type { CountryGroupId } from 'helpers/internationalisation/countryGroup';
+import { sendEventPaymentMethodSelected } from 'helpers/tracking/quantumMetric';
+import { stripeCreateSetupIntentRecaptcha } from '../checkout/helpers/stripe';
+import { FormSection, Legend } from './form';
+import {
+ checkedRadioLabelColour,
+ defaultRadioLabelColour,
+ paymentMethodBody,
+ PaymentMethodRadio,
+ PaymentMethodSelector,
+} from './paymentMethod';
+
+function paymentMethodIsActive(paymentMethod: PaymentMethod) {
+ return isSwitchOn(
+ `recurringPaymentMethods.${toPaymentMethodSwitchNaming(paymentMethod)}`,
+ );
+}
+
+type PaymentSectionProps = {
+ paymentMethodError: string | undefined;
+ setPaymentMethodError: (error: string | undefined) => void;
+ setStripeClientSecret: (clientSecret: string) => void;
+ setStripeClientSecretInProgress: (inProgress: boolean) => void;
+ recaptchaToken: string | undefined;
+ setRecaptchaToken: (token: string | undefined) => void;
+ paymentMethod: PaymentMethod | 'StripeExpressCheckoutElement' | undefined;
+ setPaymentMethod: (paymentMethod: PaymentMethod) => void;
+ sectionNumber: number;
+ stripePublicKey: string;
+ isTestUser: boolean;
+ countryGroupId: CountryGroupId;
+ countryId: IsoCountry;
+};
+
+export default function PaymentSection({
+ paymentMethodError,
+ setPaymentMethodError,
+ setStripeClientSecret,
+ setStripeClientSecretInProgress,
+ recaptchaToken,
+ setRecaptchaToken,
+ paymentMethod,
+ setPaymentMethod,
+ sectionNumber,
+ stripePublicKey,
+ isTestUser,
+ countryGroupId,
+ countryId,
+}: PaymentSectionProps) {
+ const validPaymentMethods = [
+ /* NOT YET IMPLEMENTED
+ countryGroupId === 'EURCountries' && Sepa,
+ countryId === 'US' && AmazonPay,
+ */
+ countryId === 'GB' && DirectDebit,
+ Stripe,
+ PayPal,
+ ]
+ .filter(isPaymentMethod)
+ .filter(paymentMethodIsActive);
+
+ /** Direct debit details */
+ const [accountHolderName, setAccountHolderName] = useState('');
+ const [accountNumber, setAccountNumber] = useState('');
+ const [sortCode, setSortCode] = useState('');
+ const [accountHolderConfirmation, setAccountHolderConfirmation] =
+ useState(false);
+
+ return (
+
+
+
+
+ {validPaymentMethods.map((validPaymentMethod) => {
+ const selected = paymentMethod === validPaymentMethod;
+ const { label, icon } = paymentMethodData[validPaymentMethod];
+ return (
+
+
+
+ {label}
+ {icon}
+ >
+ }
+ name="paymentMethod"
+ value={validPaymentMethod}
+ cssOverrides={
+ selected ? checkedRadioLabelColour : defaultRadioLabelColour
+ }
+ onChange={() => {
+ setPaymentMethod(validPaymentMethod);
+ setPaymentMethodError(undefined);
+ // Track payment method selection with QM
+ sendEventPaymentMethodSelected(validPaymentMethod);
+ }}
+ />
+
+ {validPaymentMethod === 'Stripe' && selected && (
+
+
+ {
+ // no-op
+ }}
+ onExpiryChange={() => {
+ // no-op
+ }}
+ onCvcChange={() => {
+ // no-op
+ }}
+ errors={{}}
+ recaptcha={
+ {
+ setStripeClientSecretInProgress(true);
+ setRecaptchaToken(token);
+ void stripeCreateSetupIntentRecaptcha(
+ isTestUser,
+ stripePublicKey,
+ token,
+ ).then((client_secret) => {
+ setStripeClientSecret(client_secret);
+ setStripeClientSecretInProgress(false);
+ });
+ }}
+ onRecaptchaExpired={() => {
+ setRecaptchaToken(undefined);
+ }}
+ />
+ }
+ />
+
+ )}
+
+ {validPaymentMethod === 'DirectDebit' && selected && (
+
+ {
+ setAccountHolderName(name);
+ }}
+ updateAccountNumber={(number: string) => {
+ setAccountNumber(number);
+ }}
+ updateSortCode={(sortCode: string) => {
+ setSortCode(sortCode);
+ }}
+ updateAccountHolderConfirmation={(
+ confirmation: boolean,
+ ) => {
+ setAccountHolderConfirmation(confirmation);
+ }}
+ recaptcha={
+ {
+ setRecaptchaToken(token);
+ }}
+ onRecaptchaExpired={() => {
+ setRecaptchaToken(undefined);
+ }}
+ />
+ }
+ formError={''}
+ errors={{}}
+ />
+
+ )}
+
+ );
+ })}
+
+
+ );
+}