import React, { useRef, useState } from "react"; import { CardComponent, CardCVV, CardExpiry, CardNumber } from "@chargebee/chargebee-js-react-wrapper"; import { PaymentElement, useStripe, useElements, Elements } from "@stripe/react-stripe-js"; import { Form, FormGroup, Spinner } from "react-bootstrap"; import { useForm } from "react-hook-form"; import * as Sentry from "@sentry/react"; import { ContinueButton, FormItem } from "../components/atoms"; import { StagerComponent } from "../components/stager"; import { Summary } from "../components/summary"; import { sortedCountries } from "../constants"; import { useCSSStyle } from "../hooks/util"; import ddLogo from "../images/dd_logo_landscape.png"; import { PaymentMethodDDSchema, FormSchema, formatPhoneE164, validate } from "../schema"; import { get as getEnv, getStr as getEnvStr, getPaymentProviders, resolveStripePaymentMethodTypes, PaymentMethod, PaymentProvider } from "../env"; import { loadStripe } from "@stripe/stripe-js"; import type { StripeError } from "@stripe/stripe-js"; import { usePostResource } from "../services/rest-resource.service"; import { SAVED_STATE_KEY } from "../services/router.service"; const STRIPE_CODE_MESSAGES: Partial> = { payment_intent_authentication_failure: "We were unable to authenticate your payment. Please try a different payment method.", payment_intent_timeout: "Payment timed out. Please try again.", }; const TECHNICAL_ERROR_TYPES = new Set([ "api_error", "invalid_request_error", "authentication_error", "rate_limit_error", "idempotency_error", "stripe_invalid_response_error", ]); const GENERIC_PAYMENT_ERROR = "An error occurred processing your payment. Please try again."; function formatStripeError(error: StripeError): string { if (error.code && STRIPE_CODE_MESSAGES[error.code]) { return STRIPE_CODE_MESSAGES[error.code]!; } if (!TECHNICAL_ERROR_TYPES.has(error.type)) { return error.message ?? GENERIC_PAYMENT_ERROR; } return GENERIC_PAYMENT_ERROR; } export const PaymentDetailsPage: StagerComponent = ({ data, setData, onCompleted }) => { const renderForm = () => { // Get first provider that matches selected payment method const paymentProviders = getPaymentProviders(); let paymentProvider: PaymentProvider | null = null; for (const provider of Object.keys(paymentProviders)) { const methods = paymentProviders[provider as PaymentProvider]; if (methods?.includes(data.paymentMethod as PaymentMethod)) { paymentProvider = provider as PaymentProvider; break; } } if (paymentProvider === "stripe") { return ; } if (paymentProvider === "gocardless") { return ; } if (paymentProvider === "chargebee") { return ; } return

Error: no payment providers available. Please contact us.

; }; return renderForm(); }; const GocardlessPaymentPage: StagerComponent = ({ data, onCompleted }) => { const form = useForm({ defaultValues: { ddAccountHolderName: [data.firstName, data.lastName].join(" "), ...data }, resolver: validate(PaymentMethodDDSchema) }); const organisation = getEnv("ORGANISATION_NAME"); return (

Your bank details

{getEnv("IS_UPDATE_FLOW") ? ( {sortedCountries.map((c) => ( ))} ) : null}
Direct Debit logo

The Direct Debit Guarantee

  • The Guarantee is offered by all banks and building societies that accept instructions to pay Direct Debits
  • If there are any changes to the amount, date or frequency of your Direct Debit {organisation} will notify you (normally 3 working days) in advance of your account being debited or as otherwise agreed. If you request {organisation} to collect a payment, confirmation of the amount and date will be given to you at the time of the request
  • If an error is made in the payment of your Direct Debit, by the organisation or your bank or building society, you are entitled to a full and immediate refund of the amount paid from your bank or building society
  • If you receive a refund you are not entitled to, you must pay it back when {organisation} asks you to
  • You can cancel a Direct Debit at any time by simply contacting your bank or building society. Written confirmation may be required. Please also notify the organisation.

Direct debit payments are processed by GoCardless. Read the{" "} GoCardless privacy notice {" "} for more information.

); }; const ChargebeePaymentPage: StagerComponent = ({ onCompleted, data }) => { const organisationName = getEnv("ORGANISATION_NAME"); const cardRef = useRef(); const form = useForm(); const chargebeeStylePropsList = [ "color", "letterSpacing", "textAlign", "textTransform", "textDecoration", "textShadow", "fontFamily", "fontWeight", "fontSize", "fontSmoothing", "fontSmoothing", "fontSmoothing", "fontStyle", "fontVariant" ]; const inputStyle = useCSSStyle( "form-control", "input", chargebeeStylePropsList ); const handleCompleted = async () => { const { token } = await cardRef.current.tokenize(); onCompleted({ paymentToken: token }); }; return (

Card details

You've chosen to join {organisationName} by paying by card.

Card Number

The long number on the front of your card.

This doesn't look like a valid credit or debit card number.
Card Expiry

Should be on the front of your card.

This doesn't look like a valid credit or debit card expiry date. It should a date in the future.
CVV

The three number security code on the back of your card.

This doesn't look like a valid credit or debit card CVV - it's normally three numbers on the back of your card.
); }; const convertCurrencyFromMajorToMinorUnits = (amount: number) => amount * 100; const StripePaymentPage: StagerComponent = ({ data, setData }) => { const stripePromise = loadStripe(getEnv("STRIPE_PUBLISHABLE_KEY") as string); const plan = (getEnv("MEMBERSHIP_PLANS") as any[]).find( (plan) => plan.value === data.membership ); const isOneOffDonation = Boolean(getEnv("DONATION_SUPPORTER_MODE")) && !data.recurDonation; // For one-off donations the amount comes from donationAmount (set by donation.page). // For subscriptions the amount is only used by Stripe Elements for display; the // actual charge is determined server-side from the plan / customMembershipAmount. const amountForElements = isOneOffDonation ? convertCurrencyFromMajorToMinorUnits(data.donationAmount as number) : (plan.amount ? convertCurrencyFromMajorToMinorUnits(plan.amount) : 100); const currency = plan.currency.toLowerCase() || "gbp"; const paymentMethodTypes = resolveStripePaymentMethodTypes(isOneOffDonation, currency); return ( ); }; const StripeForm = ({ data, setData, plan, isOneOffDonation }: { data: FormSchema; setData: (d: FormSchema) => void; plan: { frequency: string }; isOneOffDonation: boolean; }) => { const stripe = useStripe(); const elements = useElements(); const createSubscription = usePostResource< FormSchema, { id: string; customer: string; latest_invoice: { payment_intent: { id: string; client_secret: string } }; } >("/stripe/create-subscription"); const createPaymentIntent = usePostResource< FormSchema, { id: string; client_secret: string; customer: string } >("/stripe/create-payment-intent"); const [errorMessage, setErrorMessage] = useState(); const [errorDetails, setErrorDetails] = useState(); const [showErrorDetails, setShowErrorDetails] = useState(false); const [loading, setLoading] = useState(false); // Use ref for paymentMethodOrder so that it is fixed when the component is first rendered // Otherwise the Stripe form switches the order whenever the payment method is changed const paymentMethodOrder = useRef(data.paymentMethod === "directDebit" ? ["bacs_debit", "card"] : ["card", "bacs_debit"]); const handleError = (error: { message: string }) => { setLoading(false); setErrorMessage(error.message); Sentry.captureMessage(error.message, 'error') }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) { return; } setLoading(true); const { error: submitError } = await elements.submit(); if (submitError) { handleError(submitError as { message: string }); return; } try { let clientSecret: string; if (isOneOffDonation) { const paymentIntent = await createPaymentIntent({ ...data }); clientSecret = paymentIntent.client_secret; sessionStorage.setItem( SAVED_STATE_KEY, JSON.stringify({ ...data, stripeCustomerId: paymentIntent.customer, stripePaymentIntentId: paymentIntent.id }) ); } else { const subscription = await createSubscription({ ...data }); clientSecret = subscription.latest_invoice.payment_intent.client_secret; sessionStorage.setItem( SAVED_STATE_KEY, JSON.stringify({ ...data, stripeCustomerId: subscription.customer, stripeSubscriptionId: subscription.id, stripePaymentIntentId: subscription.latest_invoice.payment_intent.id }) ); } const returnUrl = new URL(window.location.href); returnUrl.searchParams.set("stripe_success", "true"); const { error } = await stripe.confirmPayment({ elements, clientSecret, confirmParams: { return_url: returnUrl.toString() } }); if (error) { Sentry.captureMessage("Stripe confirmPayment error", { level: "error", extra: { stripeError: error }, }); setLoading(false); setErrorMessage(formatStripeError(error)); setErrorDetails(JSON.stringify(error, null, 2)); setShowErrorDetails(false); return; } } catch (e: any) { console.error("Create payment error", e); Sentry.captureException(e); setLoading(false); setErrorMessage(GENERIC_PAYMENT_ERROR); setErrorDetails(JSON.stringify(e, null, 2)); setShowErrorDetails(false); } }; // Pre-fill billing details from the form data // This allows users to verify their details are correct when setting up Direct Debit const defaultValues = { billingDetails: { name: data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : undefined, email: data.email || undefined, phone: formatPhoneE164(data.phoneNumber, data.phoneCountry), address: { line1: data.addressLine1 || undefined, line2: data.addressLine2 || undefined, city: data.addressCity || undefined, postal_code: data.addressPostcode || undefined, country: data.addressCountry || undefined, } } }; return (
{ const stripePaymentMethod = e.value.type; setData({ ...data, paymentMethod: stripePaymentMethod === "bacs_debit" ? "directDebit" : "creditCard" }); }}/> {errorMessage && (
{errorMessage} {errorDetails && ( <> {" "} {showErrorDetails && (
                    {errorDetails}
                  
)} )}
)}
); };