/* eslint-disable @typescript-eslint/no-explicit-any */ import './style.css' import { CircularProgress, ThemeOptions } from '@mui/material' import Backdrop from '@mui/material/Backdrop' import Button from '@mui/material/Button' import { createTheme, ThemeProvider } from '@mui/material/styles' import { CSSProperties } from '@mui/styles' import { Stripe, StripeElementsOptions } from '@stripe/stripe-js' import axios, { AxiosError } from 'axios' import { Field, Form, Formik, FormikHelpers, FormikProps, FormikValues } from 'formik' import _find from 'lodash/find' import _forEach from 'lodash/forEach' import _get from 'lodash/get' import _identity from 'lodash/identity' import _isEmpty from 'lodash/isEmpty' import _isEqual from 'lodash/isEqual' import _map from 'lodash/map' import { nanoid } from 'nanoid' import React, { FC, useCallback, useEffect, useRef, useState } from 'react' import { AttributesConfig, checkEmailExists, getCart, getCheckoutPageConfigs, getCountries, getPaymentData, getProfileData, getStates, postOnCheckout, setCustomHeader, } from '../../api' import { updateCheckout } from '../../api/checkout' import { withCustomFields } from '../../hoc' import { usePixel } from '../../hooks/usePixel' import { IBillingInfoData } from '../../types' import { ICheckoutResponseData } from '../../types/api/checkout' import { createCheckoutDataBodyWithDefaultHolder, deleteCookieByName, getCookieByName, isBrowser, setLoggedUserData, } from '../../utils' import { ErrorFocus } from '../../utils/formikErrorFocus' import { emailRegex } from '../../validators' import { AddonsContainter, IAddonContainterProps } from '../addonsContainer' import SimpleAddonsContainer from '../addonsContainer/SimpleAddonsContainer' import SnackbarAlert from '../common/SnackbarAlert' import { ForgotPasswordModal } from '../forgotPasswordModal' import { VerificationPendingModal } from '../idVerificationContainer/VerificationPendingModal' import { LoginModal } from '../loginModal' import { IPaymentPage, PaymentContainer } from '../paymentContainer' import { SignupModal } from '../signupModal' import TimerWidget from '../timerWidget' import { usePaymentRedirect, useStripePayment } from './hooks' import { assingUniqueIds, createCheckoutDataBody, filterBillingInfoFields, getFieldComponent, getFieldLabel, getInitialValues, getValidateFunctions, ICheckoutBody, renderComponentWithProps, } from './utils' export interface IBillingInfoPage { data?: IBillingInfoData[]; ticketHoldersFields?: IBillingInfoData; handleSubmit?: ( values: FormikValues, formikHelpers: FormikHelpers, eventId: any, res: any, checkoutUpdateResponse?: any, paymentResponse?: any ) => void; onRegisterSuccess?: (value: any) => void; onRegisterError?: (e: AxiosError, email: string) => void; onSubmitError?: (e: AxiosError) => void; onGetCartSuccess?: (res: any) => void; onGetCartError?: (e: AxiosError) => void; onGetCountriesSuccess?: (res: any) => void; onGetCountriesError?: (e: AxiosError) => void; onGetStatesSuccess?: (res: any) => void; onGetStatesError?: (e: AxiosError) => void; onGetProfileDataSuccess?: (res: any) => void; onGetProfileDataError?: (e: AxiosError) => void; onGetCheckoutConfigsSuccess?: (res: any) => void; onGetCheckoutConfigsError?: (e: AxiosError) => void; onLogin?: () => void; onLoginSuccess?: () => void; onErrorClose?: () => void; onCheckoutUpdateSuccess?: (res: any) => void; onCheckoutUpdateError?: (e: AxiosError) => void; initialValues?: FormikValues; buttonName?: string; freeOrderButtonName?: string; theme?: 'light' | 'dark'; isLoggedIn?: boolean; accountInfoTitle?: string | JSX.Element; hideLogo?: boolean; themeOptions?: ThemeOptions & { input?: CSSProperties; checkbox?: CSSProperties; }; hideErrorsAlertSection?: boolean; onSkipBillingPage: (data: any) => void; skipPage?: boolean; canSkipHolderNames?: boolean; shouldFetchCountries?: boolean; onForgotPasswordSuccess?: (res: any) => void; onForgotPasswordError?: (e: AxiosError) => void; onCountdownFinish?: () => void; enableTimer?: boolean; logo?: string; showForgotPasswordButton?: boolean; showSignUpButton?: boolean; brandOptIn?: boolean; showPoweredByImage?: boolean; isCountryCodeEditable?: boolean; customFieldsOrderKeys?: string[]; customFieldsTicketHolderKeys?: string[]; onPendingVerification?: () => void; includeAddons?: boolean; addonsProps?: IAddonContainterProps; addOnDataWithCustomFields?: any; isSinglePageCheckout?: boolean; paymentProps?: Partial; paymentSectionAddon?: React.ReactNode; } const LogicRunner: FC<{ brandOptIn?: boolean; values: any; errors: any; setStates: React.Dispatch; setFieldValue: any; setValues: any; setUserValues: any; onGetStatesSuccess: any; onGetStatesError: any; shouldFetchCountries: boolean; }> = ({ values, setStates, setFieldValue, setValues, setUserValues, onGetStatesSuccess, onGetStatesError, shouldFetchCountries, brandOptIn, }) => { const prevCountry = useRef(values.country) const prevBuyerData = useRef({ firstName: '', lastName: '', email: '', phone: '', }) const hasLoadedUserData = useRef(false) useEffect(() => { const fetchStates = async () => { try { const res = await getStates(values.country) const mappedStates = _map(res.data, (item, key) => ({ label: item, value: key, })) setStates(mappedStates) if (prevCountry.current !== values.country) { const stateExists = mappedStates.find( state => state.value === values.state )?.value setFieldValue('state', stateExists ?? mappedStates[0]?.value ?? '') prevCountry.current = values.country } onGetStatesSuccess(res.data) } catch (e) { if (axios.isAxiosError(e)) { onGetStatesError(e) } } } shouldFetchCountries && fetchStates() }, [values.country, setStates, setFieldValue]) // Auto-fill first holder with buyer data useEffect(() => { const buyerFirstName = values.firstName || '' const buyerLastName = values.lastName || '' const buyerEmail = values.email || '' const buyerPhone = values.phone || '' // Check if any buyer data has changed const firstNameChanged = prevBuyerData.current.firstName !== buyerFirstName const lastNameChanged = prevBuyerData.current.lastName !== buyerLastName const emailChanged = prevBuyerData.current.email !== buyerEmail const phoneChanged = prevBuyerData.current.phone !== buyerPhone // Update holder fields individually based on what changed if (firstNameChanged) { setFieldValue('holderFirstName-0', buyerFirstName, false) prevBuyerData.current.firstName = buyerFirstName } if (lastNameChanged) { setFieldValue('holderLastName-0', buyerLastName, false) prevBuyerData.current.lastName = buyerLastName } if (emailChanged) { setFieldValue('holderEmail-0', buyerEmail, false) prevBuyerData.current.email = buyerEmail } if (phoneChanged) { setFieldValue('holderPhone-0', buyerPhone, false) prevBuyerData.current.phone = buyerPhone } }, [values.firstName, values.lastName, values.email, values.phone, setFieldValue]) const userDataEncoded = isBrowser ? window.localStorage.getItem('user_data') : '' useEffect(() => { // set user data from local storage only on initial load const getStoredUserData = () => { if (isBrowser && userDataEncoded && !hasLoadedUserData.current) { try { const parsedData = JSON.parse(userDataEncoded) const mappedValues = { firstName: parsedData?.first_name || parsedData?.firstName || '', lastName: parsedData?.last_name || parsedData?.lastName || '', email: parsedData?.email || '', phone: parsedData?.phone || '', confirmEmail: parsedData?.email || '', state: parsedData?.state || '', street_address: parsedData?.street_address || '', country: parsedData?.country || '1', zip: parsedData?.zip || '', brand_opt_in: brandOptIn ? brandOptIn : parsedData?.brand_opt_in || false, city: parsedData?.city || '', confirmPassword: '', password: '', 'holderFirstName-0': parsedData?.first_name || parsedData?.firstName || '', 'holderLastName-0': parsedData?.last_name || parsedData?.lastName || '', 'holderEmail-0': parsedData?.email || '', 'holderPhone-0': parsedData?.phone || '', } const extraDataJSON = window.localStorage.getItem('extraData') const extraData = extraDataJSON ? JSON.parse(extraDataJSON) : null setValues({ ...values, ...mappedValues, ...(extraData ?? {}) }) setUserValues(mappedValues) hasLoadedUserData.current = true } catch (e) {} } } getStoredUserData() // Only run on mount or when userDataEncoded first becomes available // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return null } // Component to check if email exists /** * A utility component that checks if an email address already exists in the system. * * Performs debounced validation and existence checking for user email addresses. * Only checks when both email and confirmEmail match, are valid, and user is not logged in. * * @param email - The primary email address to check * @param confirmEmail - The confirmation email address (must match email) * @param isLoggedIn - Whether the user is currently logged in * @param setEmailExists - Callback function to update the email existence state * * @remarks * - Uses a 500ms debounce to avoid excessive API calls * - Validates email format using regex before checking existence * - Automatically returns false if user is logged in * - Returns null as it's a logic-only component with no UI */ const EmailExistenceChecker: FC<{ email: string; confirmEmail: string; isLoggedIn: boolean; setEmailExists: (exists: boolean) => void; }> = ({ email, confirmEmail, isLoggedIn, setEmailExists }) => { useEffect(() => { const checkEmail = async () => { // Don't check if user is logged in if (isLoggedIn) { setEmailExists(false) return } // Only check if both emails exist, match, and are valid if (!email || !confirmEmail || email !== confirmEmail || !emailRegex.test(email)) { setEmailExists(false) return } // Check if email exists try { const result = await checkEmailExists(email) setEmailExists(result.exists) } catch { setEmailExists(false) } } // Debounce the check const timeoutId = setTimeout(() => { checkEmail() }, 800) return () => clearTimeout(timeoutId) }, [email, confirmEmail, isLoggedIn, setEmailExists]) return null } const BillingInfoContainer = React.memo( ({ data = [], ticketHoldersFields = { id: 1, fields: [], }, initialValues = {}, buttonName = 'Submit', freeOrderButtonName = 'Complete Registration', handleSubmit = _identity, theme = 'light', onRegisterSuccess = _identity, onRegisterError = _identity, onSubmitError = _identity, onGetCartSuccess = _identity, onGetCartError = _identity, onGetCountriesSuccess = _identity, onGetCountriesError = _identity, onGetStatesSuccess = _identity, onGetStatesError = _identity, onGetProfileDataSuccess = _identity, onGetProfileDataError = _identity, onLogin, onLoginSuccess = _identity, onCheckoutUpdateSuccess = _identity, onCheckoutUpdateError = _identity, isLoggedIn: pIsLoggedIn = false, accountInfoTitle = '', hideLogo, themeOptions, onErrorClose = _identity, hideErrorsAlertSection = false, onSkipBillingPage = _identity, skipPage = false, canSkipHolderNames = false, onForgotPasswordSuccess = _identity, onForgotPasswordError = _identity, shouldFetchCountries = true, onCountdownFinish = _identity, enableTimer = false, logo, showForgotPasswordButton = true, showSignUpButton = false, brandOptIn = false, showPoweredByImage = false, customFieldsOrderKeys, customFieldsTicketHolderKeys, isCountryCodeEditable = true, onPendingVerification = _identity, onGetCheckoutConfigsSuccess = _identity, onGetCheckoutConfigsError = _identity, includeAddons = false, addonsProps, addOnDataWithCustomFields, isSinglePageCheckout = false, paymentProps = {}, paymentSectionAddon, }: IBillingInfoPage) => { const [extraData, setExtraData] = useState(null) const [isConfigLoading, setIsConfigLoading] = useState(true) const [configs, setConfigs] = useState(null) const isNewUser = false const themeMui = createTheme(themeOptions) const elementsRef = useRef(null) const stripeRef = useRef(null) useEffect(() => { if (isBrowser) { const extraData = window.localStorage.getItem('extraData') if (extraData) { setExtraData(JSON.parse(extraData)) } } getCheckoutPageConfigs() .then(data => { if (data?.data?.attributes) { setConfigs(data.data.attributes) setIsConfigLoading(false) } onGetCheckoutConfigsSuccess(data) }) .catch(e => { setIsConfigLoading(false) onGetCheckoutConfigsError(e) }) }, [isBrowser]) const defaultCountry = isBrowser ? window.localStorage.getItem('eventCountry') : '' const userData = isBrowser && window.localStorage.getItem('user_data') ? JSON.parse(window.localStorage.getItem('user_data') || '{}') : {} const additionalConfigs = isBrowser && window.localStorage.getItem('checkoutAdditionalConfigs') ? JSON.parse( window.localStorage.getItem('checkoutAdditionalConfigs') || '{"resale": false, "resaleWithAddons": false}' ) : { resale: false, resaleWithAddons: false } const [dataWithUniqueIds, setDataWithUniqueIds] = useState( assingUniqueIds(data) ) const xtfCookie = getCookieByName('X-TF-ECOMMERCE') const [isLoggedIn, setIsLoggedIn] = useState(!!(pIsLoggedIn || xtfCookie)) const [cartInfoData, setCartInfo] = useState({}) const [countries, setCountries] = useState([]) const [states, setStates] = useState([]) const [showModalLogin, setShowModalLogin] = useState(false) const alreadyHasUser= false const [userExpired, setUserExpired] = useState(false) const [showModalSignup, setShowModalSignup] = useState(false) const [showModalForgotPassword, setShowModalForgotPassword] = useState(false) const [ticketsQuantity, setTicketsQuantity] = useState([]) const [userValues, setUserValues] = useState({ firstName: '', lastName: '', email: '', phone: '', confirmEmail: '', holderFirstName: '', holderLastName: '', holderAge: '', city: '', country: '', street_address: '', state: '', zip: '', }) const [loading, setLoading] = useState(true) const [cardLoading, setCardLoading] = useState(false) const [isCountriesLoading, setIsCountriesLoading] = useState(true) const [error, setError] = useState(null) const [phoneValidationIsLoading, setPhoneValidationIsLoading] = useState(false) const [emailExists, setEmailExists] = useState(false) const emailLogged = _get(userData, 'email', '') || _get(userValues, 'email', '') const firstNameLogged = _get(userData, 'first_name', '') || _get(userValues, 'first_name', '') const lastNameLogged = _get(userData, 'last_name', '') || _get(userValues, 'last_name', '') const phoneLogged = _get(userData, 'phone', '') || _get(userValues, 'phone', '') const showDOB = configs?.age_required const showTicketHolders = configs?.names_required const eventId = configs?.event_id const optedInFieldValue: boolean = brandOptIn ? brandOptIn : _get(cartInfoData, 'optedIn', false) const ttfOptIn = Boolean(_get(cartInfoData, 'ttfOptIn', false)) const isTable = Boolean(_get(cartInfoData, 'is_table', false)) const hideTtfOptIn: boolean = _get(cartInfoData, 'hide_ttf_opt_in', true) const expirationTime = _get(cartInfoData, 'expiresAt') const flagRequirePhone = configs?.phone_required const collectMandatoryWalletAddress = configs?.collect_mandatory_wallet_address const collectOptionalWalletAddress = configs?.collect_optional_wallet_address const flagFreeTicket = configs?.free_ticket const hidePhoneField = configs?.hide_phone_field const hideWalletAddressField = !collectOptionalWalletAddress && !collectMandatoryWalletAddress const collectMandatoryCompany = configs?.collect_mandatory_company const collectOptionalCompany = configs?.collect_optional_company const hideCompanyField = !collectOptionalCompany && !collectMandatoryCompany const collectMandatoryJobTitle = configs?.collect_mandatory_job_title const collectOptionalJobTitle = configs?.collect_optional_job_title const hideJobTitleField = !collectMandatoryJobTitle && !collectOptionalJobTitle const collectMandatoryInstagram = configs?.collect_mandatory_instagram const collectOptionalInstagram = configs?.collect_optional_instagram const hideInstagramField = !collectMandatoryInstagram && !collectOptionalInstagram const collectMandatoryBusinessCategory = configs?.collect_mandatory_business_category const collectOptionalBusinessCategory = configs?.collect_optional_business_category const eventHasAddons = configs?.has_add_on const addonMaxQuantityGroups = configs?.addon_max_quantity_groups ?? undefined const hideBusinessCategoryField = !collectMandatoryBusinessCategory && !collectOptionalBusinessCategory const [pendingVerificationMessage, setPendingVerificationMessage] = useState() const [reviewData, setReviewData] = useState({}) const [checkoutData, setCheckoutData] = useState({}) const [checkoutUpdateData, setCheckoutUpdateData] = useState(null) const prevData = useRef(data) const addAddOnsInAttributes = useCallback((checkoutBody: any) => { const selectedAddOns = window.localStorage.getItem('add_ons') || '{}' const addOnDataCapture = window.localStorage.getItem('add_on_data_capture') || '{}' checkoutBody.attributes.add_ons = JSON.parse(selectedAddOns) checkoutBody.attributes.add_on_data_capture = JSON.parse(addOnDataCapture) }, []) const [singleCheckoutAddons, setSingleCheckoutAddOns] = useState({}) const orderIsFree = !Number(checkoutData?.total) useEffect(() => { const hasUniqueId = _get(data, '[0].uniqueId') const isEqualData = _isEqual(prevData.current, data) if (!hasUniqueId || !isEqualData) { setDataWithUniqueIds(assingUniqueIds(data)) if (!isEqualData) { prevData.current = data } } }, [data]) const getQuantity = useCallback((cart: any = []) => { let qty = 0 cart.forEach((item: any) => { qty += +item.quantity }) return qty }, []) useEffect(() => { if (pIsLoggedIn !== isLoggedIn || xtfCookie) { setIsLoggedIn(!!(pIsLoggedIn || xtfCookie)) } }, [pIsLoggedIn, isLoggedIn, xtfCookie]) //just once useEffect(() => { // fetch countries data const fetchCountries = async () => { try { const res = await getCountries() setCustomHeader(res) setCountries(res.data) setIsCountriesLoading(false) onGetCountriesSuccess(res.data) } catch (e) { if (axios.isAxiosError(e)) { onGetCountriesError(e) } setIsCountriesLoading(false) } } shouldFetchCountries && fetchCountries() fetchCart() // Initialize checkout with event_id on first load if (isSinglePageCheckout && eventId) { updateCheckoutWithAddOns() } return () => { isBrowser && localStorage.removeItem('selectedTicketsQuantity') } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ eventId, isSinglePageCheckout, shouldFetchCountries, onGetCountriesSuccess, onGetCountriesError, ]) // fetch cart data const fetchCart = async () => { try { setCardLoading(true) const res = await getCart() setCustomHeader(res) const cartInfo = res.data.attributes setCartInfo(cartInfo) const { cart = [] } = cartInfo setTicketsQuantity(new Array(getQuantity(cart)).fill(null).map(() => nanoid())) onGetCartSuccess(res.data) } catch (e) { if (axios.isAxiosError(e)) { onGetCartError(e) } } finally { setCardLoading(false) } } // fetch user data const fetchUserData = async () => { try { if (isLoggedIn) { const userDataResponse = await getProfileData() const profileSpecifiedData = _get(userDataResponse, 'data') const profileDataObj = setLoggedUserData(profileSpecifiedData) setUserValues({ ...profileDataObj, firstName: profileDataObj.first_name, lastName: profileDataObj.last_name, }) window.localStorage.setItem('user_data', JSON.stringify(profileDataObj)) onGetProfileDataSuccess(userDataResponse.data) } } catch (e) { if (axios.isAxiosError(e)) { onGetProfileDataError(e) } } } useEffect(() => { fetchUserData() fetchCart() }, [isLoggedIn]) useEffect(() => { const fetchCheckoutUpdate = async () => { if (!eventId) return try { const checkoutUpdateResponse = await updateCheckout({ attributes: { event_id: eventId, is_from_resale: additionalConfigs?.resale, }, }) if (checkoutUpdateResponse.success) { const checkoutAttributes = checkoutUpdateResponse.data.attributes const cartPriceBreakdown = _get( checkoutAttributes, 'cart_price_breakdown', {} ) localStorage.setItem( 'checkoutData', JSON.stringify({ hash: '', total: _get(cartPriceBreakdown, 'total', 0) }) ) setCheckoutUpdateData(checkoutAttributes) onCheckoutUpdateSuccess({ expires_at: expirationTime, ...cartPriceBreakdown }) } } catch (error) { console.error('Failed to fetch checkout update:', error) } } fetchCheckoutUpdate() }, [eventId, additionalConfigs?.resale]) useEffect(() => { const collectPaymentData = async () => { if ( skipPage && !_isEmpty(ticketsQuantity) && !showDOB && !loading && !isNewUser ) { setLoading(true) const checkoutBody = createCheckoutDataBodyWithDefaultHolder( ticketsQuantity.length, userData ) try { if (isBrowser) { addAddOnsInAttributes(checkoutBody) } const checkoutResponse = await postOnCheckout(checkoutBody, flagFreeTicket) removeReferralKey() removeAdditionalConfigs() onSkipBillingPage(checkoutResponse.data.attributes) setLoading(false) } catch (e) { onSubmitError(e as AxiosError) if (_get(e, 'response.data.data.hasUnverifiedOrder')) { setPendingVerificationMessage(_get(e, 'response.data.message')) } } } else { setLoading(false) } } collectPaymentData() }, [skipPage, ticketsQuantity]) const collectCheckoutBody = useCallback( (values: Record, profileData?: any): Record => { let checkoutBody = {} as ICheckoutBody // Auto collect ticket holders name when it was skipped optionally if (showDOB && !showTicketHolders && canSkipHolderNames) { checkoutBody = createCheckoutDataBodyWithDefaultHolder( ticketsQuantity.length, values, true, { emailLogged, firstNameLogged, lastNameLogged, phoneLogged } ) } else { checkoutBody = createCheckoutDataBody( ticketsQuantity.length, values, { emailLogged: emailLogged || profileData.email, firstNameLogged: firstNameLogged || profileData.first_name || profileData.firstName, lastNameLogged: lastNameLogged || profileData.last_name || profileData.lastName, phoneLogged: phoneLogged || profileData.phone, }, showDOB ) } // Collect data_capture for Order Custom Fields const data_capture: Record = {} _forEach(customFieldsOrderKeys, (key: string) => { data_capture[key] = values[key] // Delete from values list delete checkoutBody.attributes[key] }) // Temp solution for hardcoded data capture values, to be deleted in the future const captureKeys = [ 'businessCategory', 'company', 'instagram', 'jobTitle', 'wallet_address', ] captureKeys.forEach(key => { const path = `data_capture.${key}` const value = _get(values, path, '') if (value !== '') { data_capture[key] = value } const attributeKey = `data_capture[${key}]` delete checkoutBody.attributes?.[attributeKey] }) // Collect data_capture for Ticket Holders Fields _forEach(checkoutBody.attributes.ticket_holders, (holder, holderIndex) => { const holderDataCapture: Record = {} _forEach(customFieldsTicketHolderKeys, customFieldKey => { const customFieldHolderName = `${customFieldKey}-${holderIndex}` const customFieldHolderKey = values[customFieldHolderName] || '' holderDataCapture[customFieldKey] = customFieldHolderKey // Delete holder specific value from values list delete checkoutBody.attributes[customFieldHolderName] }) // Assign Ticket Holder data_capture final value if (!_isEmpty(holderDataCapture)) { holder.ticket_data_capture = holderDataCapture } }) // Collect add_on_data_capture for Add-on Custom Fields const add_on_data_capture: Record = {} _forEach(values, (val: any, key: string) => { // Matches keys like: `${addonId}-${groupId}-${fieldKey}` created in AddonComponent const match = key.match(/^(\d+)-\d+-(.+)$/) if (match) { const addonId = match[1] const fieldKey = match[2] // Initialize addon bucket if (!add_on_data_capture[addonId]) { add_on_data_capture[addonId] = {} } add_on_data_capture[addonId][fieldKey] = val // Remove the raw form key from attributes to avoid leaking internals delete checkoutBody.attributes[key] } }) // Also include add_on_data_capture from localStorage for single-page checkout if (isBrowser) { const storedAddOnDataCapture = localStorage.getItem('add_on_data_capture') if (storedAddOnDataCapture) { const parsed = JSON.parse(storedAddOnDataCapture) Object.keys(parsed).forEach(addonId => { if (!add_on_data_capture[addonId]) { add_on_data_capture[addonId] = {} } Object.keys(parsed[addonId]).forEach(fieldKey => { add_on_data_capture[addonId][fieldKey] = parsed[addonId][fieldKey] }) }) } } const mergedAttributes: Record = { ...checkoutBody.attributes, data_capture, } if (!_isEmpty(add_on_data_capture)) { mergedAttributes.add_on_data_capture = add_on_data_capture } return { attributes: mergedAttributes } }, [ showDOB, showTicketHolders, canSkipHolderNames, customFieldsOrderKeys, ticketsQuantity.length, emailLogged, firstNameLogged, lastNameLogged, phoneLogged, customFieldsTicketHolderKeys, ] ) const removeReferralKey = useCallback(() => { localStorage.removeItem('referral_key') }, []) const removeAdditionalConfigs = useCallback(() => { localStorage.removeItem('checkoutAdditionalConfigs') }, []) useEffect(() => { onCheckoutUpdateSuccess({ expires_at: expirationTime, ...checkoutData }) }, [checkoutData, cartInfoData]) // Initialize payment hooks usePaymentRedirect({ stripeRef, setError, setLoading, removeReferralKey, removeAdditionalConfigs, handleSubmit, isBrowser, }) const { processPayment } = useStripePayment({ stripeRef, elementsRef, setError, isBrowser, }) const updateCheckoutWithAddOns = useCallback( async (addOns: { [key: string]: number } = {}) => { if (!isSinglePageCheckout) return const mergedAddOns = { ...singleCheckoutAddons } // Update existing entries and add new ones Object.entries(addOns).forEach(([key, value]) => { const amount = Number(value) if (amount) { mergedAddOns[key] = amount } else { delete mergedAddOns[key] } }) try { const checkoutUpdateData = { attributes: { event_id: eventId, add_ons: mergedAddOns, is_from_resale: additionalConfigs?.resale, }, } const checkoutResponse = await updateCheckout(checkoutUpdateData) if (checkoutResponse.success) { const checkoutDataObj = _get( checkoutResponse, 'data.attributes.cart_price_breakdown', {} ) setCheckoutData(checkoutDataObj) setSingleCheckoutAddOns(mergedAddOns) const updatedAttributes = _get(checkoutResponse, 'data.attributes', {}) if (updatedAttributes.additional_payment_information) { setCheckoutUpdateData(updatedAttributes) } } } catch (error) { const errorMessage = _get( error, 'response.data.message', 'Failed to update add-ons' ) setError(errorMessage) onCheckoutUpdateError(error as AxiosError) } }, [eventId, isSinglePageCheckout, onCheckoutUpdateError, onCheckoutUpdateSuccess] ) const handleAddOnSelect = useCallback( async (selectedAddOns: { [key: string]: number }) => { await updateCheckoutWithAddOns(selectedAddOns) }, [updateCheckoutWithAddOns] ) const onAddOnSelect = useCallback( (id: string, value: string, _addon: any, fieldUpdates: Record = {}) => { const quantity = parseInt(value) || 0 const addonId = id // Get current add-ons and their custom fields const currentAddOns = JSON.parse(localStorage.getItem('add_ons') || '{}') const currentDataCapture = JSON.parse( localStorage.getItem('add_on_data_capture') || '{}' ) // Update quantities const updatedAddOns = { ...currentAddOns } updatedAddOns[addonId] = quantity // Update custom fields if any const updatedDataCapture = { ...currentDataCapture } if (Object.keys(fieldUpdates).length > 0) { updatedDataCapture[addonId] = { ...(updatedDataCapture[addonId] || {}), ...fieldUpdates, } } if (quantity === 0 && updatedDataCapture[addonId]) { delete updatedDataCapture[addonId] } // Save to localStorage localStorage.setItem('add_ons', JSON.stringify(updatedAddOns)) localStorage.setItem('add_on_data_capture', JSON.stringify(updatedDataCapture)) // Call handleAddOnSelect with the updated addons object handleAddOnSelect(updatedAddOns) }, [handleAddOnSelect] ) if (loading || (enableTimer && !expirationTime && isBrowser)) { if (expirationTime === 0) { // Redirect to homepage (countdown finished and browser reloaded case) window.location.href = '/' } } const selectedCountry = _find(countries, item => item.code.toLowerCase() === defaultCountry) || {} const initialCountry = selectedCountry.id || _get(userData, 'countryId', '') || '1' const pageUrl = isBrowser ? window.location.href.split('?')[0] : '' usePixel(eventId || cartInfoData?.eventId, { page: 'billing', pageUrl }) if (isTable) { dataWithUniqueIds[0].label = 'Get Your Tables' } const stripePublishableKey = reviewData?.payment_method?.stripe_publishable_key || checkoutUpdateData?.additional_payment_information?.basic_config?.apiKey const stripeAccountId = reviewData?.payment_method?.stripe_connected_account || checkoutUpdateData?.additional_payment_information?.basic_config?.accountId const addOnsIncludedOnInvitation = additionalConfigs.resale && additionalConfigs.resaleWithAddons const elementsOptions: StripeElementsOptions = { ...checkoutUpdateData?.additional_payment_information?.elements_config, } if (loading || cardLoading || isCountriesLoading || isConfigLoading || !eventId) { return ( ) } return ( {!!expirationTime && enableTimer && ( )} {!isCountriesLoading && !isConfigLoading && ( { try { // Validation: if phone is required for ticket holders, mark errors and stop submit const flagRequirePhoneLocal = Boolean(configs?.phone_required) if (flagRequirePhoneLocal) { const holdersCount = ticketsQuantity.length let hasHolderPhoneError = false for (let i = 0; i < holdersCount; i++) { const fieldName = `holderPhone-${i}` const value = _get(values, fieldName, '') if (!value) { hasHolderPhoneError = true formikHelpers.setFieldTouched(fieldName, true, false) formikHelpers.setFieldError(fieldName, 'This field is required') } } if (hasHolderPhoneError) { // Do not proceed with submit; Formik will show errors and our PhoneNumberField now shows errors when submitCount > 0 return } } if ((!elementsRef.current || !stripeRef.current) && !orderIsFree) { setError('Fill in the payment details') return } if (isSinglePageCheckout && !orderIsFree) { // For PaymentElement, we'll use confirmPayment directly // No need to create payment method separately } if (isBrowser) { window.localStorage.setItem( 'extraData', JSON.stringify({ data_capture: { instagram: _get(values, 'data_capture.instagram', ''), company: _get(values, 'data_capture.company', ''), businessCategory: _get( values, 'data_capture.businessCategory', '' ), jobTitle: _get(values, 'data_capture.jobTitle', ''), }, }) ) } // Guest checkout: no need to register, just get profile if logged in let userDataObj = userData if (isLoggedIn) { try { const profileData = await getProfileData() const profileSpecifiedData = _get(profileData, 'data') userDataObj = setLoggedUserData(profileSpecifiedData) } catch (e) { // If profile fetch fails, use values from form userDataObj = { email: values.email, first_name: values.firstName, last_name: values.lastName, phone: values.phone, } } } else { // For guest checkout, use form values userDataObj = { email: values.email, first_name: values.firstName, last_name: values.lastName, phone: values.phone, } } if (isBrowser) { window.localStorage.setItem('user_data', JSON.stringify(userDataObj)) } const checkoutBody = collectCheckoutBody(values, userDataObj) if (isBrowser && !isSinglePageCheckout) { addAddOnsInAttributes(checkoutBody) } if (isSinglePageCheckout) { checkoutBody.attributes.add_ons = singleCheckoutAddons // Include add_on_data_capture for single-page checkout const storedAddOnDataCapture = localStorage.getItem('add_on_data_capture') if (storedAddOnDataCapture) { checkoutBody.attributes.add_on_data_capture = JSON.parse(storedAddOnDataCapture) } } const checkoutResponse = await postOnCheckout( checkoutBody, flagFreeTicket ) const checkoutUpdateResponse = await updateCheckout({ attributes: { event_id: eventId, add_ons: checkoutBody?.attributes?.add_ons ?? [], is_from_resale: additionalConfigs?.resale, }, }) setCheckoutUpdateData(checkoutUpdateResponse.data.attributes) let paymentResponse = null if (isSinglePageCheckout) { const { hash, total } = checkoutResponse.data.attributes localStorage.setItem('checkoutData', JSON.stringify({ hash, total })) const paymentDataResponse = await getPaymentData(String(hash)) if (paymentDataResponse.success) { const { attributes } = paymentDataResponse.data setReviewData(attributes) const { order_details, cart } = attributes const { tickets: [ticket], } = order_details const updatedOrderData = { add_ons: order_details.add_ons || [], total: order_details.total, subtotal: order_details.subtotal, fees: order_details.fees, pay_now: order_details.pay_now || '', id: order_details?.id, product_name: cart[0]?.product_name, ticketType: ticket?.name, quantity: ticket?.quantity, price: ticket?.price, currency: order_details?.currency, guest_count: order_details?.guest_count || '', debt: order_details?.debt || null, cost: ticket?.cost, } const isFreeTickets = (!Number(total) && !Number(updatedOrderData.total)) || !Number(updatedOrderData?.pay_now || 0) // Process payment using the hook paymentResponse = await processPayment( paymentDataResponse, values, formikHelpers, checkoutResponse, checkoutUpdateResponse, { attributes, isFreeTickets, updatedOrderData, eventId, } ) if (!paymentResponse && !isFreeTickets) { // Payment failed or redirected, don't continue return } } } removeReferralKey() removeAdditionalConfigs() handleSubmit( values, formikHelpers as FormikHelpers, eventId, checkoutResponse, checkoutUpdateResponse, paymentResponse ) } catch (e) { setLoading(false) onSubmitError(e as AxiosError) const hasUnverifiedOrder = _get( e, 'response.data.data.hasUnverifiedOrder' ) const message = _get(e, 'response.data.message', {}) as | { password?: string | null, email?: string | null } | string if (hasUnverifiedOrder && typeof message === 'string') { setPendingVerificationMessage(message) } else if (axios.isAxiosError(e)) { if ( (e as any).response?.status === 401 || _get(e, 'response.data.error') === 'invalid_token' ) { if (isBrowser) { window.localStorage.removeItem('user_data') setUserExpired(true) setShowModalLogin(true) setIsLoggedIn(false) setShowModalSignup(false) setShowModalForgotPassword(false) const event = new window.CustomEvent('tf-logout') deleteCookieByName('X-TF-ECOMMERCE') window.document.dispatchEvent(event) } } if (message && !hideErrorsAlertSection && typeof message === 'string') { setError(message) } onSubmitError(e) } // Keep form values intact - don't reset formikHelpers.setSubmitting(false) } finally { setLoading(false) } }} > {(props: FormikProps) => (
{!!error && ( { setError(null) onErrorClose() }} /> )} {!isLoggedIn && !isSinglePageCheckout && (
{accountInfoTitle}
Login & skip ahead:
{!hideLogo && (
nodata
)}
)} {isSinglePageCheckout && !addOnsIncludedOnInvitation && eventHasAddons && eventId ? ( ) : !addOnsIncludedOnInvitation && includeAddons && !isSinglePageCheckout ? ( ) : null} {!cardLoading && _map(dataWithUniqueIds, item => { const { label, labelClassName, fields } = item return (

{label}

{_map(fields, group => { const { groupClassname, groupItems } = group const filteredGroupItems = filterBillingInfoFields( groupItems, { showDOB: Boolean(showDOB), hideTtfOptIn: Boolean(hideTtfOptIn), hidePhoneField: Boolean(hidePhoneField), flagRequirePhone: Boolean(flagRequirePhone), collectMandatoryWalletAddress: Boolean( collectMandatoryWalletAddress ), collectMandatoryJobTitle: Boolean( collectMandatoryJobTitle ), collectMandatoryBusinessCategory: Boolean( collectMandatoryBusinessCategory ), collectMandatoryCompany: Boolean(collectMandatoryCompany), collectMandatoryInstagram: Boolean( collectMandatoryInstagram ), flagFreeTicket: Boolean(flagFreeTicket), hideWalletAddressField: Boolean(hideWalletAddressField), hideJobTitleField: Boolean(hideJobTitleField), hideBusinessCategoryField: Boolean( hideBusinessCategoryField ), hideCompanyField: Boolean(hideCompanyField), hideInstagramField, } ) return (
{_map(filteredGroupItems, element => { // Hide password fields in single-page checkout or when user is logged in const shouldHidePasswordFields = ['password', 'confirmPassword', 'password-info'].includes( element.name ) && (isLoggedIn || isSinglePageCheckout) return shouldHidePasswordFields ? null : (
{element.component ? ( typeof element.component === 'function' ? ( renderComponentWithProps( element.component as React.ComponentType, element.name === 'payment_info' ? { reviewData } : {} ) ) : ( element.component ) ) : ( ({ value: item.id, label: item.name, })) : element.name === 'state' ? [ { label: element.label, value: '', disabled: true, }, ...states, ] : element.selectOptions || [] } theme={theme} defaultCountry={ defaultCountry || element.defaultCountry } dateFormat={element.format} isCountryCodeEditable={ isCountryCodeEditable } /> )}
) })}
{/* Show login suggestion if email is registered */} {group.groupItems.some(el => el.name === 'confirmEmail') && emailExists && !isLoggedIn && (

✓ This email is already registered. Log in to auto-fill your information

)}
) })}
) })} {!_isEmpty(ticketHoldersFields.fields) && (

{ticketHoldersFields.label}

{_map(ticketsQuantity, (_item, index) => (
Ticket {index + 1}
{_map(ticketHoldersFields.fields, group => { const { id, groupClassname, groupItems } = group return (
{_map(groupItems, element => (
))}
) })}
))}
)}
{isSinglePageCheckout && !orderIsFree && stripePublishableKey && ( { stripeRef.current = stripe elementsRef.current = elements }} paymentFields={paymentProps.paymentFields || []} onPaymentError={paymentProps.onPaymentError || _identity} handlePayment={paymentProps.handlePayment || _identity} onGetPaymentDataSuccess={ paymentProps.onGetPaymentDataSuccess || _identity } onGetPaymentDataError={ paymentProps.onGetPaymentDataError || _identity } themeOptions={themeOptions} paymentInfoLabel="" displayPaymentButton={false} hidePaymentForm={false} hideFieldsBlock={true} isSinglePageCheckout={true} /> )} {paymentSectionAddon}
)}
)} {showModalLogin && ( { setShowModalLogin(false) }} onLogin={() => { setShowModalLogin(false) setUserExpired(false) onLoginSuccess() }} alreadyHasUser={alreadyHasUser} userExpired={userExpired} onGetProfileDataSuccess={(data: any) => { fetchCart() onGetProfileDataSuccess(data) }} onGetProfileDataError={onGetProfileDataError} showSignUpButton={showSignUpButton} showForgotPasswordButton={showForgotPasswordButton} onForgotPassword={() => { setShowModalLogin(false) setShowModalForgotPassword(true) }} onSignup={() => { setShowModalLogin(false) setShowModalSignup(true) }} showPoweredByImage={showPoweredByImage} /> )} {showModalSignup && ( { setShowModalSignup(false) }} onLogin={() => { setShowModalSignup(false) setShowModalLogin(true) }} onRegisterSuccess={onRegisterSuccess} onRegisterError={onRegisterError} showPoweredByImage={showPoweredByImage} /> )} {showModalForgotPassword && ( { setShowModalForgotPassword(false) }} onLoginButtonClick={() => { setShowModalForgotPassword(false) setShowModalLogin(true) }} onForgotPasswordSuccess={onForgotPasswordSuccess} onForgotPasswordError={onForgotPasswordError} showPoweredByImage={showPoweredByImage} displaySuccessMessage /> )} { onPendingVerification() }} />
) } ) const WithCustomFieldsBillingInfoContainer = withCustomFields(BillingInfoContainer) export { WithCustomFieldsBillingInfoContainer as BillingInfoContainer }