(\n donationDataFromStorage\n ? JSON.parse(donationDataFromStorage)\n : initialFormValues\n );\n const donationDataMemoized = useMemo(\n () => ({ donationData, setDonationData }),\n [donationData]\n );\n const [hasMounted, setHasMounted] = useState(false);\n const [scriptsLoaded, setScriptsLoaded] = useState(false);\n const [currentStep, setCurrentStep] = useState(0);\n const formAdmin = getFormAdmin(formName);\n const brand = getFormBrand(formName);\n const introBannerConfig = getFormComponents(formName).introBanner;\n const headerConfig = getFormComponents(formName).header;\n const progressStepsDetails = {\n routes: [\"Your donation\", \"Details\", \"Payment\", \"Thanks\"],\n steps: [\"Donation\", \"Details\", \"Payment\", \"Thank you\"],\n };\n const { steps, routes } = progressStepsDetails;\n const isErrorPage = [\"error\"].indexOf(page) > -1;\n const donationAmount = getDonationAmount(donationData);\n if (typeof window !== \"undefined\" && !donationDataFromStorage) {\n sessionStorage.setItem(\n `formData-${formName}`,\n JSON.stringify(initialFormValues)\n );\n }\n\n useEffect(() => {\n const validFormVersions = getValidFormVersions(formAdmin);\n const searchParams = new URLSearchParams(window.location.search);\n const formTypeQueryString =\n typeof window !== \"undefined\" ? searchParams.get(\"type\") : \"\";\n const frequencyQueryString =\n typeof window !== \"undefined\" ? searchParams.get(\"frequency\") : \"\";\n const storedFormVersion =\n typeof window !== \"undefined\"\n ? sessionStorage.getItem(`formVersion-${formName}`)\n : \"\";\n\n if (validFormVersions.length === 0 && typeof window !== \"undefined\") {\n // No valid source/product codes in config, redirect user\n datadogRum.addError(\"Form config error - no valid form versions\");\n window.location.replace(\"http://www.cancerresearchuk.org\");\n }\n\n if (storedFormVersion && !formTypeQueryString && !frequencyQueryString) {\n setFormVersion(storedFormVersion);\n } else {\n if (formTypeQueryString === \"regular\" || frequencyQueryString) {\n validFormVersions.includes(\"regular\")\n ? setFormVersion(\"regular\")\n : setFormVersion(\"single\");\n } else {\n validFormVersions.includes(\"single\")\n ? setFormVersion(\"single\")\n : setFormVersion(\"regular\");\n }\n sessionStorage.setItem(`formVersion-${formName}`, formVersion);\n }\n\n idleTimer(brand);\n setHasMounted(true);\n }, []);\n\n useEffect(() => {\n if (typeof window !== \"undefined\") {\n if (sessionStorage.getItem(`formVersion-${formName}`) !== formVersion) {\n formVersion\n ? sessionStorage.setItem(`formVersion-${formName}`, formVersion)\n : sessionStorage.removeItem(`formVersion-${formName}`);\n }\n }\n }, [formVersion]);\n\n useEffect(() => {\n const progressIndex = routes.indexOf(page);\n const stepValue = progressIndex + 1;\n\n progressIndex > -1 && setCurrentStep(stepValue);\n }, [page, routes]);\n\n useEffect(() => {\n if (!scriptsLoaded && currentStep) {\n let checkoutDonationAmount;\n if (donationAmount && Number(donationAmount)) {\n checkoutDonationAmount = formatTwoDecimals(Number(donationAmount));\n } else {\n checkoutDonationAmount = 0;\n }\n const checkoutDonationAmountWithGA = donationData.giftAid\n ? formatTwoDecimals(\n Number((Math.round(Number(donationAmount) * 100) / 100) * 1.25)\n )\n : formatTwoDecimals(Number(donationAmount));\n const productSource =\n formVersion === \"regular\"\n ? formAdmin.source.regular\n : donationData.typeRadioGroup === \"yes\"\n ? formAdmin.source.single\n : formAdmin.source.collection;\n\n let dataLayerProducts = [] as CheckoutProductsType[];\n if (currentStep === 1) {\n dataLayerProducts = [\n {\n id: formVersion === \"single\" ? \"Single Donation\" : \"donate_regular\",\n name:\n formVersion === \"single\" ? \"single donation\" : \"regular donation\",\n category: \"donation\",\n brand: getFormBrand(formName),\n donation_event_code: formAdmin.formID,\n },\n ];\n } else {\n dataLayerProducts = [\n {\n id: formVersion === \"single\" ? \"Single Donation\" : \"donate_regular\",\n name:\n formVersion === \"single\" ? \"single donation\" : \"regular donation\",\n category: \"donation\",\n variant: formVersion === \"regular\" ? donationData.frequency : \"\",\n brand: getFormBrand(formName),\n price: Number(donationAmount) || 0,\n quantity: 1,\n donation_event_code: formAdmin.formID,\n },\n ];\n }\n\n setCookieAccepted(\n brand,\n page,\n checkoutDonationAmount,\n checkoutDonationAmountWithGA,\n currentStep,\n dataLayerProducts,\n transactionId,\n paymentMethod,\n productSource,\n donationData.restriction\n );\n\n const isSinglePaymentPage =\n typeof window !== \"undefined\" &&\n !window.location.href.includes(\"regular-payment\");\n setReCAPTCHAVisibility(\n page === \"Payment\" && isSinglePaymentPage ? \"visible\" : \"hidden\"\n );\n setDatadog();\n setCookieBanner();\n setScriptsLoaded(true);\n }\n if (isErrorPage) {\n setCookieBanner();\n setCookieAccepted(brand, \"404\");\n setScriptsLoaded(true);\n }\n }, [currentStep]);\n\n if (!hasMounted) {\n return null;\n }\n\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n {/* TODO Remove this after Condensed Amount View Experiment */}\n {formName === \"support-us\" ? (\n <>\n {!isErrorPage && (\n \n )}\n {currentStep === 3 && (\n \n )}\n >\n ) : (\n <>\n {page !== \"Thanks\" && (\n \n )}\n {!isErrorPage && (\n \n )}\n {currentStep === 3 && (\n \n )}\n >\n )}\n {children}\n \n \n \n \n \n \n \n \n \n \n );\n};\n\nexport default DonationFormWrapper;\n","import { ReactElement } from \"react\";\nimport { Link } from \"@cruk/cruk-react-components\";\nimport PhoneNumber from \"./PhoneNumber\";\nimport {\n isEmail,\n isUrl,\n isPhoneNumber,\n isTemplate,\n splitIntoFragments,\n stripTemplating,\n} from \"../helpers/functions/shared\";\nimport Email from \"./Email\";\n\ntype CopyParagraphProps = {\n children: string;\n};\n\n/**\n * The current implementation expects the string with the following template:\n * 1. Plain string -> This will be rendered within the paragraph tag\n * 2. Templated string -> This will replace the templated string with a phone number or email.\n *\n * The template string syntax is as follows: \"This is phone number [1234 5678] and this is an email [email]\".\n * The function `splitIntoFragments` preserves the templating square brackets, so it still needs to be stripped creating a PhoneNumber or Email fragment.\n */\nconst IntroBannerCopyParagraph = ({\n children,\n}: CopyParagraphProps): ReactElement => (\n \n {splitIntoFragments(children)\n .map((fragment) => {\n if (isTemplate(fragment)) {\n if (isPhoneNumber(fragment)) {\n return {stripTemplating(fragment)};\n }\n\n if (isEmail(fragment)) {\n return {stripTemplating(fragment)};\n }\n\n if (isUrl(fragment)) {\n return (\n \n {stripTemplating(fragment)}\n \n );\n }\n }\n\n return fragment;\n })\n .reduce((acc, fragment) => (\n <>\n {acc}\n {fragment}\n >\n ))}\n
\n);\n\nexport default IntroBannerCopyParagraph;\n","import { ReactElement } from \"react\";\n\ntype IntroHeaderProps = {\n children: string;\n};\n\nconst IntroHeader = ({ children }: IntroHeaderProps): ReactElement => (\n <>\n {children\n .split(\"\\n\")\n .filter((str) => str.trim() !== \"\")\n .map((fragment, i) => (\n {fragment.trim()}\n ))}\n >\n);\n\nexport default IntroHeader;\n","import React, { useContext, ReactElement, HTMLAttributes } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { ThemeType } from \"@cruk/cruk-react-components\";\nimport ThemeNameContext from \"../../contexts/ThemeNameContext\";\nimport { IntroBannerType } from \"../../configs/single-donation\";\nimport IntroBannerCopyParagraph from \"../IntroBannerCopyParagraph\";\nimport IntroHeader from \"../IntroHeader\";\n\ntype IntroDivProps = HTMLAttributes & {\n formTheme: string;\n theme?: ThemeType;\n};\n\ntype IntroTextProps = HTMLAttributes & {\n formTheme: string;\n theme?: ThemeType;\n};\n\nconst IntroBannerStyled = styled.div`\n ${({ theme, formTheme }: IntroDivProps) =>\n (formTheme === \"SU2C\" &&\n css`\n background-color: #ff8e00;\n `) ||\n css`\n background-color: ${theme.tokenColors.grey_200};\n `}\n`;\n\nconst IntroBannerContainer = styled.div`\n max-width: ${({ theme }) => theme.utilities.contentMaxWidth};\n margin: 0 auto;\n padding: ${({ theme, formTheme }: IntroTextProps) =>\n formTheme === \"SU2C\"\n ? `${theme.spacing.xxs} ${theme.spacing.s} ${theme.spacing.xs}`\n : theme.spacing.s};\n\n p {\n text-align: ${({ formTheme }: IntroTextProps) =>\n formTheme === \"SU2C\" ? \"left\" : \"center\"};\n }\n\n @media (min-width: ${({ theme }) => theme.breakpoint.tablet}) {\n padding: ${({ theme, formTheme }: IntroTextProps) =>\n formTheme === \"SU2C\"\n ? `${theme.spacing.xs} ${theme.spacing.s} ${theme.spacing.l}`\n : theme.spacing.s};\n }\n`;\n\nconst IntroText = styled.p`\n ${({ theme, formTheme }: IntroTextProps) =>\n (formTheme === \"SU2C\" &&\n css`\n display: table;\n color: ${theme.colors.textLight};\n text-transform: uppercase;\n font-family: ${theme.typography.fontFamilyHeadings};\n line-height: 1.2;\n margin-bottom: ${theme.spacing.xs};\n font-size: 1.2rem;\n\n @media (min-width: 405px) {\n font-size: 1.6rem;\n }\n\n @media (min-width: ${theme.breakpoint.tablet}) {\n font-size: 2.4rem;\n }\n\n @media (min-width: ${theme.breakpoint.desktop}) {\n font-size: 3.2rem;\n }\n `) ||\n css`\n color: ${theme.colors.textDark};\n font-family: ${theme.typography.fontFamilyHeadings};\n font-size: ${theme.fontSizes.xl};\n font-weight: normal;\n margin: 0 0 ${theme.spacing.xs};\n line-height: 1.5;\n `}\n\n span {\n ${({ theme, formTheme }: IntroTextProps) =>\n formTheme === \"SU2C\" &&\n css`\n display: table;\n background-color: ${theme.colors.tertiary};\n padding: 0 ${theme.spacing.xxs};\n\n @media (min-width: ${theme.breakpoint.tablet}) {\n &:first-child {\n margin-left: -${theme.spacing.xxs};\n }\n\n &:last-child {\n margin-left: ${theme.spacing.s};\n }\n }\n `}\n }\n`;\n\nconst IntroBanner = ({\n header,\n copy,\n giftaid,\n}: IntroBannerType): ReactElement => {\n const formTheme = useContext(ThemeNameContext);\n let defaultHeaderCopy;\n if (giftaid) {\n defaultHeaderCopy =\n formTheme === \"SU2C\"\n ? \"Thank you for your donation\\n to Stand Up To Cancer\"\n : \"Thank you for your donation to Cancer Research UK\";\n } else {\n defaultHeaderCopy =\n formTheme === \"SU2C\"\n ? \"You are doing\\n something incredible.\"\n : \"You are doing something amazing.\";\n }\n const defaultBodyCopy =\n formTheme === \"SU2C\"\n ? \"Your donation speeds up life-saving cancer research.\"\n : \"Your donation will drive life-saving research.\";\n\n return (\n \n \n \n {header || defaultHeaderCopy}\n \n {!giftaid ? (\n \n {copy || defaultBodyCopy}\n \n ) : (\n \"\"\n )}\n \n \n );\n};\n\nexport default IntroBanner;\n","import { createContext, Dispatch, SetStateAction } from \"react\";\n\nconst initialValue: {\n transactionStatus: string | null;\n setTransactionStatus: Dispatch>;\n} = {\n transactionStatus: null,\n setTransactionStatus() {},\n};\n\nconst TransactionContext = createContext(initialValue);\n\nexport default TransactionContext;\n","import { createCipheriv } from \"crypto\";\nimport { NextRouter } from \"next/router\";\nimport { datadogRum } from \"@datadog/browser-rum\";\nimport {\n setErrorMessage,\n displayModal,\n} from \"../../payment-fields/hooks/braintreeClient\";\n// CONFIG\nimport { AdminType } from \"../../configs/single-donation\";\nimport { FormSchemaType } from \"../../schema/single-donation\";\n\nexport const getFailCallback = (\n formName: string,\n pushError: any,\n setSubmitting: any,\n setSubmitDisabled: any\n): ((data: any) => void) => {\n const failCallback = (data: any) => {\n if (typeof window !== \"undefined\") {\n sessionStorage.removeItem(`transactionAttempted-${formName}`);\n }\n setSubmitting(false);\n setSubmitDisabled(false);\n displayModal(false, false);\n pushError(\n data.error.code ? data.error.code : \"\",\n \"donation\",\n data.error.message\n );\n console.error(data.error);\n setErrorMessage(data.error.message);\n };\n return failCallback;\n};\n\nexport const getSuccessCallback = (\n formName: string,\n router: NextRouter\n): ((data: any) => Promise) => {\n const successCallback = async (data: any): Promise => {\n datadogRum.setUserProperty(\"transactionId\", data.id);\n if (typeof window !== \"undefined\") {\n sessionStorage.setItem(`transactionId-${formName}`, data.id);\n sessionStorage.setItem(`paymentMethod-${formName}`, data.paymentMethod);\n sessionStorage.removeItem(`transactionAttempted-${formName}`);\n }\n await router.push(`/${formName}/thanks`);\n };\n return successCallback;\n};\n\nexport const getValidFormVersions = (formAdmin: AdminType): string[] => {\n // Check which versions of form there are (single/regular)\n const validFormVersions = [];\n if (\n formAdmin?.product?.single &&\n formAdmin?.source?.single &&\n formAdmin?.product?.collection &&\n formAdmin?.source?.collection &&\n formAdmin?.emailType?.single\n ) {\n validFormVersions.push(\"single\");\n }\n if (\n formAdmin?.product?.regular &&\n formAdmin?.source?.regular &&\n formAdmin?.emailType?.regular\n ) {\n validFormVersions.push(\"regular\");\n }\n return validFormVersions;\n};\n\nexport const getDonationAmount = (donationData?: FormSchemaType): string =>\n donationData?.donationAmount.otherAmount\n ? Number(donationData.donationAmount.otherAmount).toFixed(2)\n : donationData?.donationAmount.amountRadioGroup\n ? Number(donationData.donationAmount.amountRadioGroup).toFixed(2)\n : \"0.00\";\n\nexport const getResearchAim = (\n researchArea: Record\n): string => {\n let outputString;\n switch (researchArea.value) {\n case \"RES769\":\n outputString = \"in diagnosing cancer early\";\n break;\n case \"RES828\":\n outputString = \"in developing better treatments sooner\";\n break;\n case \"RES813\":\n outputString = \"in preventing cancer sooner\";\n break;\n default:\n outputString = `to beat ${researchArea.label\n .toString()\n .toLowerCase()} sooner`;\n break;\n }\n\n return outputString;\n};\n\nexport const encrypt = (key: string, message: string, id: string): string => {\n const iv = Buffer.from(id, \"utf8\").toString(\"hex\").slice(0, 16);\n const cypher = createCipheriv(\"aes-256-cbc\", key, iv);\n\n const encryptedMsg =\n cypher.update(message, \"utf8\", \"hex\") + cypher.final(\"hex\");\n\n return encryptedMsg;\n};\n","import { nanoid } from \"nanoid\";\nimport { datadogRum } from \"@datadog/browser-rum\";\nimport {\n clearTransactionAttemptCache,\n clickableTelNum,\n isProductJourney,\n setTransactionAttemptSellerReference,\n} from \"../../helpers/functions/shared\";\nimport { PAYMENT_API_GATEWAY_URL } from \"../../helpers/functions/shared\";\n\nexport type TokenizationData = {\n token: string;\n ipAddress: string;\n};\n\nexport async function fetchTokenizationData(\n merchantAccountId: string,\n pushError: (\n errorCode: string,\n errorType: string,\n errorMessage: string\n ) => void\n): Promise {\n try {\n const tokenizationResponse = await fetch(\n `${PAYMENT_API_GATEWAY_URL}tokenization`,\n {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n merchantAccountId: merchantAccountId,\n }),\n }\n );\n\n const { data: tokenizationData } = await tokenizationResponse.json();\n return {\n token: tokenizationData.createClientToken.clientToken,\n ipAddress: tokenizationData.ipAddress,\n };\n } catch (err: any) {\n if (err instanceof Error) {\n pushError(\"\", \"donation\", err.message);\n console.error(err);\n }\n }\n}\n\nexport const submitTransaction = async (\n nonce: string,\n donationAmount: string,\n merchantAccountId: string,\n metaData: any,\n reCaptchaId: string,\n getReCaptchaToken: any,\n failCallback: any,\n successCallback: any,\n formName: string\n): Promise => {\n const reCaptchaToken = await getReCaptchaToken();\n const correlationId = nanoid(10);\n let errorCode = \"\";\n\n if (isProductJourney(merchantAccountId)) {\n // TODO Check merchantID for donations; correlationID cache\n setTransactionAttemptSellerReference(metaData.sellerReference);\n } else {\n // transaction attempted for single donation\n sessionStorage.setItem(`transactionAttempted-${formName}`, \"true\");\n }\n\n fetch(`${PAYMENT_API_GATEWAY_URL}transaction`, {\n body: JSON.stringify({\n amount: donationAmount,\n correlationId: correlationId,\n merchantAccountId: merchantAccountId,\n metadata: metaData,\n transaction: nonce,\n recaptcha: {\n action: \"pws_donation\",\n token: reCaptchaToken,\n siteKey: reCaptchaId,\n },\n }),\n method: \"POST\",\n })\n .then((response) => {\n if (!response.ok) {\n errorCode = response.status.toString();\n return null;\n } else {\n return response.json();\n }\n })\n .then((data) => {\n clearTransactionAttemptCache();\n if (data) {\n if (data.error) {\n failCallback(data);\n } else {\n successCallback(data);\n }\n } else {\n const data = {\n error: {\n code: errorCode,\n message:\n \"Sorry but we can't process your payment with this card. Please try again with another payment method or call us on 0300 123 1022 to make a payment.\",\n },\n };\n failCallback(data);\n }\n })\n .catch((error) => {\n console.error(error);\n });\n};\n\nexport const setErrorMessage = (\n message?: string,\n container?: HTMLDivElement\n): void => {\n const errorContainer =\n container ||\n document.querySelector(\"[data-response-type='braintreeResponseOutput']\");\n if (errorContainer) {\n if (message && message !== \"\") {\n errorContainer.innerHTML = `${message.replace(\n \"0300 123 1022\",\n clickableTelNum(\"0300 123 1022\")\n )}
`;\n } else {\n errorContainer.innerHTML = \"\";\n }\n if (errorContainer.offsetWidth > 0 && errorContainer.offsetHeight > 0)\n errorContainer.scrollIntoView();\n }\n};\n\nexport const displayModal = (\n showModal: boolean,\n showSpinner: boolean\n): void => {\n const loader = document.getElementById(\"loader\");\n const spin = document.getElementById(\"spin\");\n if (loader) loader.style.display = showModal ? \"block\" : \"none\";\n if (spin) spin.style.display = showSpinner ? \"block\" : \"none\";\n setErrorMessage();\n};\n\nexport const validRequiredFormData = (isValid: boolean): void => {\n const errorMsg =\n \"Please ensure all required form fields have been filled correctly.\";\n if (!isValid) {\n setErrorMessage(errorMsg);\n datadogRum.addError(\"Form not filled\", { errorType: \"validation\" });\n } else {\n setErrorMessage();\n }\n};\n"],"names":["mailto","children","href","PaymentSummaryStyled","styled","theme","spacing","s","xs","tokenColors","grey_200","PaymentSummaryContainer","utilities","contentMaxWidth","PaymentSummaryContent","breakpoint","tablet","PaymentAmount","CartDataList","cartInfo","type","frequency","amount","cartData","cartDataItems","items","cartDataTotal","total","map","item","i","label","toLowerCase","includes","value","h2","marginBottom","SiteContainer","page","paymentMethod","transactionId","formName","useContext","FormNameContext","useState","transactionStatus","setTransactionStatus","transactionStatusMemoized","useMemo","formVersion","setFormVersion","formVersionMemoized","donationDataFromStorage","sessionStorage","getItem","JSON","parse","initialFormValues","donationData","setDonationData","donationDataMemoized","hasMounted","setHasMounted","scriptsLoaded","setScriptsLoaded","currentStep","setCurrentStep","formAdmin","getFormAdmin","brand","getFormBrand","introBannerConfig","getFormComponents","introBanner","headerConfig","header","steps","routes","isErrorPage","indexOf","donationAmount","getDonationAmount","setItem","stringify","useEffect","validFormVersions","getValidFormVersions","searchParams","URLSearchParams","window","location","search","formTypeQueryString","get","frequencyQueryString","storedFormVersion","length","datadogRum","replace","idleTimer","removeItem","progressIndex","checkoutDonationAmount","Number","formatTwoDecimals","checkoutDonationAmountWithGA","giftAid","Math","round","productSource","source","regular","typeRadioGroup","single","collection","dataLayerProducts","id","name","category","donation_event_code","formID","variant","price","quantity","setCookieAccepted","restriction","isSinglePaymentPage","setReCAPTCHAVisibility","setDatadog","setCookieBanner","getFormTheme","getFormThemeName","formTheme","title","optimizelyId","logoAltText","logoAlt","logoImageSrc","logoSrc","logoLinkTitle","logoTitle","logoLinkUrl","logoUrl","siteSlogan","current","copy","splitIntoFragments","fragment","isTemplate","isPhoneNumber","PhoneNumber","stripTemplating","isEmail","Email","isUrl","Link","target","reduce","acc","split","filter","str","trim","IntroBannerStyled","css","IntroBannerContainer","xxs","l","IntroText","colors","textLight","typography","fontFamilyHeadings","desktop","textDark","fontSizes","xl","tertiary","defaultHeaderCopy","giftaid","ThemeNameContext","defaultBodyCopy","initialValue","TransactionContext","createContext","getFailCallback","pushError","setSubmitting","setSubmitDisabled","data","displayModal","error","code","message","console","setErrorMessage","getSuccessCallback","router","successCallback","push","product","emailType","otherAmount","toFixed","amountRadioGroup","getResearchAim","researchArea","outputString","toString","encrypt","key","iv","Buffer","from","slice","cypher","createCipheriv","update","fetchTokenizationData","merchantAccountId","fetch","PAYMENT_API_GATEWAY_URL","method","headers","Accept","body","tokenizationResponse","json","tokenizationData","token","createClientToken","clientToken","ipAddress","Error","submitTransaction","nonce","metaData","reCaptchaId","getReCaptchaToken","failCallback","reCaptchaToken","correlationId","nanoid","errorCode","isProductJourney","setTransactionAttemptSellerReference","sellerReference","metadata","transaction","recaptcha","action","siteKey","then","response","ok","status","clearTransactionAttemptCache","container","errorContainer","document","querySelector","innerHTML","clickableTelNum","offsetWidth","offsetHeight","scrollIntoView","showModal","showSpinner","loader","getElementById","spin","style","display","validRequiredFormData","isValid","errorType"],"sourceRoot":""}