tf-checkout-react 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/api/auth.d.ts +22 -0
  2. package/dist/api/index.d.ts +1 -1
  3. package/dist/components/billing-info-container/utils.d.ts +1 -0
  4. package/dist/components/common/PhoneNumberField.d.ts +1 -1
  5. package/dist/components/loginForm/index.d.ts +1 -0
  6. package/dist/components/loginModal/index.d.ts +1 -0
  7. package/dist/components/preRegistration/PreRegistrationComplete.d.ts +2 -0
  8. package/dist/tf-checkout-react.cjs.development.js +2166 -1864
  9. package/dist/tf-checkout-react.cjs.development.js.map +1 -1
  10. package/dist/tf-checkout-react.cjs.production.min.js +1 -1
  11. package/dist/tf-checkout-react.cjs.production.min.js.map +1 -1
  12. package/dist/tf-checkout-react.esm.js +2166 -1864
  13. package/dist/tf-checkout-react.esm.js.map +1 -1
  14. package/dist/tf-checkout-styles.css +1 -1
  15. package/dist/utils/createCheckoutDataBodyWithDefaultHolder.d.ts +1 -0
  16. package/dist/validators/index.d.ts +4 -0
  17. package/package.json +2 -2
  18. package/src/api/auth.ts +49 -0
  19. package/src/api/index.ts +1 -1
  20. package/src/components/billing-info-container/index.tsx +228 -112
  21. package/src/components/billing-info-container/style.css +46 -2
  22. package/src/components/billing-info-container/utils.tsx +5 -1
  23. package/src/components/common/CustomField.tsx +1 -1
  24. package/src/components/common/PhoneNumberField.tsx +4 -2
  25. package/src/components/confirmationContainer/index.tsx +4 -0
  26. package/src/components/loginForm/index.tsx +19 -3
  27. package/src/components/loginModal/index.tsx +19 -3
  28. package/src/components/loginModal/style.css +6 -2
  29. package/src/components/preRegistration/PreRegistrationComplete.tsx +12 -2
  30. package/src/components/preRegistration/constants.tsx +6 -4
  31. package/src/components/preRegistration/index.tsx +144 -110
  32. package/src/components/preRegistration/style.css +3 -0
  33. package/src/components/preRegistration/utils.ts +9 -1
  34. package/src/components/ticketsContainer/index.tsx +79 -21
  35. package/src/components/timerWidget/style.css +2 -1
  36. package/src/types/api/common.d.ts +1 -0
  37. package/src/types/api/payment.d.ts +2 -0
  38. package/src/types/formFields.d.ts +1 -1
  39. package/src/utils/createCheckoutDataBodyWithDefaultHolder.ts +3 -1
  40. package/src/validators/index.ts +22 -1
@@ -26,6 +26,7 @@ export interface ILoginFormProps {
26
26
  showForgotPasswordButton?: boolean;
27
27
  showSignUpButton?: boolean;
28
28
  showPoweredByImage?: boolean;
29
+ registerUrl?: string;
29
30
  }
30
31
 
31
32
  // interface IUserData {
@@ -75,6 +76,7 @@ export const LoginForm: FC<ILoginFormProps> = ({
75
76
  showForgotPasswordButton = false,
76
77
  showSignUpButton = false,
77
78
  showPoweredByImage = false,
79
+ registerUrl = 'https://www.ticketfairy.com/register',
78
80
  }) => {
79
81
  const [error, setError] = useState('')
80
82
  return (
@@ -171,9 +173,23 @@ export const LoginForm: FC<ILoginFormProps> = ({
171
173
  )}
172
174
  {showSignUpButton && (
173
175
  <div className="forgot-password">
174
- <span aria-hidden="true" onClick={onSignupButtonClick}>
175
- Sign up
176
- </span>
176
+ {onSignupButtonClick !== _identity ? (
177
+ <span
178
+ aria-hidden="true"
179
+ onClick={onSignupButtonClick}
180
+ style={{ cursor: 'pointer' }}
181
+ >
182
+ Sign up
183
+ </span>
184
+ ) : (
185
+ <a
186
+ href={registerUrl}
187
+ target="_blank"
188
+ rel="noopener noreferrer"
189
+ >
190
+ Sign up
191
+ </a>
192
+ )}
177
193
  </div>
178
194
  )}
179
195
  {showPoweredByImage ? <PoweredBy /> : null}
@@ -30,6 +30,7 @@ export interface Props {
30
30
  showForgotPasswordButton?: boolean;
31
31
  showSignUpButton?: boolean;
32
32
  showPoweredByImage?: boolean;
33
+ registerUrl?: string;
33
34
  }
34
35
 
35
36
  const style: React.CSSProperties = {
@@ -57,6 +58,7 @@ export const LoginModal: FC<Props> = ({
57
58
  showForgotPasswordButton = false,
58
59
  showSignUpButton = false,
59
60
  showPoweredByImage = false,
61
+ registerUrl = 'https://www.ticketfairy.com/register',
60
62
  }) => {
61
63
  const [error, setError] = useState('')
62
64
  return (
@@ -162,9 +164,23 @@ export const LoginModal: FC<Props> = ({
162
164
  )}
163
165
  {showSignUpButton && (
164
166
  <div className="forgot-password">
165
- <span aria-hidden="true" onClick={onSignup}>
166
- Sign up
167
- </span>
167
+ {onSignup !== _identity ? (
168
+ <span
169
+ aria-hidden="true"
170
+ onClick={onSignup}
171
+ style={{ cursor: 'pointer' }}
172
+ >
173
+ Sign up
174
+ </span>
175
+ ) : (
176
+ <a
177
+ href={registerUrl}
178
+ target="_blank"
179
+ rel="noopener noreferrer"
180
+ >
181
+ Sign up
182
+ </a>
183
+ )}
168
184
  </div>
169
185
  )}
170
186
  {showPoweredByImage ? <PoweredBy /> : null}
@@ -66,9 +66,13 @@
66
66
  .forgot-password {
67
67
  text-align: center;
68
68
  }
69
- .forgot-password span {
69
+ .forgot-password span,
70
+ .forgot-password a {
70
71
  cursor: pointer;
72
+ color: inherit;
73
+ text-decoration: none;
71
74
  }
72
- .forgot-password span:hover {
75
+ .forgot-password span:hover,
76
+ .forgot-password a:hover {
73
77
  text-decoration: underline;
74
78
  }
@@ -6,6 +6,7 @@ import _get from 'lodash/get'
6
6
  import _identity from 'lodash/identity'
7
7
  import _isEmpaty from 'lodash/isEmpty'
8
8
  import React, { FC, useEffect, useState } from 'react'
9
+ import "./style.css"
9
10
 
10
11
  import {
11
12
  getPreRegistrationInfluencers,
@@ -25,6 +26,7 @@ const isWindowDefined = typeof window !== 'undefined'
25
26
  export const PreRegistrationComplete: FC<
26
27
  IPreRegistrationCompleteProps & {
27
28
  onGetConfirmationDataError?: (error: AxiosError) => void;
29
+ skipInitialValidation?: boolean;
28
30
  themeOptions?: ThemeOptions & {
29
31
  input?: CSSProperties;
30
32
  checkbox?: CSSProperties;
@@ -42,6 +44,7 @@ export const PreRegistrationComplete: FC<
42
44
  onLoginSuccess = _identity,
43
45
  logo,
44
46
  themeOptions,
47
+ skipInitialValidation = false,
45
48
  }) => {
46
49
  const themeMui = createTheme(themeOptions)
47
50
 
@@ -65,6 +68,12 @@ export const PreRegistrationComplete: FC<
65
68
  useCookieListener(X_TF_ECOMMERCE, value => setIsLoggedIn(Boolean(value)))
66
69
  useEffect(() => {
67
70
  const fetchConfirmationData = async () => {
71
+ // Skip validation if this is a freshly completed pre-registration
72
+ if (skipInitialValidation) {
73
+ onGetConfirmationDataSuccess()
74
+ return
75
+ }
76
+
68
77
  try {
69
78
  if (hash && isLoggedIn) {
70
79
  const shareOptionsData = await getPreRegistrationShareOptions({
@@ -98,6 +107,7 @@ export const PreRegistrationComplete: FC<
98
107
  onGetConfirmationDataError,
99
108
  onGetConfirmationDataSuccess,
100
109
  isLoggedIn,
110
+ skipInitialValidation,
101
111
  ])
102
112
 
103
113
  const onClose = () => {
@@ -121,9 +131,9 @@ export const PreRegistrationComplete: FC<
121
131
  {isLoggedIn && _isEmpaty(error) ? (
122
132
  <div className={`${classNamePrefix}_pre_registration_complete_container`}>
123
133
  <CopyMessageModal showCopyModal={showCopyModal} onClose={onClose} />
124
- <p className={`${classNamePrefix}_pre_registration_complete_header`}>
134
+ <h2 className={`${classNamePrefix}_pre_registration_complete_header preregistration_confirmation_header`}>
125
135
  {pageHeader}
126
- </p>
136
+ </h2>
127
137
  <div className={`${classNamePrefix}_pre_registration_complete_message`}>
128
138
  {pageMessage}
129
139
  </div>
@@ -1,5 +1,7 @@
1
1
  import React from 'react'
2
2
 
3
+ import { emailValidator } from '../../validators'
4
+
3
5
  export const getFormFieldsNotLoggedIn = (
4
6
  clientName?: string | number
5
7
  ): IFormFieldsSection[] => [
@@ -38,14 +40,14 @@ export const getFormFieldsNotLoggedIn = (
38
40
  label: 'Email',
39
41
  type: 'email',
40
42
  required: true,
41
- onValidate: null,
43
+ onValidate: emailValidator,
42
44
  },
43
45
  {
44
46
  name: 'confirmEmail',
45
47
  label: 'Confirm Email',
46
48
  type: 'email',
47
49
  required: true,
48
- onValidate: null,
50
+ onValidate: emailValidator,
49
51
  },
50
52
  ],
51
53
  },
@@ -120,14 +122,14 @@ export const getFormFieldsLoggedIn = (
120
122
  label: 'Email',
121
123
  type: 'email',
122
124
  required: true,
123
- onValidate: null,
125
+ onValidate: emailValidator,
124
126
  },
125
127
  {
126
128
  name: 'confirmEmail',
127
129
  label: 'Confirm Email',
128
130
  type: 'email',
129
131
  required: true,
130
- onValidate: null,
132
+ onValidate: emailValidator,
131
133
  },
132
134
  ],
133
135
  },
@@ -18,6 +18,7 @@ import { ForgotPasswordModal, IForgotPasswordProps } from '../forgotPasswordModa
18
18
  import { LoginModal, Props } from '../loginModal'
19
19
  import { getFormFieldsLoggedIn, getFormFieldsNotLoggedIn } from './constants'
20
20
  import { FieldsSection } from './FieldsSection'
21
+ import { PreRegistrationComplete } from './PreRegistrationComplete'
21
22
  import { getFormInitialValues, updateFormFieldsAttributes } from './utils'
22
23
 
23
24
  const X_TF_ECOMMERCE = 'X-TF-ECOMMERCE'
@@ -64,8 +65,19 @@ export const PreRegistration: FC<IPreRegistrationProps> = ({
64
65
  const [isLoggedIn, setIsLoggedIn] = useState(Boolean(getCookieByName(X_TF_ECOMMERCE)))
65
66
  const [confirmModalState, setConfirmModalState] = useState({ show: false, message: '' })
66
67
  const [, setUserData] = useState({} as IProfileData)
68
+ const [isPreRegistrationComplete, setIsPreRegistrationComplete] = useState(false)
67
69
  useCookieListener(X_TF_ECOMMERCE, value => setIsLoggedIn(Boolean(value)))
68
70
 
71
+ // Check if user already has a pre-registration when logged in
72
+ useEffect(() => {
73
+ if (!isLoggedIn || !isWindowDefined) return
74
+
75
+ const savedHash = window.localStorage.getItem(`pre-registration-hash-${eventId}`)
76
+ if (savedHash) {
77
+ setIsPreRegistrationComplete(true)
78
+ }
79
+ }, [isLoggedIn, eventId])
80
+
69
81
  const themeMui = createTheme(themeOptions)
70
82
 
71
83
  const formFieldsLoggedIn = getFormFieldsLoggedIn(CONFIGS.CLIENT_NAME)
@@ -99,7 +111,10 @@ export const PreRegistration: FC<IPreRegistrationProps> = ({
99
111
  fetchCountries()
100
112
  }, [])
101
113
 
102
- const hash = getQueryVariable('hash') || ''
114
+ const localStorageHash = isWindowDefined
115
+ ? window.localStorage.getItem('pre-registration-hash') || ''
116
+ : ''
117
+ const hash = getQueryVariable('hash') || localStorageHash
103
118
  const referrerId = getQueryVariable('referrer_id') || ''
104
119
  return (
105
120
  <ThemeProvider theme={themeMui}>
@@ -176,120 +191,139 @@ export const PreRegistration: FC<IPreRegistrationProps> = ({
176
191
  displaySuccessMessage
177
192
  />
178
193
  )}
179
- <h2>Pre-Registration</h2>
180
- <Formik
181
- initialValues={getFormInitialValues(formFields)}
182
- enableReinitialize={true}
183
- onSubmit={async (values: FormikValues) => {
184
- try {
185
- if (isLoggedIn) {
186
- if (isPreregistrationStarted) {
187
- const updatedValues = { ...values }
188
- const holderAgeDate = new Date(values.holderAge)
189
- updatedValues.dobDay = holderAgeDate.getDate()
190
- updatedValues.dobMonth = holderAgeDate.getMonth() + 1
191
- updatedValues.dobYear = holderAgeDate.getFullYear()
192
- updatedValues.referrerId = referrerId
193
- updatedValues.shareHash = hash
194
- // remove date picker string value
195
- delete updatedValues.holderAge
194
+ {isPreRegistrationComplete ? (
195
+ <PreRegistrationComplete
196
+ eventId={eventId}
197
+ logo={logo}
198
+ themeOptions={themeOptions}
199
+ onLoginSuccess={onLoginSuccess}
200
+ data={{} as IShareOptionsData}
201
+ classNamePrefix=""
202
+ onLinkCopied={_identity}
203
+ updateShareActionData={_identity}
204
+ hash={hash}
205
+ shareActionData={{} as ISubmitShareActionAttributes}
206
+ skipInitialValidation={true}
207
+ />
208
+ ) : (
209
+ <>
210
+ <h2>Pre-Registration</h2>
211
+ <Formik
212
+ initialValues={getFormInitialValues(formFields)}
213
+ enableReinitialize={true}
214
+ onSubmit={async (values: FormikValues) => {
215
+ try {
216
+ if (isLoggedIn) {
217
+ if (isPreregistrationStarted) {
218
+ const updatedValues = { ...values }
219
+ const holderAgeDate = new Date(values.holderAge)
220
+ updatedValues.dobDay = holderAgeDate.getDate()
221
+ updatedValues.dobMonth = holderAgeDate.getMonth() + 1
222
+ updatedValues.dobYear = holderAgeDate.getFullYear()
223
+ updatedValues.referrerId = referrerId
224
+ updatedValues.shareHash = hash
225
+ // remove date picker string value
226
+ delete updatedValues.holderAge
196
227
 
197
- const confirmationData = await confirmPreRegistration(
198
- eventId,
199
- updatedValues as IConfirmPreRegistrationRequestData
200
- )
201
- if (isWindowDefined) {
202
- window.localStorage.setItem(
203
- 'pre-registration-hash',
204
- _get(confirmationData, 'attributes.hash')
205
- )
206
- }
228
+ const confirmationData = await confirmPreRegistration(
229
+ eventId,
230
+ updatedValues as IConfirmPreRegistrationRequestData
231
+ )
232
+ if (isWindowDefined) {
233
+ window.localStorage.setItem(
234
+ `pre-registration-hash-${eventId}`,
235
+ _get(confirmationData, 'attributes.hash')
236
+ )
237
+ }
207
238
 
208
- onConfirmationSuccess(confirmationData)
209
- } else {
210
- setConfirmModalState({
211
- show: true,
212
- message: 'The preregistration has not started',
213
- })
214
- }
215
- } else {
216
- const bodyFormData = new FormData()
217
- bodyFormData.append('first_name', values.firstName)
218
- bodyFormData.append('last_name', values.lastName)
219
- bodyFormData.append('email', values.email)
220
- bodyFormData.append('confirm_email', values.confirmEmail)
221
- bodyFormData.append('zip', values.zip)
222
- bodyFormData.append('country', values.country)
223
- bodyFormData.append('password', values.password)
224
- bodyFormData.append('password_confirmation', values.confirmPassword)
225
- bodyFormData.append('client_id', CONFIGS.CLIENT_ID)
226
- bodyFormData.append('client_secret', CONFIGS.CLIENT_SECRET)
227
- bodyFormData.append('register_for', 'prereg')
239
+ setIsPreRegistrationComplete(true)
240
+ onConfirmationSuccess(confirmationData)
241
+ } else {
242
+ setConfirmModalState({
243
+ show: true,
244
+ message: 'The preregistration has not started',
245
+ })
246
+ }
247
+ } else {
248
+ const bodyFormData = new FormData()
249
+ bodyFormData.append('first_name', values.firstName)
250
+ bodyFormData.append('last_name', values.lastName)
251
+ bodyFormData.append('email', values.email)
252
+ bodyFormData.append('confirm_email', values.confirmEmail)
253
+ bodyFormData.append('zip', values.zip)
254
+ bodyFormData.append('country', values.country)
255
+ bodyFormData.append('password', values.password)
256
+ bodyFormData.append('password_confirmation', values.confirmPassword)
257
+ bodyFormData.append('client_id', CONFIGS.CLIENT_ID)
258
+ bodyFormData.append('client_secret', CONFIGS.CLIENT_SECRET)
259
+ bodyFormData.append('register_for', 'prereg')
228
260
 
229
- const res = await register(bodyFormData)
230
- const profileRes = await getProfileData()
231
- onLoginSuccess(res.data)
232
- if (isWindowDefined) {
233
- window.localStorage.setItem(
234
- 'user_data',
235
- JSON.stringify(_get(profileRes, 'data'))
236
- )
237
- setUserData(_get(profileRes, 'data'))
238
- }
239
- }
240
- } catch (e) {
241
- const error = e as AxiosError
242
- let errorMessage = error?.message || 'Error'
243
- const emailErrors = _get(error, 'response.data.message.email') || ''
244
- const errorDataMessage = _get(error, 'response.data.message')
261
+ const res = await register(bodyFormData)
262
+ const profileRes = await getProfileData()
263
+ onLoginSuccess(res.data)
264
+ if (isWindowDefined) {
265
+ window.localStorage.setItem(
266
+ 'user_data',
267
+ JSON.stringify(_get(profileRes, 'data'))
268
+ )
269
+ setUserData(_get(profileRes, 'data'))
270
+ }
271
+ }
272
+ } catch (e) {
273
+ const error = e as AxiosError
274
+ let errorMessage = error?.message || 'Error'
275
+ const emailErrors = _get(error, 'response.data.message.email') || ''
276
+ const errorDataMessage = _get(error, 'response.data.message')
245
277
 
246
- if (typeof errorDataMessage === 'string') {
247
- errorMessage = errorDataMessage
248
- }
249
- if (emailErrors?.length > 0) {
250
- if (emailErrors[0] === 'The email is already used') {
251
- setShowModalLogin(true)
252
- setAlreadyHasUser(true)
253
- }
254
- } else {
255
- setError(errorMessage)
256
- }
278
+ if (typeof errorDataMessage === 'string') {
279
+ errorMessage = errorDataMessage
280
+ }
281
+ if (emailErrors?.length > 0) {
282
+ if (emailErrors[0] === 'The email is already used') {
283
+ setShowModalLogin(true)
284
+ setAlreadyHasUser(true)
285
+ }
286
+ } else {
287
+ setError(errorMessage)
288
+ }
257
289
 
258
- onConfirmationError(error)
259
- }
260
- }}
261
- >
262
- {props => (
263
- <Form>
264
- <div className="login-modal-body">
265
- <FieldsSection
266
- formFields={formFields}
267
- values={props.values}
268
- setFieldValue={props.setFieldValue}
269
- containerClass="pre-registration"
270
- countries={countries}
271
- themeOptions={themeOptions}
272
- />
273
- </div>
274
- <div className="button-container">
275
- <Button
276
- type="submit"
277
- variant="contained"
278
- className="login-register-button"
279
- disabled={props.isSubmitting}
280
- >
281
- {props.isSubmitting ? (
282
- <CircularProgress size={26} />
283
- ) : isLoggedIn ? (
284
- 'Confirm Pre-Registration'
285
- ) : (
286
- 'Create Account'
287
- )}
288
- </Button>
289
- </div>
290
- </Form>
291
- )}
292
- </Formik>
290
+ onConfirmationError(error)
291
+ }
292
+ }}
293
+ >
294
+ {props => (
295
+ <Form>
296
+ <div className="login-modal-body">
297
+ <FieldsSection
298
+ formFields={formFields}
299
+ values={props.values}
300
+ setFieldValue={props.setFieldValue}
301
+ containerClass="pre-registration"
302
+ countries={countries}
303
+ themeOptions={themeOptions}
304
+ />
305
+ </div>
306
+ <div className="button-container">
307
+ <Button
308
+ type="submit"
309
+ variant="contained"
310
+ className="login-register-button"
311
+ disabled={props.isSubmitting}
312
+ >
313
+ {props.isSubmitting ? (
314
+ <CircularProgress size={26} />
315
+ ) : isLoggedIn ? (
316
+ 'Confirm Pre-Registration'
317
+ ) : (
318
+ 'Create Account'
319
+ )}
320
+ </Button>
321
+ </div>
322
+ </Form>
323
+ )}
324
+ </Formik>
325
+ </>
326
+ )}
293
327
  </div>
294
328
  </ThemeProvider>
295
329
  )
@@ -0,0 +1,3 @@
1
+ .preregistration_confirmation_header {
2
+ margin-bottom: 2rem;
3
+ }
@@ -6,7 +6,11 @@ import _isEmpty from 'lodash/isEmpty'
6
6
  import _map from 'lodash/map'
7
7
  import _split from 'lodash/split'
8
8
 
9
- import { combineValidators, requiredValidator } from '../../validators'
9
+ import {
10
+ combineValidators,
11
+ passwordValidator,
12
+ requiredValidator,
13
+ } from '../../validators'
10
14
  import { IShareButton } from '../confirmationContainer'
11
15
 
12
16
  export const getValidateFunctions = ({
@@ -26,6 +30,10 @@ export const getValidateFunctions = ({
26
30
  validationFunctions.push(element.onValidate)
27
31
  }
28
32
 
33
+ if (element.name === 'password') {
34
+ validationFunctions.push(passwordValidator)
35
+ }
36
+
29
37
  if (element.name === 'confirmEmail') {
30
38
  const isSameEmail = (confirmEmail?: string) =>
31
39
  values.email !== confirmEmail ? 'Please confirm your email address correctly' : null
@@ -48,8 +48,8 @@ import ConfirmModal from '../confirmModal'
48
48
  import Countdown from '../countdown'
49
49
  import { VerificationPendingModal } from '../idVerificationContainer/VerificationPendingModal'
50
50
  import { LoginModal } from '../loginModal'
51
- import WaitingList from '../waitingList'
52
51
  import { PreRegistration } from '../preRegistration'
52
+ import WaitingList from '../waitingList'
53
53
  import { AccessCodeSection } from './AccessCodeSection'
54
54
  import { PromoCodeSection } from './PromoCodeSection'
55
55
  import { ReferralLogic } from './ReferralLogic'
@@ -417,6 +417,41 @@ export const TicketsContainer = ({
417
417
  const handleTicketSelect = (key: string, value: number | string, isTable = false) => {
418
418
  localStorage.setItem('selectedTicketsQuantity', value.toString())
419
419
  setSelectedTickets(prevState => {
420
+ // Allow multiple ticket types to be selected simultaneously when flag is enabled
421
+ if (event?.allowMultipleTicketTypePurchases === true) {
422
+ // Check if we're switching between tables and regular tickets
423
+ const hasExistingSelection = Object.keys(prevState).some(k => k !== 'isTable')
424
+ const switchingTicketType = hasExistingSelection && prevState.isTable !== isTable
425
+
426
+ // If switching from tables to regular tickets or vice versa, clear all selections
427
+ if (switchingTicketType && Number(value) > 0) {
428
+ return {
429
+ [key]: value,
430
+ isTable,
431
+ }
432
+ }
433
+
434
+ // If value is 0, remove this ticket from selection
435
+ if (!value || Number(value) === 0) {
436
+ const newState = { ...prevState }
437
+ delete newState[key]
438
+ // If no ticket keys remain (only isTable left), return empty state
439
+ const ticketKeys = Object.keys(newState).filter(k => k !== 'isTable')
440
+ if (ticketKeys.length === 0) {
441
+ return { isTable: false } as ISelectedTickets
442
+ }
443
+ return { ...newState, isTable: prevState.isTable }
444
+ }
445
+
446
+ // If value > 0, add or update this ticket while keeping others of the same type selected
447
+ return {
448
+ ...prevState,
449
+ [key]: value,
450
+ isTable,
451
+ }
452
+ }
453
+
454
+ // Default behavior: only one ticket type at a time
420
455
  if (Object.keys(prevState)[0] !== key && !value) {
421
456
  return prevState
422
457
  }
@@ -441,32 +476,55 @@ export const TicketsContainer = ({
441
476
  const timeSlotTickets = _flatten(_map(timeSlotGroups, slots => slots))
442
477
 
443
478
  setHandleBookIsLoading(true)
444
- const ticket = event?.isTimeSlotEvent
445
- ? _find(timeSlotTickets || [], item => Number(selectedTickets[item.id]) > 0) ||
446
- ({} as ITicket)
447
- : _find(tickets, item => Number(selectedTickets[item.id]) > 0) || ({} as ITicket)
448
- const optionName = _get(ticket, 'optionName')
449
- const ticketId = _get(ticket, 'id')
450
- const productCartQuantity = +selectedTickets[ticket.id]
451
- const ticketQuantity = +selectedTickets[ticket.id]
479
+
480
+ // Unified flow: works for both single and multiple ticket types
481
+ const ticketsList = event?.isTimeSlotEvent ? timeSlotTickets : tickets
482
+
483
+ // Get all selected ticket IDs with quantity > 0 (excluding 'isTable' key)
484
+ const selectedTicketIds = Object.keys(selectedTickets).filter(
485
+ key => key !== 'isTable' && Number(selectedTickets[key]) > 0
486
+ )
487
+
488
+ // Build ticket_types object with all selected tickets (works for 1 or N tickets)
489
+ const ticketTypesData: any = {}
490
+ let totalProductCartQuantity = 0
491
+ let firstTicket: ITicket | null = null
492
+
493
+ selectedTicketIds.forEach(ticketId => {
494
+ const ticket = _find(ticketsList || [], item => String(item.id) === ticketId) as ITicket
495
+ if (ticket) {
496
+ if (!firstTicket) firstTicket = ticket
497
+ const optionName = _get(ticket, 'optionName')
498
+ const quantity = +selectedTickets[ticketId]
499
+ totalProductCartQuantity += quantity
500
+
501
+ ticketTypesData[ticketId] = {
502
+ product_options: {
503
+ [optionName]: ticketId,
504
+ ticket_price: ticket.price,
505
+ },
506
+ quantity,
507
+ }
508
+ }
509
+ })
510
+
511
+ if (!firstTicket) {
512
+ setHandleBookIsLoading(false)
513
+ return
514
+ }
515
+
516
+ const firstOptionName = _get(firstTicket, 'optionName')
517
+ const firstTicketId = _get(firstTicket, 'id')
452
518
 
453
519
  const data: ICartRequestData = {
454
520
  attributes: {
455
521
  alternative_view_id: null,
456
- product_cart_quantity: productCartQuantity,
522
+ product_cart_quantity: totalProductCartQuantity,
457
523
  product_options: {
458
- [optionName]: ticketId,
524
+ [firstOptionName]: firstTicketId,
459
525
  },
460
526
  product_id: eventId,
461
- ticket_types: {
462
- [ticketId]: {
463
- product_options: {
464
- [optionName]: ticketId,
465
- ticket_price: ticket.price,
466
- },
467
- quantity: ticketQuantity,
468
- },
469
- },
527
+ ticket_types: ticketTypesData,
470
528
  },
471
529
  }
472
530
 
@@ -502,7 +560,7 @@ export const TicketsContainer = ({
502
560
  : {}
503
561
 
504
562
  const checkoutBody = createCheckoutDataBodyWithDefaultHolder(
505
- ticketQuantity,
563
+ totalProductCartQuantity,
506
564
  userData
507
565
  )
508
566