diff --git a/frontend/src/components/routes/product-widget/CollectInformation/index.tsx b/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
index 671cf19412..a864cc6b39 100644
--- a/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
+++ b/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
@@ -33,6 +33,7 @@ import countries from "../../../../../data/countries.json";
import classes from "./CollectInformation.module.scss";
import {trackEvent, AnalyticsEvents} from "../../../../utilites/analytics.ts";
import {clearWaitlistJoinedForEvent} from "../../../../hooks/useWaitlistJoined.ts";
+import {useCheckoutPrefill, CheckoutPrefill} from "../../../../hooks/useCheckoutPrefill.ts";
const LoadingSkeleton = () =>
(
@@ -48,6 +49,8 @@ export const CollectInformation = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const isFromWaitlist = searchParams.get('waitlist') === 'true';
+ const {prefill, lock} = useCheckoutPrefill();
+ const isLocked = (field: keyof CheckoutPrefill) => lock && prefill[field] !== undefined;
const {
isFetched: isOrderFetched,
data: order,
@@ -296,18 +299,38 @@ export const CollectInformation = () => {
useEffect(() => {
if (isEventFetched && isOrderFetched && isQuestionsFetched && productQuestions && orderQuestions) {
- const products = createProductsAndQuestions(createProductIdToQuestionMap());
+ const builtProducts = createProductsAndQuestions(createProductIdToQuestionMap());
const formOrderQuestions = createFormOrderQuestions();
+ const orderPrefill = {
+ ...(prefill.first_name !== undefined && {first_name: prefill.first_name}),
+ ...(prefill.last_name !== undefined && {last_name: prefill.last_name}),
+ ...(prefill.email !== undefined && {email: prefill.email, email_confirmation: prefill.email}),
+ };
+
+ const ticketProductIds = new Set(
+ (products ?? [])
+ .filter(product => product && product.product_type === 'TICKET')
+ .map(product => product!.id)
+ );
+
+ const prefilledProducts = builtProducts.map((product: any) =>
+ (!isPerOrderCollection && ticketProductIds.has(product.product_id))
+ ? {...product, ...orderPrefill}
+ : product
+ );
+
form.setValues({
...form.values,
- products: products,
+ products: prefilledProducts,
order: {
...form.values.order,
+ ...orderPrefill,
questions: formOrderQuestions,
},
});
}
+ // prefill/lock intentionally omitted: they're memoized off query params that don't change during the page's lifetime
}, [isEventFetched, isOrderFetched, isQuestionsFetched]);
useEffect(() => {
@@ -433,12 +456,14 @@ export const CollectInformation = () => {
withAsterisk
label={t`First Name`}
placeholder={t`First name`}
+ disabled={isLocked('first_name')}
{...form.getInputProps("order.first_name")}
/>
@@ -449,6 +474,7 @@ export const CollectInformation = () => {
type={"email"}
label={t`Email Address`}
placeholder={t`Email Address`}
+ disabled={isLocked('email')}
rightSection={isEmailValid(form.values.order.email) ? : null}
{...form.getInputProps("order.email")}
/>
@@ -457,12 +483,13 @@ export const CollectInformation = () => {
type={"email"}
label={t`Confirm Email Address`}
placeholder={t`Confirm Email Address`}
+ disabled={isLocked('email')}
rightSection={isEmailValid(form.values.order.email_confirmation) ? : null}
{...form.getInputProps("order.email_confirmation")}
/>
- {orderRequiresAttendeeDetails && !isPerOrderCollection && totalTicketAttendees > 0 && (
+ {orderRequiresAttendeeDetails && !isPerOrderCollection && totalTicketAttendees > 0 && !lock && (
{totalTicketAttendees === 1 ? (
{
withAsterisk
label={t`First Name`}
placeholder={t`First name`}
+ disabled={isLocked('first_name')}
{...form.getInputProps(`products.${currentProductIndex}.first_name`)}
/>
@@ -661,6 +690,7 @@ export const CollectInformation = () => {
type={"email"}
label={t`Email Address`}
placeholder={t`Email Address`}
+ disabled={isLocked('email')}
rightSection={isEmailValid(form.values.products[currentProductIndex]?.email || '') ?
: null}
{...form.getInputProps(`products.${currentProductIndex}.email`)}
@@ -670,6 +700,7 @@ export const CollectInformation = () => {
type={"email"}
label={t`Confirm Email Address`}
placeholder={t`Confirm Email Address`}
+ disabled={isLocked('email')}
rightSection={isEmailValid(form.values.products[currentProductIndex]?.email_confirmation || '') ?
: null}
{...form.getInputProps(`products.${currentProductIndex}.email_confirmation`)}
diff --git a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
index 95d9ae43b0..68fdd9fdb5 100644
--- a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
+++ b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx
@@ -37,6 +37,7 @@ import {IconChevronRight, IconX} from "@tabler/icons-react"
import {getSessionIdentifier} from "../../../../utilites/sessionIdentifier.ts";
import {Constants} from "../../../../constants.ts";
import {clearWaitlistJoinedForEvent} from "../../../../hooks/useWaitlistJoined.ts";
+import {CHECKOUT_PREFILL_PARAM_KEYS} from "../../../../hooks/useCheckoutPrefill.ts";
const AFFILIATE_EXPIRY_DAYS = 30;
@@ -150,16 +151,29 @@ const SelectProducts = (props: SelectProductsProps) => {
onSuccess: (data) => queryClient.invalidateQueries()
.then(() => {
const url = '/checkout/' + eventId + '/' + data.data.short_id + '/details';
+
+ // Forward checkout-prefill params (name/email/lock) from the event page
+ // to the details step, since this navigation would otherwise drop them.
+ const sourceParams = new URLSearchParams(window.location.search);
+ const prefillParams = new URLSearchParams();
+ CHECKOUT_PREFILL_PARAM_KEYS.forEach((key) => {
+ const value = sourceParams.get(key);
+ if (value !== null) {
+ prefillParams.set(key, value);
+ }
+ });
+ const prefillQuery = prefillParams.toString();
+
if (props.widgetMode === 'embedded') {
- window.open(
- url + '?session_identifier=' + data.data.session_identifier + '&utm_source=embedded_widget',
- '_blank'
- );
+ const embeddedQuery = 'session_identifier=' + data.data.session_identifier
+ + '&utm_source=embedded_widget'
+ + (prefillQuery ? '&' + prefillQuery : '');
+ window.open(url + '?' + embeddedQuery, '_blank');
setOrderInProcessOverlayVisible(true);
return;
}
- return navigate(url);
+ return navigate(url + (prefillQuery ? '?' + prefillQuery : ''));
}),
onError: (error: any) => {
diff --git a/frontend/src/hooks/useCheckoutPrefill.ts b/frontend/src/hooks/useCheckoutPrefill.ts
new file mode 100644
index 0000000000..167464944b
--- /dev/null
+++ b/frontend/src/hooks/useCheckoutPrefill.ts
@@ -0,0 +1,53 @@
+import {useMemo} from "react";
+import {useSearchParams} from "react-router";
+
+export interface CheckoutPrefill {
+ first_name?: string;
+ last_name?: string;
+ email?: string;
+}
+
+export interface UseCheckoutPrefillResult {
+ prefill: CheckoutPrefill;
+ lock: boolean;
+}
+
+// Query params the checkout details form reads to prefill itself. Also forwarded
+// from the event page to the details step so prefill survives order creation
+// (see SelectProducts).
+export const CHECKOUT_PREFILL_PARAM_KEYS = ["first_name", "last_name", "email", "lock"] as const;
+
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export const useCheckoutPrefill = (): UseCheckoutPrefillResult => {
+ const [searchParams] = useSearchParams();
+
+ const firstNameParam = searchParams.get("first_name");
+ const lastNameParam = searchParams.get("last_name");
+ const emailParam = searchParams.get("email");
+ const lockParam = searchParams.get("lock");
+
+ return useMemo(() => {
+ const prefill: CheckoutPrefill = {};
+
+ const firstName = firstNameParam?.trim();
+ if (firstName) {
+ prefill.first_name = firstName;
+ }
+
+ const lastName = lastNameParam?.trim();
+ if (lastName) {
+ prefill.last_name = lastName;
+ }
+
+ const email = emailParam?.trim();
+ if (email && EMAIL_REGEX.test(email)) {
+ prefill.email = email;
+ }
+
+ const hasPrefill = Object.keys(prefill).length > 0;
+ const lock = hasPrefill && (lockParam === "true" || lockParam === "1");
+
+ return {prefill, lock};
+ }, [firstNameParam, lastNameParam, emailParam, lockParam]);
+};