Repository: guardian/support-frontend
Pull request #7009: create AB test Logic on one time Contribution checkout for Soft Opt-In
Author: login: maloky
Labels: name: Seen-on-PROD
Created: 2025-05-08T13:42:23Z | Merged: 2025-05-15T13:56:30Z

<!-- all sections optional, delete any you don't need -->
## What are you doing in this PR?

1. Create a new ab test participation to track soft opt-in (SOI) consent on the on time contribution checkout.
2. Add backend support for `similarProductsConsent` flag for both Stripe and Paypal payment methods to the Acquisition Event Bus.
**Stripe**:  Pass the value directly on the request payload for the payment-api
**PayPal**:  Set the value as cookie so it can be passed by the return endpoint after PayPal redirect. 
3. Refactor Implementation for getting the `similarProductsConsent` value from the Form.

Refactor main points:
- use a hidden input element to track consent value when displayed
- remove unneeded prop that evaluates component visibility since the input will provide us that value
- move getConsentValue to own module, and use hidden input value as flag.
  
<!--
This pr adds a widget to the doogle so that we can buy a sub from any page in one click
For detailed changes see the inline self-review comments.
-->

[**Trello Card**](https://trello.com/c/nez975uB/1576-build-a-b-c-test-on-adding-soft-opt-in-tick-box-to-one-time-journey-checkout)

## Why are you doing this?
We are rolling out soft in consent to one time contribution checkout page.
<!--
Remember, PRs are documentation for future contributors.
-->

## How to test

### Locally
| SimilarProductsConsent | OneTimeContributionConsent |
| -- | -- | 
| [Control](https://support.thegulocal.com/uk/checkout?product=SupporterPlus&ratePlan=Monthly#ab-similarProductsConsent=Control) | [Control](https://support.thegulocal.com/uk/one-time-checkout#ab-oneTimeContributionConsent=Control) | 
| [VariantA](https://support.thegulocal.com/uk/checkout?product=SupporterPlus&ratePlan=Monthly#ab-similarProductsConsent=VariantA) | [VariantA](https://support.thegulocal.com/uk/one-time-checkout#ab-oneTimeContributionConsent=VariantA) | 
| [VariantB](https://support.thegulocal.com/uk/checkout?product=SupporterPlus&ratePlan=Monthly#ab-similarProductsConsent=VariantB) | [VariantB](https://support.thegulocal.com/uk/one-time-checkout#ab-oneTimeContributionConsent=VariantB) | 
### Similar Products

1. Open the browser dev tools in the network tab and filter for `create`.
2. On the payload tab for the request check the sent object.

![Screenshot 2025-05-08 at 12 59 38](https://github.com/user-attachments/assets/e416b033-e330-45f7-becd-67bce5eadfb4)

### One Time Contribution

1. Open the browser dev tools in the network tab and filter for `create-payment`.
2. On the payload tab for the request check the sent object.

![Screenshot 2025-05-08 at 16 42 57](https://github.com/user-attachments/assets/f7544934-fddb-4c1a-9cb8-b16ab6312bed)

<!-- Provide instructions to help others verify the change. This could take the form of "On PROD, do X and witness Y. On this branch, do X and witness Z. " -->

## How can we measure success?

<!-- Do you expect errors to decrease? Do you expect user journeys to be simplified? What can be used to prove this? A filtered view of logs or analytics, etc? -->

## Have we considered potential risks?

<!-- What are the potential risks and how can they be mitigated? Does an error require an alarm? Should user help, infosec, or legal be informed of this change? Is private information guarded? Do we need to add anything in the backlog? -->

## Accessibility test checklist

-  [ ] [Tested with screen reader](https://github.com/guardian/accessibility/blob/main/people-and-technology/03-visual.md#screen-reader)
-  [ ] [Navigable with keyboard](https://github.com/guardian/accessibility/blob/main/people-and-technology/02-physical.md#keyboard)
-  [ ] [Colour contrast passed](https://github.com/guardian/accessibility/blob/main/people-and-technology/03-visual.md#contrast)
-  [ ] [The change doesn't use only colour to convey meaning](https://github.com/guardian/accessibility/blob/main/people-and-technology/03-visual.md#use-of-colour)

## Screenshots
| Control | Variant A | Variant B |
| ---- | ---- | ---- |
| ![support thegulocal com_uk_one-time-checkout](https://github.com/user-attachments/assets/08bda67d-69fb-46a7-8ee7-c5b205ae5eec) | ![support thegulocal com_uk_one-time-checkout (1)](https://github.com/user-attachments/assets/d945fcf2-f8fc-444a-98bf-ebf7fc89b3e3) | ![support thegulocal com_uk_one-time-checkout (2)](https://github.com/user-attachments/assets/9fef630c-a6d5-4923-bca0-c8fd0b760aef) |
 




Diff:
diff --git a/support-frontend/app/controllers/PayPalOneOff.scala b/support-frontend/app/controllers/PayPalOneOff.scala
index 576208544a..15b9705e52 100644
--- a/support-frontend/app/controllers/PayPalOneOff.scala
+++ b/support-frontend/app/controllers/PayPalOneOff.scala
@@ -102,11 +102,15 @@ class PayPalOneOff(
         "payerId" -> PayerID,
       )
 
+      val similarProductsConsentCookie = request.cookies.get("gu_similar_products_consent")
+
+      val similarProductsConsent = similarProductsConsentCookie.flatMap(_.value.toBooleanOption)
+
       val isTestUser = testUsers.isTestUser(request)
       val userAgent = request.headers.get("user-agent")
 
       paymentAPIService
-        .executePaypalPayment(paymentJSON, acquisitionData, email, isTestUser, userAgent)
+        .executePaypalPayment(paymentJSON, acquisitionData, email, isTestUser, userAgent, similarProductsConsent)
         .fold(resultFromPaymentAPIError, success => resultFromPaypalSuccess(success, country, thankyou))
     }
 }
diff --git a/support-frontend/app/controllers/TestUsersManagement.scala b/support-frontend/app/controllers/TestUsersManagement.scala
index 1532091327..34f3cf60a7 100644
--- a/support-frontend/app/controllers/TestUsersManagement.scala
+++ b/support-frontend/app/controllers/TestUsersManagement.scala
@@ -28,7 +28,7 @@ class TestUsersManagement(
         Cookie(
           "_test_username",
           testUser.token,
-          maxAge = Some(2 * 24 * 60 * 60),// in seconds
+          maxAge = Some(2 * 24 * 60 * 60), // in seconds
           httpOnly = false,
           domain = Some(cookieDomain.value),
         ),
diff --git a/support-frontend/app/services/PaymentAPIService.scala b/support-frontend/app/services/PaymentAPIService.scala
index 28d675df7f..deb810c900 100644
--- a/support-frontend/app/services/PaymentAPIService.scala
+++ b/support-frontend/app/services/PaymentAPIService.scala
@@ -41,6 +41,7 @@ case class ExecutePaymentBody(
     email: String,
     acquisitionData: JsValue,
     paymentData: JsObject,
+    similarProductsConsent: Option[Boolean],
 )
 
 object ExecutePaymentBody {
@@ -98,8 +99,9 @@ class PaymentAPIService(wsClient: WSClient, val paymentAPIUrl: String)(implicit
       email: String,
       isTestUser: Boolean,
       userAgent: Option[String],
+      similarProductsConsent: Option[Boolean],
   )(implicit ec: ExecutionContext): EitherT[Future, PaymentAPIResponseError[PayPalError], PayPalSuccess] = {
-    val data = ExecutePaymentBody(Some(email), email, acquisitionData, paymentJSON)
+    val data = ExecutePaymentBody(Some(email), email, acquisitionData, paymentJSON, similarProductsConsent)
     postPaypalData(data, isTestUser, userAgent).subflatMap(decodePaymentAPIResponse[PayPalError, PayPalSuccess])
   }
 }
diff --git a/support-frontend/assets/helpers/abTests/abtestDefinitions.ts b/support-frontend/assets/helpers/abTests/abtestDefinitions.ts
index ad5c8a7023..afe68e58d6 100644
--- a/support-frontend/assets/helpers/abTests/abtestDefinitions.ts
+++ b/support-frontend/assets/helpers/abTests/abtestDefinitions.ts
@@ -145,4 +145,28 @@ export const tests: Tests = {
 		targetPage: pageUrlRegexes.contributions.genericCheckoutOnly,
 		excludeContributionsOnlyCountries: false,
 	},
+	oneTimeContributionConsent: {
+		variants: [
+			{
+				id: 'control',
+			},
+			{
+				id: 'VariantA',
+			},
+			{
+				id: 'VariantB',
+			},
+		],
+		audiences: {
+			ALL: {
+				offset: 0,
+				size: 1,
+			},
+		},
+		isActive: false,
+		referrerControlled: false,
+		seed: 99,
+		targetPage: pageUrlRegexes.contributions.oneTimeCheckoutOnly,
+		excludeContributionsOnlyCountries: false,
+	},
 };
diff --git a/support-frontend/assets/helpers/forms/paymentIntegrations/oneOffContributions.ts b/support-frontend/assets/helpers/forms/paymentIntegrations/oneOffContributions.ts
index 450e67ed9a..cc7224f344 100644
--- a/support-frontend/assets/helpers/forms/paymentIntegrations/oneOffContributions.ts
+++ b/support-frontend/assets/helpers/forms/paymentIntegrations/oneOffContributions.ts
@@ -75,6 +75,7 @@ type StripeChargeData = {
 
 export type CreateStripePaymentIntentRequest = StripeChargeData & {
 	paymentMethodId: string | PaymentMethod;
+	similarProductsConsent?: boolean;
 };
 
 // Data that should be posted to the payment API to get a url for the PayPal UI
diff --git a/support-frontend/assets/helpers/productCatalog.ts b/support-frontend/assets/helpers/productCatalog.ts
index bf8f8e834e..d41ed2d7db 100644
--- a/support-frontend/assets/helpers/productCatalog.ts
+++ b/support-frontend/assets/helpers/productCatalog.ts
@@ -86,18 +86,6 @@ export const showSimilarProductsConsentForRatePlan = (
 	ratePlanKey: ActiveRatePlanKey,
 ) => !productDescription.ratePlans[ratePlanKey]?.hideSimilarProductsConsent;
 
-export const userShouldSeeConsentCheckbox = (
-	productDescription: ProductDescription,
-	ratePlanKey: ActiveRatePlanKey,
-	abParticipations: Participations,
-) => {
-	return (
-		['VariantA', 'VariantB'].includes(
-			abParticipations.similarProductsConsent ?? '',
-		) && showSimilarProductsConsentForRatePlan(productDescription, ratePlanKey)
-	);
-};
-
 export function filterBenefitByRegion(
 	benefit: {
 		specificToRegions?: CountryGroupId[];
diff --git a/support-frontend/assets/pages/[countryGroupId]/checkout/components/PersonalDetailsFields.tsx b/support-frontend/assets/pages/[countryGroupId]/checkout/components/PersonalDetailsFields.tsx
index d688aa9b54..24af365187 100644
--- a/support-frontend/assets/pages/[countryGroupId]/checkout/components/PersonalDetailsFields.tsx
+++ b/support-frontend/assets/pages/[countryGroupId]/checkout/components/PersonalDetailsFields.tsx
@@ -4,7 +4,7 @@ import {
 	doesNotContainExtendedEmojiOrLeadingSpace,
 	preventDefaultValidityMessage,
 } from 'pages/[countryGroupId]/validation';
-import { SimilarProductsConsent } from '../../components/SimilarProductsConsent';
+import SimilarProductsConsent from '../../components/SimilarProductsConsent';
 import { PersonalEmailFields } from './PersonalEmailFields';
 
 type PersonalDetailsFieldsProps = {
@@ -18,7 +18,7 @@ type PersonalDetailsFieldsProps = {
 	confirmedEmail: string;
 	setConfirmedEmail: (value: string) => void;
 	isSignedIn: boolean;
-	showSimilarProductsConsent: boolean;
+	showSimilarProductsConsent?: boolean;
 };
 
 export function PersonalDetailsFields({
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.test.tsx b/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.test.tsx
new file mode 100644
index 0000000000..12eec4413c
--- /dev/null
+++ b/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.test.tsx
@@ -0,0 +1,49 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { trackComponentClick } from '../../../helpers/tracking/behaviour';
+import SimilarProductsConsent, { CONSENT_ID } from './SimilarProductsConsent';
+
+jest.mock('../../../helpers/tracking/behaviour', () => ({
+	trackComponentClick: jest.fn(),
+}));
+
+describe('SimilarProductsConsent', () => {
+	beforeEach(() => {
+		jest.clearAllMocks();
+	});
+
+	it('renders a hidden input with the correct name and default value "true"', () => {
+		render(<SimilarProductsConsent />);
+
+		const consentInput = screen.getByTestId('consentValue');
+
+		expect(consentInput).toHaveAttribute('name', CONSENT_ID);
+		expect(consentInput).toHaveAttribute('value', 'true');
+	});
+
+	it('calls trackComponentClick with "false" when checkbox is unchecked', () => {
+		render(<SimilarProductsConsent />);
+		const checkbox = screen.getByRole('checkbox');
+
+		// Uncheck the checkbox
+		fireEvent.click(checkbox);
+
+		const consentInput = screen.getByTestId('consentValue');
+		expect(consentInput).toHaveAttribute('value', 'false');
+
+		expect(trackComponentClick).toHaveBeenCalledWith(CONSENT_ID, 'false');
+	});
+
+	it('toggles the checkbox back to checked and tracks with "true"', () => {
+		render(<SimilarProductsConsent />);
+		const checkbox = screen.getByRole('checkbox');
+
+		// Uncheck and then re-check
+		fireEvent.click(checkbox); // false
+		fireEvent.click(checkbox); // true
+
+		const consentInput = screen.getByTestId('consentValue');
+		expect(consentInput).toHaveAttribute('value', 'true');
+
+		expect(trackComponentClick).toHaveBeenLastCalledWith(CONSENT_ID, 'true');
+	});
+});
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.tsx b/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.tsx
index 08c42b095e..2b8a4b7f33 100644
--- a/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.tsx
+++ b/support-frontend/assets/pages/[countryGroupId]/components/SimilarProductsConsent.tsx
@@ -1,19 +1,29 @@
 import { Checkbox } from '@guardian/source/react-components';
+import { useState } from 'react';
 import { trackComponentClick } from '../../../helpers/tracking/behaviour';
 
-export function SimilarProductsConsent() {
+export const CONSENT_ID = 'similar-products-consent-checkbox';
+
+export default function SimilarProductsConsent() {
+	const [consentValue, setConsentValue] = useState(true);
 	return (
-		<Checkbox
-			name="similarProductsConsent"
-			label="Receive information on our products and ways to support and enjoy our journalism. Untick to opt out."
-			defaultChecked={true}
-			onClick={(event) => {
-				const checked = event.currentTarget.checked;
-				trackComponentClick(
-					'similar-products-consent-checkbox',
-					checked.toString(),
-				);
-			}}
-		/>
+		<>
+			<input
+				type="hidden"
+				name={CONSENT_ID}
+				value={consentValue.toString()}
+				data-testid="consentValue"
+			/>
+			<Checkbox
+				name="consentCheckbox"
+				label="Receive information on our products and ways to support and enjoy our journalism. Untick to opt out."
+				defaultChecked
+				onClick={(event) => {
+					const checked = event.currentTarget.checked;
+					setConsentValue(checked);
+					trackComponentClick(CONSENT_ID, checked.toString());
+				}}
+			/>
+		</>
 	);
 }
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx b/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
index 3ac83e77e0..146be9ad01 100644
--- a/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
+++ b/support-frontend/assets/pages/[countryGroupId]/components/checkoutComponent.tsx
@@ -61,7 +61,6 @@ import {
 	productCatalogDescription,
 	productCatalogDescriptionNewBenefits,
 	showSimilarProductsConsentForRatePlan,
-	userShouldSeeConsentCheckbox,
 } from 'helpers/productCatalog';
 import type { Promotion } from 'helpers/productPrice/promotions';
 import type { AddressFormFieldError } from 'helpers/redux/checkout/address/state';
@@ -121,7 +120,7 @@ import {
 	PaymentMethodRadio,
 	PaymentMethodSelector,
 } from './paymentMethod';
-import { SimilarProductsConsent } from './SimilarProductsConsent';
+import SimilarProductsConsent from './SimilarProductsConsent';
 import { SubmitButton } from './submitButton';
 
 const countriesRequiringBillingState = ['US', 'CA', 'AU'];
@@ -524,11 +523,6 @@ export function CheckoutComponent({
 				paymentFields,
 				productFields,
 				hasDeliveryAddress: !!productDescription.deliverableTo,
-				userWasShownCheckbox: userShouldSeeConsentCheckbox(
-					productDescription,
-					ratePlanKey,
-					abParticipations,
-				),
 				abParticipations,
 				promotion,
 				contributionAmount,
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/formOnSubmit.ts b/support-frontend/assets/pages/[countryGroupId]/components/formOnSubmit.ts
index 54a26d23a9..5be3fd07ab 100644
--- a/support-frontend/assets/pages/[countryGroupId]/components/formOnSubmit.ts
+++ b/support-frontend/assets/pages/[countryGroupId]/components/formOnSubmit.ts
@@ -30,20 +30,12 @@ import {
 	deleteFormDetails,
 	persistFormDetails,
 } from '../checkout/helpers/stripeCheckoutSession';
+import getConsentValue from '../helpers/getConsentValue';
 import { createSubscription } from './createSubscription';
 import type { PaymentMethod } from './paymentFields';
 import { FormSubmissionError } from './paymentFields';
+import { CONSENT_ID } from './SimilarProductsConsent';
 
-const getConsentValue = (formData: FormData, userWasShownCheckbox: boolean) => {
-	const similarProductsCheckbox = formData.get('similarProductsConsent');
-	if (similarProductsCheckbox) {
-		return similarProductsCheckbox === 'on';
-	}
-	if (userWasShownCheckbox) {
-		return false;
-	}
-	return;
-};
 export const submitForm = async ({
 	geoId,
 	productKey,
@@ -53,7 +45,6 @@ export const submitForm = async ({
 	paymentFields,
 	productFields,
 	hasDeliveryAddress,
-	userWasShownCheckbox,
 	abParticipations,
 	promotion,
 	contributionAmount,
@@ -66,7 +57,6 @@ export const submitForm = async ({
 	paymentFields: RegularPaymentFields;
 	productFields: ProductFields;
 	hasDeliveryAddress: boolean;
-	userWasShownCheckbox: boolean;
 	abParticipations: Participations;
 	promotion: Promotion | undefined;
 	contributionAmount: number | undefined;
@@ -87,11 +77,6 @@ export const submitForm = async ({
 		productFields,
 	);
 
-	const similarProductsConsent = getConsentValue(
-		formData,
-		userWasShownCheckbox,
-	);
-
 	const promoCode = promotion?.promoCode;
 	const appliedPromotion =
 		promoCode !== undefined
@@ -102,6 +87,7 @@ export const submitForm = async ({
 			: undefined;
 	const supportAbTests = getSupportAbTests(abParticipations);
 	const deliveryInstructions = formData.get('deliveryInstructions') as string;
+	const similarProductsConsent = getConsentValue(formData, CONSENT_ID);
 
 	const paymentRequest: RegularPaymentRequest = {
 		...personalData,
diff --git a/support-frontend/assets/pages/[countryGroupId]/components/oneTimeCheckoutComponent.tsx b/support-frontend/assets/pages/[countryGroupId]/components/oneTimeCheckoutComponent.tsx
index cf08373223..61d1caf969 100644
--- a/support-frontend/assets/pages/[countryGroupId]/components/oneTimeCheckoutComponent.tsx
+++ b/support-frontend/assets/pages/[countryGroupId]/components/oneTimeCheckoutComponent.tsx
@@ -87,6 +87,7 @@ import {
 } from '../../../helpers/storage/abandonedBasketCookies';
 import { PersonalEmailFields } from '../checkout/components/PersonalEmailFields';
 import { setThankYouOrder } from '../checkout/helpers/sessionStorage';
+import getConsentValue from '../helpers/getConsentValue';
 import {
 	doesNotContainExtendedEmojiOrLeadingSpace,
 	preventDefaultValidityMessage,
@@ -101,6 +102,7 @@ import {
 	PaymentMethodRadio,
 	PaymentMethodSelector,
 } from './paymentMethod';
+import SimilarProductsConsent, { CONSENT_ID } from './SimilarProductsConsent';
 
 /**
  * We have not added StripeExpressCheckoutElement to the old PaymentMethod
@@ -140,6 +142,19 @@ const tcContainer = css`
 	}
 `;
 
+const similarProductsConsentCheckboxContainer = css`
+	padding: 6px ${space[4]}px;
+	background-color: ${neutral[97]};
+	border-radius: 12px;
+	margin: ${space[4]}px 0px ${space[2]}px;
+	${from.tablet} {
+		margin-top: ${space[5]}px 0px 0px;
+	}
+	> div > input {
+		background-color: ${neutral[100]};
+	}
+`;
+
 type OneTimeCheckoutComponentProps = {
 	geoId: GeoId;
 	appConfig: AppConfig;
@@ -370,7 +385,9 @@ export function OneTimeCheckoutComponent({
 		}
 	};
 
-	const formOnSubmit = async () => {
+	const formOnSubmit = async (formData: FormData) => {
+		const similarProductsConsent = getConsentValue(formData, CONSENT_ID);
+
 		if (finalAmount) {
 			setIsProcessingPayment(true);
 
@@ -403,6 +420,11 @@ export function OneTimeCheckoutComponent({
 					'acquisition_data',
 					encodeURIComponent(JSON.stringify(acquisitionData)),
 				);
+				cookie.set(
+					'gu_similar_products_consent',
+					JSON.stringify(similarProductsConsent),
+					1, // daysToLive
+				);
 			}
 
 			let paymentMethodResult;
@@ -479,6 +501,7 @@ export function OneTimeCheckoutComponent({
 						publicKey: stripePublicKey,
 						recaptchaToken: recaptchaToken ?? '',
 						paymentMethodId: paymentMethodResult.paymentMethod.id,
+						similarProductsConsent,
 					};
 					paymentResult = await processStripePaymentIntentRequest(
 						stripeData,
@@ -612,8 +635,10 @@ export function OneTimeCheckoutComponent({
 				ref={formRef}
 				onSubmit={(event) => {
 					event.preventDefault();
+					const form = event.currentTarget;
+					const formData = new FormData(form);
 					/** we defer this to an external function as a lot of the payment methods use async */
-					void formOnSubmit();
+					void formOnSubmit(formData);
 
 					return false;
 				}}
@@ -730,6 +755,9 @@ export function OneTimeCheckoutComponent({
 								isEmailAddressReadOnly={isSignedIn}
 								isSignedIn={isSignedIn}
 							/>
+							{abParticipations.oneTimeContributionConsent === 'VariantA' && (
+								<SimilarProductsConsent />
+							)}
 
 							{countryId === 'US' && (
 								<div>
@@ -849,6 +877,11 @@ export function OneTimeCheckoutComponent({
 								finalAmount ? finalAmount : 0,
 							)}
 						/>
+						{abParticipations.oneTimeContributionConsent === 'VariantB' && (
+							<div css={similarProductsConsentCheckboxContainer}>
+								<SimilarProductsConsent />
+							</div>
+						)}
 						<div
 							css={css`
 								margin: ${space[8]}px 0 ${space[6]}px;
diff --git a/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.test.ts b/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.test.ts
new file mode 100644
index 0000000000..67de609ff6
--- /dev/null
+++ b/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.test.ts
@@ -0,0 +1,27 @@
+import { CONSENT_ID } from '../components/SimilarProductsConsent';
+import getConsentValue from './getConsentValue';
+
+describe('getConsentValue', () => {
+	it('returns true when consent is explicitly "true"', () => {
+		const formData = new FormData();
+		formData.set(CONSENT_ID, 'true');
+
+		const result = getConsentValue(formData, CONSENT_ID);
+		expect(result).toBe(true);
+	});
+
+	it('returns false when consent is explicitly "false"', () => {
+		const formData = new FormData();
+		formData.set(CONSENT_ID, 'false');
+
+		const result = getConsentValue(formData, CONSENT_ID);
+		expect(result).toBe(false);
+	});
+
+	it('returns undefined when the key is not present', () => {
+		const formData = new FormData();
+
+		const result = getConsentValue(formData, CONSENT_ID);
+		expect(result).toBeUndefined();
+	});
+});
diff --git a/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.ts b/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.ts
new file mode 100644
index 0000000000..107cabb161
--- /dev/null
+++ b/support-frontend/assets/pages/[countryGroupId]/helpers/getConsentValue.ts
@@ -0,0 +1,14 @@
+function getConsentValue(
+	formData: FormData,
+	productConsent: string,
+): boolean | undefined {
+	const consentValue = formData.get(productConsent);
+
+	if (!consentValue) {
+		return;
+	}
+
+	return consentValue === 'true';
+}
+
+export default getConsentValue;
diff --git a/support-frontend/assets/pages/supporter-plus-landing/components/coverTransactionCost.tsx b/support-frontend/assets/pages/supporter-plus-landing/components/coverTransactionCost.tsx
index 6681dfe3db..b6ee41e731 100644
--- a/support-frontend/assets/pages/supporter-plus-landing/components/coverTransactionCost.tsx
+++ b/support-frontend/assets/pages/supporter-plus-landing/components/coverTransactionCost.tsx
@@ -20,7 +20,7 @@ const coverTransactionDivider = css`
 `;
 
 const coverTransactionCheckboxContainer = css`
-	padding: ${space[4]}px;
+	padding: 6px ${space[4]}px;
 	background-color: ${neutral[97]};
 	border-radius: 12px;
 	margin: ${space[4]}px 0px ${space[2]}px;
diff --git a/support-payment-api/src/main/scala/backend/PaypalBackend.scala b/support-payment-api/src/main/scala/backend/PaypalBackend.scala
index fb31a7399e..c58cec1fca 100644
--- a/support-payment-api/src/main/scala/backend/PaypalBackend.scala
+++ b/support-payment-api/src/main/scala/backend/PaypalBackend.scala
@@ -107,6 +107,7 @@ class PaypalBackend(
               identityUserDetails.map(_.id),
               executePaymentData.acquisitionData,
               clientBrowserInfo,
+              executePaymentData.similarProductsConsent,
             )
 
             EnrichedPaypalPayment(payment, Some(executePaymentData.email), identityUserDetails.map(_.userType))
@@ -130,8 +131,9 @@ class PaypalBackend(
       identityId: Option[String],
       acquisitionData: AcquisitionData,
       clientBrowserInfo: ClientBrowserInfo,
+      similarProductsConsent: Option[Boolean],
   ): Unit = {
-    trackContribution(payment, acquisitionData, email, identityId, clientBrowserInfo)
+    trackContribution(payment, acquisitionData, email, identityId, clientBrowserInfo, similarProductsConsent)
       .map(errors =>
         cloudWatchService.recordPostPaymentTasksErrors(
           PaymentProvider.Paypal,
@@ -162,6 +164,7 @@ class PaypalBackend(
       email: String,
       identityId: Option[String],
       clientBrowserInfo: ClientBrowserInfo,
+      similarProductsConsent: Option[Boolean],
   ): Future[List[BackendError]] = {
     ContributionData.fromPaypalCharge(
       payment,
@@ -176,7 +179,8 @@ class PaypalBackend(
           PaypalAcquisition(payment, acquisitionData, contributionData.identityId, clientBrowserInfo)
 
         track(
-          acquisition = AcquisitionDataRowBuilder.buildFromPayPal(paypalAcquisition, contributionData),
+          acquisition =
+            AcquisitionDataRowBuilder.buildFromPayPal(paypalAcquisition, contributionData, similarProductsConsent),
           contributionData,
         )
     }
diff --git a/support-payment-api/src/main/scala/model/acquisition/AcquisitionDataRowBuilder.scala b/support-payment-api/src/main/scala/model/acquisition/AcquisitionDataRowBuilder.scala
index 2257f0cb3f..1fe1c6e3cc 100644
--- a/support-payment-api/src/main/scala/model/acquisition/AcquisitionDataRowBuilder.scala
+++ b/support-payment-api/src/main/scala/model/acquisition/AcquisitionDataRowBuilder.scala
@@ -60,13 +60,14 @@ object AcquisitionDataRowBuilder {
       postalCode = contributionData.postalCode,
       state = contributionData.countrySubdivisionCode,
       email = Some(contributionData.email),
-      similarProductsConsent = None, // TODO: We are going to leave this unset for now to keep the behaviour unchanged
+      similarProductsConsent = acquisition.stripeChargeData.similarProductsConsent,
     )
   }
 
   def buildFromPayPal(
       acquisition: PaypalAcquisition,
       contributionData: ContributionData,
+      similarProductsConsent: Option[Boolean],
   ): AcquisitionDataRow = {
     val acquisitionData = acquisition.acquisitionData
     val transaction = acquisition.payment.getTransactions.get(0)
@@ -105,7 +106,7 @@ object AcquisitionDataRowBuilder {
       postalCode = contributionData.postalCode,
       state = contributionData.countrySubdivisionCode,
       email = Some(contributionData.email),
-      similarProductsConsent = None, // TODO: We are going to leave this unset for now to keep the behaviour unchanged
+      similarProductsConsent = similarProductsConsent,
     )
   }
 
diff --git a/support-payment-api/src/main/scala/model/paypal/PaypalPaymentData.scala b/support-payment-api/src/main/scala/model/paypal/PaypalPaymentData.scala
index 66f8a5a24d..63dd0e8917 100644
--- a/support-payment-api/src/main/scala/model/paypal/PaypalPaymentData.scala
+++ b/support-payment-api/src/main/scala/model/paypal/PaypalPaymentData.scala
@@ -36,4 +36,5 @@ case class ExecutePaypalPaymentData(
     paymentData: ExecutePaymentData,
     acquisitionData: AcquisitionData,
     email: String,
+    similarProductsConsent: Option[Boolean],
 )
diff --git a/support-payment-api/src/main/scala/model/stripe/StripeRequest.scala b/support-payment-api/src/main/scala/model/stripe/StripeRequest.scala
index b70c30c3da..a00467988f 100644
--- a/support-payment-api/src/main/scala/model/stripe/StripeRequest.scala
+++ b/support-payment-api/src/main/scala/model/stripe/StripeRequest.scala
@@ -65,6 +65,7 @@ sealed trait StripeRequest {
   val paymentData: BaseStripePaymentData // data required to create a Stripe payment
   val acquisitionData: AcquisitionData // data required to create an acquisition event (used for analytics)
   val publicKey: StripePublicKey // required to determine which Stripe service to use
+  val similarProductsConsent: Option[Boolean]
 }
 
 // Models for the Stripe Payment Intents API
@@ -76,6 +77,7 @@ object StripePaymentIntentRequest {
       acquisitionData: AcquisitionData,
       publicKey: StripePublicKey,
       recaptchaToken: String,
+      similarProductsConsent: Option[Boolean],
   ) extends StripeRequest
 
   case class ConfirmPaymentIntent(
@@ -83,6 +85,7 @@ object StripePaymentIntentRequest {
       paymentData: StripePaymentData,
       acquisitionData: AcquisitionData,
       publicKey: StripePublicKey,
+      similarProductsConsent: Option[Boolean],
   ) extends StripeRequest
 
   import controllers.JsonReadableOps._
diff --git a/support-payment-api/src/test/scala/backend/PaypalBackendSpec.scala b/support-payment-api/src/test/scala/backend/PaypalBackendSpec.scala
index 2d4cadd933..310e71fcf5 100644
--- a/support-payment-api/src/test/scala/backend/PaypalBackendSpec.scala
+++ b/support-payment-api/src/test/scala/backend/PaypalBackendSpec.scala
@@ -49,6 +49,7 @@ class PaypalBackendFixture(implicit ec: ExecutionContext) extends MockitoSugar {
     ExecutePaymentData("paymentId", "payerId"),
     acquisitionData,
     "email@email.com",
+    similarProductsConsent = Some(false),
   )
   val paypalRefundWebHookData = PaypalRefundWebHookData(
     body = PaypalRefundWebHookBody("parent_payment_id", "{}"),
@@ -365,6 +366,7 @@ class PaypalBackendSpec extends AnyWordSpec with Matchers with FutureEitherValue
           "a@b.com",
           None,
           clientBrowserInfo,
+          None,
         )
 
         result.futureValue mustBe List(BackendError.Database(dbError))
@@ -393,6 +395,7 @@ class PaypalBackendSpec extends AnyWordSpec with Matchers with FutureEitherValue
           "a@b.com",
           None,
           clientBrowserInfo,
+          None,
         )
         result.futureValue mustEqual errors
 
diff --git a/support-payment-api/src/test/scala/backend/StripeBackendSpec.scala b/support-payment-api/src/test/scala/backend/StripeBackendSpec.scala
index 40de0ddb69..ad0f25bfb0 100644
--- a/support-payment-api/src/test/scala/backend/StripeBackendSpec.scala
+++ b/support-payment-api/src/test/scala/backend/StripeBackendSpec.scala
@@ -63,9 +63,16 @@ class StripeBackendFixture(implicit ec: ExecutionContext) extends MockitoSugar {
       acquisitionData,
       stripePublicKey,
       recaptchaToken,
+      similarProductsConsent = Some(false),
     )
   val confirmPaymentIntent =
-    ConfirmPaymentIntent("id", stripePaymentData, acquisitionData, stripePublicKey)
+    ConfirmPaymentIntent(
+      "id",
+      stripePaymentData,
+      acquisitionData,
+      stripePublicKey,
+      similarProductsConsent = Some(false),
+    )
 
   val countrySubdivisionCode = Some("NY")
   val clientBrowserInfo = ClientBrowserInfo("", "", None, None, countrySubdivisionCode)
@@ -253,6 +260,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         when(mockSwitchService.allSwitches).thenReturn(switchServiceStripeOffResponse)
 
@@ -269,6 +277,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         when(mockSwitchService.allSwitches).thenReturn(switchServiceStripeOffResponse)
 
@@ -285,6 +294,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         when(mockSwitchService.allSwitches).thenReturn(switchServiceStripeOffResponse)
 
@@ -302,6 +312,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         populateChargeMock()
         when(paymentIntentMock.getStatus).thenReturn("succeeded")
@@ -334,6 +345,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         populateChargeMock()
         when(paymentIntentMock.getStatus).thenReturn("succeeded")
@@ -365,6 +377,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         populateChargeMock()
         when(paymentIntentMock.getStatus).thenReturn("succeeded")
@@ -402,6 +415,7 @@ class StripeBackendSpec
             acquisitionData,
             stripePublicKey,
             recaptchaToken,
+            similarProductsConsent = Some(false),
           )
         populateChargeMock()
         stripeBackend.createPaymentIntent(createPaymentIntentWithStripeCheckout, clientBrowserInfo).futureLeft mustBe
diff --git a/support-payment-api/src/test/scala/controllers/StripeControllerSpec.scala b/support-payment-api/src/test/scala/controllers/StripeControllerSpec.scala
index f758dc07ae..2f603b79ff 100644
--- a/support-payment-api/src/test/scala/controllers/StripeControllerSpec.scala
+++ b/support-payment-api/src/test/scala/controllers/StripeControllerSpec.scala
@@ -120,6 +120,7 @@ class StripeControllerSpec extends AnyWordSpec with Status with Matchers {
     ),
     publicKey = StripePublicKey("pk_test_FOO"),
     recaptchaToken = "recaptchaToken",
+    similarProductsConsent = Some(true),
   )
 
   "StripeController" when {


Reviews:
user: login: maloky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:31:10Z
user: login: maloky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:32:59Z
user: login: rupertbates
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:44:17Z
user: login: rupertbates
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:45:15Z
user: login: maloky
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:59:04Z
user: login: rupertbates
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T10:30:11Z
user: login: johnduffell
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:47:20Z
user: login: johnduffell
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:48:21Z

Review comments:
diff_hunk: @@ -47,18 +47,6 @@ export const showSimilarProductsConsentForRatePlan = (
 	ratePlanKey: string,
 ) => !productDescription.ratePlans[ratePlanKey]?.hideSimilarProductsConsent;
 
-export const userShouldSeeConsentCheckbox = (
-	productDescription: ProductDescription,
-	ratePlanKey: string,
-	abParticipations: Participations,
-) => {
-	return (
-		['VariantA', 'VariantB'].includes(
-			abParticipations.similarProductsConsent ?? '',
-		) && showSimilarProductsConsentForRatePlan(productDescription, ratePlanKey)
-	);
-};
-
path: support-frontend/assets/helpers/productCatalog.ts
user: login: maloky
body: Note To Reviewers: Removed as it became redundant since we already check for this when rendering the component.
created_at: 2025-05-08T16:31:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,41 @@
+import { Checkbox } from '@guardian/source/react-components';
+import { useState } from 'react';
+import { trackComponentClick } from '../../../helpers/tracking/behaviour';
+
+export const productConsents = {
+	similarProductsConsent: 'similar-products-consent-checkbox',
+	oneTimeContributionConsent: 'one-time-contribution-consent-checkbox',
+} satisfies Record<string, string>;
+
+export type ProductConsent = keyof typeof productConsents;
+
+export default function SoftOptInCheckoutConsent({
+	productConsent,
+}: {
+	productConsent: ProductConsent;
+}) {
+	const [consentValue, setConsentValue] = useState(true);
+	return (
+		<>
+			<input
+				type="hidden"
+				name={productConsent}
+				value={consentValue.toString()}
+				data-testid="consentValue"
+			/>
path: support-frontend/assets/pages/[countryGroupId]/components/SoftOptInCheckoutConsent.tsx
user: login: maloky
body: Note to Reviewers: This input will provide us with all the possible outcomes. undefined if not visible, or true/false when visible.
created_at: 2025-05-08T16:32:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -403,6 +420,11 @@ export function OneTimeCheckoutComponent({
 					'acquisition_data',
 					encodeURIComponent(JSON.stringify(acquisitionData)),
 				);
+				cookie.set(
+					'gu_similar_procucts_consent',
path: support-frontend/assets/pages/[countryGroupId]/components/oneTimeCheckoutComponent.tsx
user: login: rupertbates
body: ```suggestion
					'gu_similar_products_consent',
```
created_at: 2025-05-13T15:44:16Z
author_association: MEMBER
diff_hunk: @@ -102,11 +102,21 @@ class PayPalOneOff(
         "payerId" -> PayerID,
       )
 
+      val similarProductsConsentCookie = request.cookies.get("gu_similar_procucts_consent")
path: support-frontend/app/controllers/PayPalOneOff.scala
user: login: rupertbates
body: ```suggestion
      val similarProductsConsentCookie = request.cookies.get("gu_similar_products_consent")
```
created_at: 2025-05-13T15:45:14Z
author_association: MEMBER
diff_hunk: @@ -102,11 +102,21 @@ class PayPalOneOff(
         "payerId" -> PayerID,
       )
 
+      val similarProductsConsentCookie = request.cookies.get("gu_similar_procucts_consent")
path: support-frontend/app/controllers/PayPalOneOff.scala
user: login: maloky
body: at least I was consistent 😂 
created_at: 2025-05-13T16:59:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,11 +102,21 @@ class PayPalOneOff(
         "payerId" -> PayerID,
       )
 
+      val similarProductsConsentCookie = request.cookies.get("gu_similar_products_consent")
+
+      val similarProductsConsent = similarProductsConsentCookie.flatMap { cookie =>
+        cookie.value.toLowerCase match {
+          case "true" => Some(true)
+          case "false" => Some(false)
+          case _ => None
+        }
+      }
path: support-frontend/app/controllers/PayPalOneOff.scala
user: login: johnduffell
body: ```suggestion
      val similarProductsConsent = similarProductsConsentCookie.flatMap(_.value.toBooleanOption)
```
see https://scastie.scala-lang.org/ehFqkaqyTNGZSMlKcKjLxA
created_at: 2025-05-15T12:47:19Z
author_association: MEMBER
diff_hunk: @@ -55,7 +56,6 @@ class PaymentAPIService(wsClient: WSClient, val paymentAPIUrl: String)(implicit
 
   val payPalCreatePaymentEndpoint: String = s"$paymentAPIUrl$paypalCreatePaymentPath"
   val payPalExecutePaymentEndpoint: String = s"$paymentAPIUrl$paypalExecutePaymentPath"
-
path: support-frontend/app/services/PaymentAPIService.scala
user: login: johnduffell
body: unless it was intentional, keep this spacer line back in
created_at: 2025-05-15T12:48:20Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-08T13:44:59Z
body: **Size Change:** +373 B (+0.03%) 

**Total Size:** 1.46 MB

| Filename | Size | Change |  |
| :--- | :---: | :---: | :---: |
| `./public/compiled-assets/webpack/468.js` | 0 B | -34.7 kB (removed) | 🏆 |
| `./public/compiled-assets/webpack/649.js` | 34.9 kB | +34.9 kB (new file) | 🆕 |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./public/compiled-assets/javascripts/[countryGroupId]/events/router.js` | 92.4 kB | +23 B (+0.02%) |
| `./public/compiled-assets/javascripts/[countryGroupId]/router.js` | 98.5 kB | +14 B (+0.01%) |
| `./public/compiled-assets/javascripts/ausMomentMap.js` | 108 kB | 0 B |
| `./public/compiled-assets/javascripts/contributionsRedirectStyles.js` | 20 B | 0 B |
| `./public/compiled-assets/javascripts/downForMaintenancePage.js` | 69.8 kB | +7 B (+0.01%) |
| `./public/compiled-assets/javascripts/error404Page.js` | 69.8 kB | +4 B (+0.01%) |
| `./public/compiled-assets/javascripts/error500Page.js` | 69.7 kB | +5 B (+0.01%) |
| `./public/compiled-assets/javascripts/favicons.js` | 618 B | 0 B |
| `./public/compiled-assets/javascripts/paperSubscriptionLandingPage.js` | 90.5 kB | +22 B (+0.02%) |
| `./public/compiled-assets/javascripts/payPalErrorPage.js` | 68.3 kB | +3 B (0%) |
| `./public/compiled-assets/javascripts/payPalErrorPageStyles.js` | 20 B | 0 B |
| `./public/compiled-assets/javascripts/promotionTerms.js` | 76.1 kB | +20 B (+0.03%) |
| `./public/compiled-assets/javascripts/subscriptionsLandingPage.js` | 100 kB | +22 B (+0.02%) |
| `./public/compiled-assets/javascripts/unsupportedBrowserStyles.js` | 20 B | 0 B |
| `./public/compiled-assets/javascripts/weeklySubscriptionCheckoutPage.js` | 159 kB | +13 B (+0.01%) |
| `./public/compiled-assets/javascripts/weeklySubscriptionLandingPage.js` | 94 kB | +19 B (+0.02%) |
| `./public/compiled-assets/webpack/100.js` | 5.8 kB | 0 B |
| `./public/compiled-assets/webpack/231.js` | 20.2 kB | +15 B (+0.07%) |
| `./public/compiled-assets/webpack/460.js` | 14.4 kB | 0 B |
| `./public/compiled-assets/webpack/471.js` | 24.2 kB | 0 B |
| `./public/compiled-assets/webpack/474.js` | 4.98 kB | 0 B |
| `./public/compiled-assets/webpack/500.js` | 3.52 kB | 0 B |
| `./public/compiled-assets/webpack/566.js` | 4.36 kB | 0 B |
| `./public/compiled-assets/webpack/63.js` | 2.17 kB | 0 B |
| `./public/compiled-assets/webpack/741.js` | 8.81 kB | 0 B |
| `./public/compiled-assets/webpack/751.js` | 7.08 kB | -47 B (-0.66%) |
| `./public/compiled-assets/webpack/778.js` | 107 kB | 0 B |
| `./public/compiled-assets/webpack/935.js` | 25.5 kB | 0 B |
| `./public/compiled-assets/webpack/943.js` | 7.58 kB | 0 B |
| `./public/compiled-assets/webpack/checkout.js` | 18.7 kB | -145 B (-0.77%) |
| `./public/compiled-assets/webpack/GuardianAdLiteLanding.js` | 8.4 kB | 0 B |
| `./public/compiled-assets/webpack/LandingPage.js` | 15.4 kB | 0 B |
| `./public/compiled-assets/webpack/oneTimeCheckout.js` | 7.36 kB | +161 B (+2.24%) |
| `./public/compiled-assets/webpack/ThankYou.js` | 43.6 kB | 0 B |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T13:45:04Z
body: ### [Deploy build 14708 of `support::stripe-patrons-data` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=support%3A%3Astripe-patrons-data&build=14708&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 14708 of `support::stripe-patrons-data` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=support%3A%3Astripe-patrons-data&build=14708&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 14708 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=support%3A%3Astripe-patrons-data&build=14708&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=support%3A%3Astripe-patrons-data&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for support::stripe-patrons-data -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T13:46:40Z
body: ### [Deploy build 23972 of `support:support-workers-mono` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=support%3Asupport-workers-mono&build=23972&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 23972 of `support:support-workers-mono` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=support%3Asupport-workers-mono&build=23972&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 23972 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=support%3Asupport-workers-mono&build=23972&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=support%3Asupport-workers-mono&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for support:support-workers-mono -->
author_association: CONTRIBUTOR
user: login: prout-bot
created_at: 2025-05-15T14:06:25Z
body: Seen on [PROD](https://support.theguardian.com/uk/subscribe) (merged by @maloky 9 minutes and 54 seconds ago) 
- Keep an eye out for the post deployment Playwright smoke tests [github action](https://github.com/guardian/support-frontend/actions/workflows/playwright-smoke.yml)
- If the tests have failed, the [post deployment test runbook](https://github.com/guardian/support-frontend/wiki/Post-deployment-test-runbook) should help troubleshoot.


#### Sentry Release: [support-client-side](https://sentry.io/the-guardian/support-client-side/releases/de28ba3e5a965956bb6c2586feaf9c54063f3e64/), [support](https://sentry.io/the-guardian/support/releases/de28ba3e5a965956bb6c2586feaf9c54063f3e64/)
author_association: NONE