tf-checkout-react 1.7.30 → 1.7.32-beta.0

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.
@@ -8,6 +8,8 @@ export interface IGroupItem {
8
8
  component?: ReactNode | JSX.Element | HTMLElement;
9
9
  onValidate?: (value: any) => void;
10
10
  uniqueId?: string;
11
+ /** Ticket custom fields only: ticket types this field is scoped to. */
12
+ ticketTypeIds?: string[] | null;
11
13
  [key: string]: any;
12
14
  }
13
15
  export interface IFieldData {
@@ -17,6 +19,8 @@ export interface IFieldData {
17
19
  groupLabelClassName?: string;
18
20
  id?: number;
19
21
  uniqueId?: string;
22
+ /** Set on the ticket-holder group built from event custom fields. */
23
+ customFields?: boolean;
20
24
  }
21
25
  export interface IBillingInfoData {
22
26
  fields: IFieldData[];
@@ -33,3 +33,23 @@ export declare const getAddOnDataWithCustomFields: (customFields: any) => {
33
33
  };
34
34
  };
35
35
  export declare const getFieldsKeys: (customFields: any) => any[];
36
+ /**
37
+ * Ticket type of each holder slot, in holder order, expanded from the checkout
38
+ * breakdown (`tickets_price_breakdown`: one line per ticket type with a quantity).
39
+ * Returns [] when the breakdown doesn't line up with the holder count — callers
40
+ * then treat every holder's ticket type as unknown and show all fields.
41
+ */
42
+ interface ITicketsPriceBreakdownLine {
43
+ ticket_type_id?: string | number | null;
44
+ quantity?: string | number;
45
+ }
46
+ export declare const getHolderTicketTypeIds: (ticketsPriceBreakdown: ITicketsPriceBreakdownLine[] | undefined, holdersCount: number) => string[];
47
+ /**
48
+ * Whether a ticket-holder custom field applies to a ticket of `ticketTypeId`.
49
+ * Unscoped fields apply to every ticket; an unknown ticket type fails open so a
50
+ * required field is never silently dropped.
51
+ */
52
+ export declare const customFieldAppliesToTicketType: (field: {
53
+ ticketTypeIds?: string[] | null;
54
+ }, ticketTypeId?: string | undefined) => boolean;
55
+ export {};
@@ -10,6 +10,6 @@ export { replaceVarInString } from './replaceVarInString';
10
10
  export { isBrowser } from './isBrowser';
11
11
  export { getFormInitialValues } from './form';
12
12
  export { setLoggedUserData, logoutUser } from './auth';
13
- export { getDataWithCustomFields, getFieldsKeys } from './customFields';
13
+ export { customFieldAppliesToTicketType, getDataWithCustomFields, getFieldsKeys, getHolderTicketTypeIds, } from './customFields';
14
14
  export { createElementFromHTML } from './htmlNodeFromString';
15
15
  export { isJson } from './jsonUtils';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.7.30",
2
+ "version": "1.7.32-beta.0",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -0,0 +1,69 @@
1
+ import { customFieldsDataAdapter } from './customFields'
2
+
3
+ // `data.attributes` of GET v1/event/:id/custom_fields for an event whose only
4
+ // ticket field is scoped to two ticket types.
5
+ const scopedTicketFieldPayload = [
6
+ {
7
+ ticket: {
8
+ group: {
9
+ id: '2242',
10
+ label: null,
11
+ image: '',
12
+ type: 'ticket',
13
+ order: '0',
14
+ fields: [
15
+ {
16
+ id: '6766',
17
+ image: '',
18
+ type: 'select',
19
+ name: 'shoe_size',
20
+ order: '1',
21
+ label: 'Cycling Shoe Size',
22
+ defaultValue: '',
23
+ required: true,
24
+ description: null,
25
+ settings: {
26
+ showOnMyTickets: true,
27
+ showOnPdf: false,
28
+ showOnScan: false,
29
+ allowUpdate: false,
30
+ ticketTypes: [225685, 225687],
31
+ },
32
+ options: [
33
+ { id: '9650', name: '34', value: '34' },
34
+ { id: '9651', name: '36', value: '36' },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ },
40
+ },
41
+ ]
42
+
43
+ describe('customFieldsDataAdapter', () => {
44
+ it('keeps the ticket types a ticket field is scoped to', () => {
45
+ const { ticketsFields } = customFieldsDataAdapter(scopedTicketFieldPayload)
46
+
47
+ expect(ticketsFields).toHaveLength(1)
48
+ expect(ticketsFields[0]).toMatchObject({
49
+ name: 'shoe_size',
50
+ required: true,
51
+ ticketTypeIds: ['225685', '225687'],
52
+ })
53
+ })
54
+
55
+ it('leaves unscoped fields without ticketTypeIds', () => {
56
+ const payload = [
57
+ {
58
+ ticket: {
59
+ group: {
60
+ fields: [{ name: 'nickname', type: 'text', settings: { ticketTypes: [] } }],
61
+ },
62
+ },
63
+ },
64
+ ]
65
+ const { ticketsFields } = customFieldsDataAdapter(payload)
66
+
67
+ expect(ticketsFields[0]).not.toHaveProperty('ticketTypeIds')
68
+ })
69
+ })
@@ -13,6 +13,11 @@ export const optionsAdapter = (options: any) => {
13
13
  return adaptedOptions
14
14
  }
15
15
 
16
+ // `settings.ticketTypes` scopes a ticket field to those ticket types; absent or
17
+ // empty means it applies to every ticket. Ids arrive as numbers, compare as strings.
18
+ export const ticketTypeIdsAdapter = (ticketTypes: unknown): string[] | null =>
19
+ Array.isArray(ticketTypes) && ticketTypes.length ? ticketTypes.map(String) : null
20
+
16
21
  export const fieldDataAdapter = (field: IGroupItem) => {
17
22
  const {
18
23
  name,
@@ -34,6 +39,11 @@ export const fieldDataAdapter = (field: IGroupItem) => {
34
39
  value: _get(restValues, 'defaultValue'),
35
40
  }
36
41
 
42
+ const ticketTypeIds = ticketTypeIdsAdapter(_get(restValues, 'settings.ticketTypes'))
43
+ if (ticketTypeIds) {
44
+ adaptedField.ticketTypeIds = ticketTypeIds
45
+ }
46
+
37
47
  switch (type) {
38
48
  case 'textarea':
39
49
  adaptedField.type = 'text'
@@ -16,8 +16,9 @@ import _identity from 'lodash/identity'
16
16
  import _isEmpty from 'lodash/isEmpty'
17
17
  import _isEqual from 'lodash/isEqual'
18
18
  import _map from 'lodash/map'
19
+ import _omit from 'lodash/omit'
19
20
  import { nanoid } from 'nanoid'
20
- import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
21
+ import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
21
22
 
22
23
  import {
23
24
  AttributesConfig,
@@ -34,12 +35,14 @@ import {
34
35
  import { updateCheckout } from '../../api/checkout'
35
36
  import { withCustomFields } from '../../hoc'
36
37
  import { usePixel } from '../../hooks/usePixel'
37
- import { IBillingInfoData } from '../../types'
38
+ import { IBillingInfoData, IGroupItem } from '../../types'
38
39
  import { ICheckoutResponseData } from '../../types/api/checkout'
39
40
  import {
40
41
  createCheckoutDataBodyWithDefaultHolder,
42
+ customFieldAppliesToTicketType,
41
43
  deleteCookieByName,
42
44
  getCookieByName,
45
+ getHolderTicketTypeIds,
43
46
  isBrowser,
44
47
  setLoggedUserData,
45
48
  } from '../../utils'
@@ -124,6 +127,8 @@ export interface IBillingInfoPage {
124
127
  isCountryCodeEditable?: boolean;
125
128
  customFieldsOrderKeys?: string[];
126
129
  customFieldsTicketHolderKeys?: string[];
130
+ /** Event to load custom fields for. Defaults to `?event_id`, then the cart's event. */
131
+ customFieldsEventId?: string | number;
127
132
  onPendingVerification?: () => void;
128
133
  includeAddons?: boolean;
129
134
  addonsProps?: IAddonContainterProps;
@@ -506,6 +511,17 @@ const BillingInfoContainer = React.memo(
506
511
  useState<ICheckoutResponseData | null>(null)
507
512
  const prevData = useRef(data)
508
513
 
514
+ // Ticket type of each holder slot — scopes ticket custom fields that list
515
+ // `ticketTypes` in their settings to the holders of those ticket types.
516
+ const holderTicketTypeIds = useMemo(
517
+ () =>
518
+ getHolderTicketTypeIds(
519
+ _get(checkoutUpdateData, 'cart_price_breakdown.tickets_price_breakdown'),
520
+ ticketsQuantity.length
521
+ ),
522
+ [checkoutUpdateData, ticketsQuantity.length]
523
+ )
524
+
509
525
  const addAddOnsInAttributes = useCallback((checkoutBody: any) => {
510
526
  const selectedAddOns = window.localStorage.getItem('add_ons') || '{}'
511
527
  const addOnDataCapture = window.localStorage.getItem('add_on_data_capture') || '{}'
@@ -756,13 +772,21 @@ const BillingInfoContainer = React.memo(
756
772
  })
757
773
 
758
774
  // Collect data_capture for Ticket Holders Fields
775
+ const ticketCustomGroup = _find(ticketHoldersFields.fields, 'customFields')
776
+ const ticketCustomFields: any[] = ticketCustomGroup?.groupItems || []
759
777
  _forEach(checkoutBody.attributes.ticket_holders, (holder, holderIndex) => {
760
778
  const holderDataCapture: Record<string, any> = {}
761
779
 
762
780
  _forEach(customFieldsTicketHolderKeys, customFieldKey => {
763
781
  const customFieldHolderName = `${customFieldKey}-${holderIndex}`
764
- const customFieldHolderKey = values[customFieldHolderName] || ''
765
- holderDataCapture[customFieldKey] = customFieldHolderKey
782
+ const field = _find(ticketCustomFields, ({ name }) => name === customFieldKey)
783
+ // Fields scoped to other ticket types were never shown to this holder.
784
+ if (
785
+ !field ||
786
+ customFieldAppliesToTicketType(field, holderTicketTypeIds[+holderIndex])
787
+ ) {
788
+ holderDataCapture[customFieldKey] = values[customFieldHolderName] || ''
789
+ }
766
790
 
767
791
  // Delete holder specific value from values list
768
792
  delete checkoutBody.attributes[customFieldHolderName]
@@ -833,6 +857,8 @@ const BillingInfoContainer = React.memo(
833
857
  lastNameLogged,
834
858
  phoneLogged,
835
859
  customFieldsTicketHolderKeys,
860
+ ticketHoldersFields,
861
+ holderTicketTypeIds,
836
862
  ]
837
863
  )
838
864
 
@@ -1512,40 +1538,58 @@ const BillingInfoContainer = React.memo(
1512
1538
  <div key={_item}>
1513
1539
  <h5>Ticket {index + 1}</h5>
1514
1540
  {_map(ticketHoldersFields.fields, group => {
1515
- const { id, groupClassname, groupItems } = group
1541
+ const { id, groupClassname } = group
1542
+ const groupItems = group.customFields
1543
+ ? group.groupItems.filter(item =>
1544
+ customFieldAppliesToTicketType(
1545
+ item,
1546
+ holderTicketTypeIds[index]
1547
+ )
1548
+ )
1549
+ : group.groupItems
1550
+ if (_isEmpty(groupItems)) return null
1516
1551
  return (
1517
1552
  <div key={id}>
1518
1553
  <div className={groupClassname}>
1519
- {_map(groupItems, element => (
1520
- <div className={element.className} key={element.name}>
1521
- <Field
1522
- {...element}
1523
- type={
1524
- element.type === 'radio' ||
1525
- element.type === 'checkbox'
1526
- ? undefined
1527
- : element.type
1528
- }
1529
- name={`${element.name}-${index}`}
1530
- label={getFieldLabel(element, configs)}
1531
- component={getFieldComponent(element)}
1532
- validate={getValidateFunctions(
1533
- element,
1534
- states,
1535
- props.values,
1536
- props.errors
1537
- )}
1538
- setPhoneValidationIsLoading={
1539
- setPhoneValidationIsLoading
1540
- }
1541
- fill={element.type === 'phone' ? true : undefined}
1542
- defaultCountry={
1543
- defaultCountry || element.defaultCountry
1544
- }
1545
- isCountryCodeEditable={isCountryCodeEditable}
1546
- />
1547
- </div>
1548
- ))}
1554
+ {_map(groupItems, item => {
1555
+ // Scoping metadata, not a Field prop.
1556
+ const element = _omit(item, 'ticketTypeIds') as IGroupItem
1557
+ return (
1558
+ <div
1559
+ className={element.className}
1560
+ key={element.name}
1561
+ >
1562
+ <Field
1563
+ {...element}
1564
+ type={
1565
+ element.type === 'radio' ||
1566
+ element.type === 'checkbox'
1567
+ ? undefined
1568
+ : element.type
1569
+ }
1570
+ name={`${element.name}-${index}`}
1571
+ label={getFieldLabel(element, configs)}
1572
+ component={getFieldComponent(element)}
1573
+ validate={getValidateFunctions(
1574
+ element,
1575
+ states,
1576
+ props.values,
1577
+ props.errors
1578
+ )}
1579
+ setPhoneValidationIsLoading={
1580
+ setPhoneValidationIsLoading
1581
+ }
1582
+ fill={
1583
+ element.type === 'phone' ? true : undefined
1584
+ }
1585
+ defaultCountry={
1586
+ defaultCountry || element.defaultCountry
1587
+ }
1588
+ isCountryCodeEditable={isCountryCodeEditable}
1589
+ />
1590
+ </div>
1591
+ )
1592
+ })}
1549
1593
  </div>
1550
1594
  </div>
1551
1595
  )
@@ -74,11 +74,11 @@ export const TicketsSection = ({
74
74
  const ticketFinalPrice = ticket.feeIncluded && !ticket.isForward
75
75
  ? +(ticket.x_face_value || ticket.basePrice)
76
76
  : ticket.feeIncluded ? +ticket.basePrice : +ticket.cost
77
- const ticketFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketFinalPrice)}`
77
+ const ticketFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketFinalPrice.toFixed(2))}`
78
78
  const ticketOldFinalPrice = ticket.feeIncluded && !ticket.isForward
79
79
  ? +(ticket.x_face_value || ticket.basePrice)
80
80
  : ticket.feeIncluded ? +ticket.oldBasePrice : +ticket.oldCost
81
- const ticketOldFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketOldFinalPrice)}`
81
+ const ticketOldFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketOldFinalPrice.toFixed(2))}`
82
82
  const ticketFeeAmount = (+ticket.basePrice - +ticket.cost).toFixed(2)
83
83
  const ticketBreakdown = !ticket.feeIncluded
84
84
  ? '(+ fees at checkout)'
@@ -214,11 +214,11 @@ export const TicketsSection = ({
214
214
  const ticketFinalPrice = ticket.feeIncluded && !ticket.isForward
215
215
  ? +(ticket.x_face_value || ticket.basePrice)
216
216
  : ticket.feeIncluded ? +ticket.basePrice : +ticket.cost
217
- const ticketFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketFinalPrice)}`
217
+ const ticketFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketFinalPrice.toFixed(2))}`
218
218
  const ticketOldFinalPrice = ticket.feeIncluded && !ticket.isForward
219
219
  ? +(ticket.x_face_value || ticket.basePrice)
220
220
  : ticket.feeIncluded ? +ticket.oldBasePrice : +ticket.oldCost
221
- const ticketOldFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketOldFinalPrice)}`
221
+ const ticketOldFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketOldFinalPrice.toFixed(2))}`
222
222
  const ticketFeeAmount = (+ticket.basePrice - +ticket.cost).toFixed(2)
223
223
  const ticketBreakdown = !ticket.feeIncluded
224
224
  ? '(+ fees at checkout)'
@@ -66,11 +66,11 @@ export const TimeSlotTicketRow = ({
66
66
  const ticketFinalPrice = ticket.feeIncluded && !ticket.isForward
67
67
  ? +(ticket.x_face_value || ticket.basePrice)
68
68
  : ticket.feeIncluded ? +ticket.basePrice : +ticket.cost
69
- const ticketFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketFinalPrice)}`
69
+ const ticketFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketFinalPrice.toFixed(2))}`
70
70
  const ticketOldFinalPrice = ticket.feeIncluded && !ticket.isForward
71
71
  ? +(ticket.x_face_value || ticket.basePrice)
72
72
  : ticket.feeIncluded ? +ticket.oldBasePrice : +ticket.oldCost
73
- const ticketOldFinalPriceFormatted = `${priceSymbol} ${Math.round(ticketOldFinalPrice)}`
73
+ const ticketOldFinalPriceFormatted = `${priceSymbol} ${parseFloat(ticketOldFinalPrice.toFixed(2))}`
74
74
  const ticketFeeAmount = (+ticket.basePrice - +ticket.cost).toFixed(2)
75
75
  const ticketBreakdown = !ticket.feeIncluded
76
76
  ? '(+ fees at checkout)'
@@ -3,7 +3,7 @@ import _get from 'lodash/get'
3
3
  import _identity from 'lodash/identity'
4
4
  import React, { useEffect, useState } from 'react'
5
5
 
6
- import { getCustomFields } from '../../api'
6
+ import { getCheckoutPageConfigs, getCustomFields } from '../../api'
7
7
  import {
8
8
  getDataWithCustomFields,
9
9
  getFieldsKeys,
@@ -19,6 +19,7 @@ const withCustomFields = (Children: any) => (props: any) => {
19
19
  fields: [],
20
20
  },
21
21
  skipCustomFields = true,
22
+ customFieldsEventId: eventIdProp,
22
23
  onGetCustomFieldsError = _identity,
23
24
  onGetCustomFieldsSuccess = _identity,
24
25
  ...args
@@ -30,9 +31,23 @@ const withCustomFields = (Children: any) => (props: any) => {
30
31
  const [customFieldsTicketHolderKeys, setCustomFieldsTicketHolderKeys] = useState([] as any)
31
32
  const [customFieldsAddOnKeys, setCustomFieldsAddOnKeys] = useState([] as any)
32
33
 
34
+ // The billing page URL often carries no event id (path routes, single-page
35
+ // checkouts), so fall back to the event the cart session belongs to. Without
36
+ // any id, the request would hit `/v1/event//custom_fields` and fail.
37
+ const resolveEventId = async (): Promise<string> => {
38
+ const fromUrl = getQueryVariable('event_id') || getQueryVariable('eventId')
39
+ if (eventIdProp || fromUrl) return String(eventIdProp || fromUrl)
40
+
41
+ const configs = await getCheckoutPageConfigs()
42
+ const fromCart = _get(configs, 'data.attributes.event_id')
43
+ return fromCart ? String(fromCart) : ''
44
+ }
45
+
33
46
  const getFields = async () => {
34
47
  try {
35
- const eventId = getQueryVariable('event_id') || getQueryVariable("eventId") || ''
48
+ const eventId = await resolveEventId()
49
+ if (!eventId) return
50
+
36
51
  const customFields = await getCustomFields(eventId)
37
52
 
38
53
  const orderFieldsKeys = getFieldsKeys(_get(customFields, 'orderFields', []))
@@ -11,6 +11,8 @@ export interface IGroupItem {
11
11
  component?: ReactNode | JSX.Element | HTMLElement;
12
12
  onValidate?: (value: any) => void;
13
13
  uniqueId?: string;
14
+ /** Ticket custom fields only: ticket types this field is scoped to. */
15
+ ticketTypeIds?: string[] | null;
14
16
 
15
17
  // aditional props
16
18
  [key: string]: any;
@@ -25,6 +27,8 @@ export interface IFieldData {
25
27
  groupLabelClassName?: string;
26
28
  id?: number;
27
29
  uniqueId?: string;
30
+ /** Set on the ticket-holder group built from event custom fields. */
31
+ customFields?: boolean;
28
32
  }
29
33
  export interface IBillingInfoData {
30
34
  fields: IFieldData[];
@@ -0,0 +1,41 @@
1
+ import { customFieldAppliesToTicketType, getHolderTicketTypeIds } from './customFields'
2
+
3
+ describe('getHolderTicketTypeIds', () => {
4
+ it('expands the breakdown into one ticket type per holder, in order', () => {
5
+ const breakdown = [
6
+ { ticket_type_id: 225685, quantity: 2 },
7
+ { ticket_type_id: 225690, quantity: 1 },
8
+ ]
9
+
10
+ expect(getHolderTicketTypeIds(breakdown, 3)).toEqual(['225685', '225685', '225690'])
11
+ })
12
+
13
+ it('returns [] when the breakdown does not match the holder count', () => {
14
+ expect(getHolderTicketTypeIds([{ ticket_type_id: 1, quantity: 2 }], 3)).toEqual([])
15
+ })
16
+
17
+ it('returns [] without a breakdown', () => {
18
+ expect(getHolderTicketTypeIds(undefined, 2)).toEqual([])
19
+ })
20
+ })
21
+
22
+ describe('customFieldAppliesToTicketType', () => {
23
+ const shoeSize = { ticketTypeIds: ['225685', '225687'] }
24
+
25
+ it('shows a scoped field to holders of a listed ticket type', () => {
26
+ expect(customFieldAppliesToTicketType(shoeSize, '225687')).toBe(true)
27
+ })
28
+
29
+ it('hides a scoped field from holders of other ticket types', () => {
30
+ expect(customFieldAppliesToTicketType(shoeSize, '225690')).toBe(false)
31
+ })
32
+
33
+ it('shows unscoped fields to every holder', () => {
34
+ expect(customFieldAppliesToTicketType({}, '225690')).toBe(true)
35
+ expect(customFieldAppliesToTicketType({ ticketTypeIds: null }, '225690')).toBe(true)
36
+ })
37
+
38
+ it('fails open when the holder ticket type is unknown', () => {
39
+ expect(customFieldAppliesToTicketType(shoeSize, undefined)).toBe(true)
40
+ })
41
+ })
@@ -1,3 +1,4 @@
1
+ import _forEach from 'lodash/forEach'
1
2
  import _isEmpty from 'lodash/isEmpty'
2
3
  import _map from 'lodash/map'
3
4
 
@@ -78,3 +79,41 @@ export const getAddOnDataWithCustomFields = (customFields: any) => {
78
79
 
79
80
  export const getFieldsKeys = (customFields: any) =>
80
81
  _map(customFields, field => field.name)
82
+
83
+ /**
84
+ * Ticket type of each holder slot, in holder order, expanded from the checkout
85
+ * breakdown (`tickets_price_breakdown`: one line per ticket type with a quantity).
86
+ * Returns [] when the breakdown doesn't line up with the holder count — callers
87
+ * then treat every holder's ticket type as unknown and show all fields.
88
+ */
89
+ interface ITicketsPriceBreakdownLine {
90
+ ticket_type_id?: string | number | null;
91
+ quantity?: string | number;
92
+ }
93
+
94
+ export const getHolderTicketTypeIds = (
95
+ ticketsPriceBreakdown: ITicketsPriceBreakdownLine[] | undefined,
96
+ holdersCount: number
97
+ ): string[] => {
98
+ if (!Array.isArray(ticketsPriceBreakdown)) return []
99
+ const slots: string[] = []
100
+ _forEach(ticketsPriceBreakdown, line => {
101
+ for (let i = 0; i < Number(line?.quantity || 0); i++) {
102
+ slots.push(String(line?.ticket_type_id ?? ''))
103
+ }
104
+ })
105
+ return slots.length === holdersCount ? slots : []
106
+ }
107
+
108
+ /**
109
+ * Whether a ticket-holder custom field applies to a ticket of `ticketTypeId`.
110
+ * Unscoped fields apply to every ticket; an unknown ticket type fails open so a
111
+ * required field is never silently dropped.
112
+ */
113
+ export const customFieldAppliesToTicketType = (
114
+ field: { ticketTypeIds?: string[] | null },
115
+ ticketTypeId?: string
116
+ ) =>
117
+ !ticketTypeId ||
118
+ _isEmpty(field.ticketTypeIds) ||
119
+ (field.ticketTypeIds || []).includes(String(ticketTypeId))
@@ -10,6 +10,11 @@ export { replaceVarInString } from './replaceVarInString'
10
10
  export { isBrowser } from './isBrowser'
11
11
  export { getFormInitialValues } from './form'
12
12
  export { setLoggedUserData, logoutUser } from './auth'
13
- export { getDataWithCustomFields, getFieldsKeys } from './customFields'
13
+ export {
14
+ customFieldAppliesToTicketType,
15
+ getDataWithCustomFields,
16
+ getFieldsKeys,
17
+ getHolderTicketTypeIds,
18
+ } from './customFields'
14
19
  export { createElementFromHTML } from './htmlNodeFromString'
15
20
  export { isJson } from './jsonUtils'