@tellescope/react-components 1.230.2 → 1.232.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.
Files changed (50) hide show
  1. package/lib/cjs/Forms/forms.js +2 -2
  2. package/lib/cjs/Forms/forms.js.map +1 -1
  3. package/lib/cjs/Forms/forms.v2.js +4 -4
  4. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  5. package/lib/cjs/Forms/hooks.d.ts +111 -3
  6. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  7. package/lib/cjs/Forms/hooks.js +30 -38
  8. package/lib/cjs/Forms/hooks.js.map +1 -1
  9. package/lib/cjs/Forms/inputs.d.ts +2 -2
  10. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  11. package/lib/cjs/Forms/inputs.js +71 -18
  12. package/lib/cjs/Forms/inputs.js.map +1 -1
  13. package/lib/cjs/Forms/inputs.v2.d.ts +2 -4
  14. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  15. package/lib/cjs/Forms/inputs.v2.js +13 -242
  16. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  17. package/lib/esm/CMS/components.d.ts +1 -0
  18. package/lib/esm/CMS/components.d.ts.map +1 -1
  19. package/lib/esm/Forms/form_responses.d.ts +1 -0
  20. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  21. package/lib/esm/Forms/forms.d.ts +3 -3
  22. package/lib/esm/Forms/forms.js +2 -2
  23. package/lib/esm/Forms/forms.js.map +1 -1
  24. package/lib/esm/Forms/forms.v2.d.ts +3 -3
  25. package/lib/esm/Forms/forms.v2.js +4 -4
  26. package/lib/esm/Forms/forms.v2.js.map +1 -1
  27. package/lib/esm/Forms/hooks.d.ts +112 -3
  28. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  29. package/lib/esm/Forms/hooks.js +31 -39
  30. package/lib/esm/Forms/hooks.js.map +1 -1
  31. package/lib/esm/Forms/inputs.d.ts +3 -3
  32. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  33. package/lib/esm/Forms/inputs.js +72 -19
  34. package/lib/esm/Forms/inputs.js.map +1 -1
  35. package/lib/esm/Forms/inputs.v2.d.ts +3 -5
  36. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  37. package/lib/esm/Forms/inputs.v2.js +15 -244
  38. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  39. package/lib/esm/controls.d.ts +2 -2
  40. package/lib/esm/inputs.d.ts +1 -1
  41. package/lib/esm/inputs.native.d.ts +1 -0
  42. package/lib/esm/inputs.native.d.ts.map +1 -1
  43. package/lib/esm/state.d.ts +315 -315
  44. package/lib/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +9 -9
  46. package/src/Forms/forms.tsx +2 -2
  47. package/src/Forms/forms.v2.tsx +12 -12
  48. package/src/Forms/hooks.tsx +49 -62
  49. package/src/Forms/inputs.tsx +73 -6
  50. package/src/Forms/inputs.v2.tsx +23 -404
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.230.2",
3
+ "version": "1.232.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -47,13 +47,13 @@
47
47
  "@reduxjs/toolkit": "^1.6.2",
48
48
  "@stripe/react-stripe-js": "^2.9.0",
49
49
  "@stripe/stripe-js": "^1.52.1",
50
- "@tellescope/constants": "1.230.2",
51
- "@tellescope/sdk": "1.230.2",
52
- "@tellescope/types-client": "1.230.2",
53
- "@tellescope/types-models": "1.230.2",
54
- "@tellescope/types-utilities": "1.230.2",
55
- "@tellescope/utilities": "1.230.2",
56
- "@tellescope/validation": "1.230.2",
50
+ "@tellescope/constants": "1.232.0",
51
+ "@tellescope/sdk": "1.232.0",
52
+ "@tellescope/types-client": "1.232.0",
53
+ "@tellescope/types-models": "1.232.0",
54
+ "@tellescope/types-utilities": "1.232.0",
55
+ "@tellescope/utilities": "1.232.0",
56
+ "@tellescope/validation": "1.232.0",
57
57
  "@typescript-eslint/eslint-plugin": "^4.33.0",
58
58
  "@typescript-eslint/parser": "^4.33.0",
59
59
  "css-to-react-native": "^3.0.0",
@@ -83,7 +83,7 @@
83
83
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
84
84
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
85
85
  },
86
- "gitHead": "0bc1e80c698398e1c62c4f026fa76475883f2396",
86
+ "gitHead": "b79c7d50da5ff767345e58331f483cc541abef20",
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  }
@@ -316,7 +316,7 @@ export const QuestionForField = ({
316
316
  <AppointmentBooking formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
317
317
  )
318
318
  : field.type === 'Stripe' ? (
319
- <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} />
319
+ <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} responses={responses} enduser={enduser} />
320
320
  )
321
321
  : field.type === 'Chargebee' ? (
322
322
  <Chargebee field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Chargebee'>} setCustomerId={setCustomerId} form={form} />
@@ -325,7 +325,7 @@ export const QuestionForField = ({
325
325
  <StringLong field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string' | 'stringLong'>} form={form} />
326
326
  )
327
327
  : field.type === 'Rich Text' ? (
328
- <RichText field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
328
+ <RichText key={field.id} field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
329
329
  )
330
330
  : field.type === 'email' ? (
331
331
  <Email field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'email'>} form={form} />
@@ -204,9 +204,9 @@ export const QuestionForField = ({
204
204
  ) {
205
205
  return null
206
206
  }
207
- return (
207
+ return (
208
208
  // margin leaves room for error message in Question Group
209
- <Flex column flex={1} style={{ marginBottom: spacing ?? 25 }} id={field.id}>
209
+ <Flex column flex={1} style={{ marginBottom: spacing ?? 25 }} id={field.id}>
210
210
  {field.type !== 'Redirect' && field.title &&
211
211
  <Typography component="h4" style={{
212
212
  marginTop: 15, // ensures PDF display doesn't push description into overlap with logo / title at top of form
@@ -223,13 +223,15 @@ export const QuestionForField = ({
223
223
  <div style={{ marginTop: 15 }}></div>
224
224
  }
225
225
 
226
- {feedback.length > 0 &&
226
+ <Description field={field} style={{ fontSize: 14, color: '#00000099', marginBottom: 11 }} />
227
+
228
+ {feedback.length > 0 &&
227
229
  <Flex column style={{ marginBottom: 11, marginTop: 3, }}>
228
230
  {feedback.map((f, i) => (
229
231
  <Typography key={i} color="error" style={{ fontSize: 20 }}>
230
232
  {f}
231
233
  </Typography>
232
- ))}
234
+ ))}
233
235
  </Flex>
234
236
  }
235
237
 
@@ -308,7 +310,7 @@ export const QuestionForField = ({
308
310
  <AppointmentBooking formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
309
311
  )
310
312
  : field.type === 'Stripe' ? (
311
- <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} />
313
+ <Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} responses={responses} enduser={enduser} />
312
314
  )
313
315
  : field.type === 'Chargebee' ? (
314
316
  <Chargebee field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Chargebee'>} setCustomerId={setCustomerId} form={form} />
@@ -317,7 +319,7 @@ export const QuestionForField = ({
317
319
  <StringLong field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string' | 'stringLong'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
318
320
  )
319
321
  : field.type === 'Rich Text' ? (
320
- <RichText field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
322
+ <RichText key={field.id} field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
321
323
  )
322
324
  : field.type === 'email' ? (
323
325
  <Email field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'email'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
@@ -435,17 +437,15 @@ export const QuestionForField = ({
435
437
  </Flex>
436
438
  )}
437
439
 
438
- <Description field={field} style={{ fontSize: 14, color: '#00000099', marginTop: 4 }} />
439
-
440
440
  {field.type !== 'Question Group' &&
441
- <Typography color="error" style={{ marginTop: 3, height: 10, fontSize: 14, marginBottom: -10 }}>
441
+ <Typography color="error" style={{ marginTop: 3, height: 10, fontSize: 14, marginBottom: -10 }}>
442
442
  {(validationMessage === 'A response is required' || validationMessage === 'A value must be checked' || validationMessage === 'A file is required' || 'Enter a valid phone number' || 'Insurer is required')
443
- ? value.touched
443
+ ? value.touched
444
444
  ? form_display_text_for_language(form, validationMessage)
445
- : null
445
+ : null
446
446
  : form_display_text_for_language(form, validationMessage)
447
447
  }
448
- </Typography>
448
+ </Typography>
449
449
  }
450
450
  </Flex>
451
451
  )
@@ -684,7 +684,27 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
684
684
  setResponses(initializeFields())
685
685
  }, [formId, initializeFields])
686
686
 
687
+ // Create templated versions of responses for UI display
688
+ // This applies template value replacements (e.g., {{enduser.BMI}}) to field titles/descriptions
689
+ // The templated responses are used ONLY for display and submission, not for navigation logic
690
+ const templatedResponses = useMemo(() => {
691
+ return responses.map(response => {
692
+ const originalField = fields.find(f => f.id === response.fieldId) || response.field
687
693
 
694
+ return {
695
+ ...response,
696
+ fieldTitle: replace_form_field_template_values(originalField.title || '', { enduser, responses }),
697
+ fieldDescription: replace_form_field_template_values(originalField.description || '', { enduser, responses }),
698
+ fieldHtmlDescription: replace_form_field_template_values(originalField.htmlDescription || '', { enduser, responses }),
699
+ field: {
700
+ ...response.field,
701
+ title: replace_form_field_template_values(originalField.title || '', { enduser, responses }),
702
+ description: replace_form_field_template_values(originalField.description || '', { enduser, responses }),
703
+ htmlDescription: replace_form_field_template_values(originalField.htmlDescription || '', { enduser, responses }),
704
+ }
705
+ }
706
+ })
707
+ }, [responses, fields, enduser])
688
708
 
689
709
  // placeholders for initial files, reset when fields prop changes, since questions are now different (e.g. different form selected)
690
710
  const fileInitRef = useRef('')
@@ -706,15 +726,28 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
706
726
  }, [formId, initializeFiles])
707
727
 
708
728
  const currentValue = (
709
- responses.find(f => f.fieldId === activeField.value.id)
729
+ templatedResponses.find(f => f.fieldId === activeField.value.id)
710
730
  )
711
731
  const currentFileValue = (
712
732
  selectedFiles.find(f => f.fieldId === activeField.value.id)
713
733
  )
714
734
 
735
+ // Create templated version of activeField for UI display
736
+ // This applies template replacements to the field's title/description
737
+ const templatedActiveField = useMemo(() => {
738
+ const templatedResponse = templatedResponses.find(r => r.fieldId === activeField.value.id)
739
+ if (templatedResponse) {
740
+ return {
741
+ ...activeField,
742
+ value: templatedResponse.field
743
+ }
744
+ }
745
+ return activeField
746
+ }, [activeField, templatedResponses])
747
+
715
748
  const logicOptions: NextFieldLogicOptions = {
716
749
  urlLogicValue,
717
- activeResponses: responses.filter(r => r.includeInSubmit),
750
+ activeResponses: templatedResponses.filter(r => r.includeInSubmit),
718
751
  dateOfBirth: enduser?.dateOfBirth,
719
752
  gender: enduser?.gender,
720
753
  state: enduser?.state,
@@ -1274,10 +1307,11 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1274
1307
 
1275
1308
  if (!accessCode && session.type === 'enduser') throw new Error('enduser session without accessCode')
1276
1309
  try {
1310
+ // Use templatedResponses for submission to ensure template values are resolved
1277
1311
  const responsesToSubmit = (
1278
- options?.includedFieldIds
1279
- ? options.includedFieldIds.map(id => responses.find(r => r.fieldId === id)!)
1280
- : responses.filter(r => r.includeInSubmit)
1312
+ options?.includedFieldIds
1313
+ ? options.includedFieldIds.map(id => templatedResponses.find(r => r.fieldId === id)!)
1314
+ : templatedResponses.filter(r => r.includeInSubmit)
1281
1315
  )
1282
1316
 
1283
1317
  // ensure Question Group responses are included
@@ -1286,7 +1320,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1286
1320
  if (r.answer.type !== 'Question Group') continue
1287
1321
 
1288
1322
  for (const f of r.answer.value ?? []) {
1289
- const match = responses.find(r => r.fieldId === f?.id)
1323
+ const match = templatedResponses.find(r => r.fieldId === f?.id)
1290
1324
  if (!match || responsesToSubmit.find(r => r.fieldId === match.fieldId)) continue
1291
1325
 
1292
1326
  // hidden in group by conditional logic
@@ -1403,7 +1437,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1403
1437
  } finally {
1404
1438
  setSubmittingStatus(undefined)
1405
1439
  }
1406
- }, [accessCode, automationStepId, enduserId, responses, selectedFiles, session, handleUpload, existingResponses, ga4measurementId, rootResponseId, parentResponseId, calendarEventId, goBackURL, logicOptions, handleFileUpload])
1440
+ }, [accessCode, automationStepId, enduserId, responses, templatedResponses, selectedFiles, session, handleUpload, existingResponses, ga4measurementId, rootResponseId, parentResponseId, calendarEventId, goBackURL, logicOptions, handleFileUpload])
1407
1441
 
1408
1442
  const isNextDisabled = useCallback(() => {
1409
1443
  if (uploadingFiles.length) { return true }
@@ -1419,30 +1453,6 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1419
1453
  return false
1420
1454
  }, [activeField, validateField, uploadingFiles])
1421
1455
 
1422
- // Helper function to apply templating to responses
1423
- // Templates field titles/descriptions with current enduser data and form response values
1424
- // Can be called whenever we need to update templates (e.g., on "next" button click)
1425
- const applyTemplatingToResponses = useCallback((
1426
- currentResponses: Response[]
1427
- ): Response[] => {
1428
- return currentResponses.map(response => {
1429
- const originalField = fields.find(f => f.id === response.fieldId) || response.field
1430
-
1431
- return {
1432
- ...response,
1433
- fieldTitle: replace_form_field_template_values(originalField.title || '', { enduser, responses: currentResponses }),
1434
- fieldDescription: replace_form_field_template_values(originalField.description || '', { enduser, responses: currentResponses }),
1435
- fieldHtmlDescription: replace_form_field_template_values(originalField.htmlDescription || '', { enduser, responses: currentResponses }),
1436
- field: {
1437
- ...response.field,
1438
- title: replace_form_field_template_values(originalField.title || '', { enduser, responses: currentResponses }),
1439
- description: replace_form_field_template_values(originalField.description || '', { enduser, responses: currentResponses }),
1440
- htmlDescription: replace_form_field_template_values(originalField.htmlDescription || '', { enduser, responses: currentResponses }),
1441
- }
1442
- }
1443
- })
1444
- }, [fields, enduser])
1445
-
1446
1456
  const autoAdvanceRef = useRef(false)
1447
1457
  // don't make option, to avoid user passing invalid data, like an onclick event
1448
1458
  const goToNextField = useCallback((answer: FormResponseValue['answer'] | undefined) => {
@@ -1450,24 +1460,10 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1450
1460
  if (isNextDisabled() && currentValue?.answer.type !== 'Hidden Value') return
1451
1461
 
1452
1462
  console.log('going to next field')
1453
-
1454
- // If an answer is provided (e.g., from Hidden Value), update responses first
1455
- const responsesWithAnswer = answer
1456
- ? responses.map(r =>
1457
- r.fieldId === currentValue.fieldId
1458
- ? { ...r, answer }
1459
- : r
1460
- )
1461
- : responses
1462
-
1463
- // Apply templating to all responses including the newly updated answer
1464
- const templatedResponses = applyTemplatingToResponses(responsesWithAnswer)
1465
- setResponses(templatedResponses)
1466
-
1467
1463
  if (currentValue.answer.type === 'Question Group') {
1468
1464
  const responsesToSave = (
1469
1465
  (currentValue.field.options?.subFields || [])
1470
- .map(({ id }) => templatedResponses.find(f => f.fieldId === id)!)
1466
+ .map(({ id }) => responses.find(f => f.fieldId === id)!)
1471
1467
  .filter(f => f && f?.answer.type !== 'file' && f?.answer.type !== 'files')
1472
1468
  )
1473
1469
  if (responsesToSave.length) {
@@ -1478,7 +1474,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1478
1474
  })
1479
1475
  .catch(console.error)
1480
1476
  }
1481
- }
1477
+ }
1482
1478
  else if (currentValue?.answer?.type !== 'file' && currentValue?.answer?.type !== 'files' && (formResponseId || accessCode)) {
1483
1479
  session.api.form_responses.save_field_response({
1484
1480
  accessCode,
@@ -1493,26 +1489,17 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1493
1489
 
1494
1490
  try { window.scrollTo({ top: 0 }) } catch(err) {} // scroll to top if needed
1495
1491
  setActiveField(activeField => {
1496
- let newField = getNextField(activeField, currentValue, templatedResponses, logicOptions)
1492
+ let newField = getNextField(activeField, currentValue, responses, logicOptions)
1497
1493
 
1498
1494
  // when autoadvancing, prevent adding duplicates by checking whether already on stack
1499
1495
  if (newField !== undefined && !prevFieldStackRef.current.find(v => v.value.id === activeField?.value.id)) {
1500
1496
  prevFieldStackRef.current.push(activeField)
1501
1497
  setCurrentPageIndex(i => i + 1)
1502
1498
  }
1503
-
1504
- const fieldToReturn = newField || activeField
1505
-
1506
- // Apply templating to the active field by pulling from the templated responses
1507
- // This ensures the UI displays templated titles/descriptions immediately
1508
- const templatedResponse = templatedResponses.find(r => r.fieldId === fieldToReturn.value.id)
1509
- if (templatedResponse) {
1510
- fieldToReturn.value = templatedResponse.field
1511
- }
1512
-
1513
- return fieldToReturn
1499
+
1500
+ return newField || activeField
1514
1501
  })
1515
- }, [prevFieldStackRef, currentValue, isNextDisabled, updateFormResponse, session, responses, logicOptions, accessCode, formResponseId, setActiveField, setCurrentPageIndex, applyTemplatingToResponses])
1502
+ }, [prevFieldStackRef, currentValue, isNextDisabled, updateFormResponse, session, responses, logicOptions, accessCode, formResponseId, setActiveField, setCurrentPageIndex])
1516
1503
 
1517
1504
  useEffect(() => {
1518
1505
  if (dontAutoadvance) return
@@ -1636,12 +1623,12 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1636
1623
  return {
1637
1624
  enduserId,
1638
1625
  formResponseId,
1639
- activeField,
1626
+ activeField: templatedActiveField, // Use templated activeField for UI display
1640
1627
  currentValue,
1641
1628
  currentFileValue,
1642
1629
  getResponsesWithQuestionGroupAnswers,
1643
1630
  fields,
1644
- responses,
1631
+ responses: templatedResponses, // Use templated responses - only display fields differ, answer values unchanged
1645
1632
  selectedFiles,
1646
1633
  onFieldChange,
1647
1634
  onAddFile,
@@ -4,7 +4,7 @@ import { Autocomplete, Box, Button, Checkbox, Chip, Collapse, Divider, FormContr
4
4
  import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
6
  import { CANVAS_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
7
- import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, format_stripe_subscription_interval, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, replace_enduser_template_values, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
7
+ import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, format_stripe_subscription_interval, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, object_is_empty, replace_enduser_template_values, responses_satisfy_conditions, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
8
  import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
10
10
  import Slider from '@mui/material/Slider';
@@ -1703,7 +1703,18 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1703
1703
  )
1704
1704
  }
1705
1705
 
1706
- export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }: FormInputProps<'Stripe'> & {
1706
+ // Helper to emit GTM purchase event for Stripe payments (single source of truth)
1707
+ const emitStripePurchaseEvent = (field: FormField, cost: number) => {
1708
+ emit_gtm_event({
1709
+ event: 'form_purchase',
1710
+ productIds: field.options?.productIds || [],
1711
+ fieldId: field.id,
1712
+ value: cost / 100, // Convert cents to dollars
1713
+ currency: 'USD',
1714
+ })
1715
+ }
1716
+
1717
+ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId, form, responses, enduser }: FormInputProps<'Stripe'> & {
1707
1718
  setCustomerId: React.Dispatch<React.SetStateAction<string | undefined>>,
1708
1719
  }) => {
1709
1720
  const session = useResolvedSession()
@@ -1718,6 +1729,38 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1718
1729
  const [availableProducts, setAvailableProducts] = useState<any[]>([])
1719
1730
  const [loadingProducts, setLoadingProducts] = useState(false)
1720
1731
 
1732
+ // Compute visible products based on conditional logic
1733
+ const visibleProducts = useMemo(() => {
1734
+ if (!showProductSelection || availableProducts.length === 0) {
1735
+ return availableProducts
1736
+ }
1737
+
1738
+ return availableProducts.filter(product => {
1739
+ // Find condition for this product
1740
+ const productCondition = field.options?.productConditions?.find(c => c.productId === product._id)
1741
+
1742
+ // If no condition defined, show by default
1743
+ if (!productCondition?.showCondition || object_is_empty(productCondition.showCondition)) {
1744
+ return true
1745
+ }
1746
+
1747
+ // Evaluate condition against current form responses
1748
+ return responses_satisfy_conditions(responses || [], productCondition.showCondition, {
1749
+ dateOfBirth: enduser?.dateOfBirth,
1750
+ gender: enduser?.gender,
1751
+ state: enduser?.state,
1752
+ form,
1753
+ activeResponses: responses,
1754
+ })
1755
+ })
1756
+ }, [availableProducts, field.options?.productConditions, responses, showProductSelection, enduser, form])
1757
+
1758
+ // Automatically deselect products that become hidden
1759
+ useEffect(() => {
1760
+ const visibleProductIds = visibleProducts.map(p => p._id)
1761
+ setSelectedProducts(prev => prev.filter(id => visibleProductIds.includes(id)))
1762
+ }, [visibleProducts])
1763
+
1721
1764
  const fetchRef = useRef(false)
1722
1765
  useEffect(() => {
1723
1766
  if (fetchRef.current) return
@@ -1784,6 +1827,16 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1784
1827
  : 0 // Will be calculated by existing Stripe flow when not in selection mode
1785
1828
  )
1786
1829
 
1830
+ // Emit GTM purchase event once when success screen is displayed
1831
+ const purchaseEmittedRef = useRef(false)
1832
+ useEffect(() => {
1833
+ // Only emit for actual purchases (chargeImmediately), not for saving card details
1834
+ if (value && field.options?.chargeImmediately && !purchaseEmittedRef.current) {
1835
+ emitStripePurchaseEvent(field, cost)
1836
+ purchaseEmittedRef.current = true
1837
+ }
1838
+ }, [value, field, cost])
1839
+
1787
1840
  // Handle product selection step
1788
1841
  if (showProductSelection) {
1789
1842
  if (error) {
@@ -1815,6 +1868,20 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1815
1868
  </Grid>
1816
1869
  )
1817
1870
  }
1871
+
1872
+ // Check if all products are filtered out by conditional logic
1873
+ if (visibleProducts.length === 0) {
1874
+ return (
1875
+ <Grid container direction="column" spacing={2} alignItems="center">
1876
+ <Grid item>
1877
+ <Typography color="textSecondary">
1878
+ No products are available based on your previous answers.
1879
+ </Typography>
1880
+ </Grid>
1881
+ </Grid>
1882
+ )
1883
+ }
1884
+
1818
1885
  const isSingleSelection = field.options?.radio === true
1819
1886
 
1820
1887
  const handleProductSelection = (productId: string) => {
@@ -1862,7 +1929,7 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1862
1929
  <Typography variant="h6">Select Product{isSingleSelection ? '' : 's'}</Typography>
1863
1930
  </Grid>
1864
1931
 
1865
- {availableProducts.map((product) => {
1932
+ {visibleProducts.map((product) => {
1866
1933
  // Use real-time Stripe pricing if available, fallback to Tellescope pricing
1867
1934
  const price = product.currentPrice || product.cost
1868
1935
  const priceAmount = price?.amount || 0
@@ -3126,7 +3193,7 @@ export const contact_is_valid = (e: Partial<Enduser>) => {
3126
3193
  }
3127
3194
  }
3128
3195
 
3129
- export const RelatedContactsInput = ({ field, value: _value, onChange, ...props }: FormInputProps<'Related Contacts'>) => {
3196
+ export const RelatedContactsInput = ({ field, value: _value, onChange, error: parentError, ...props }: FormInputProps<'Related Contacts'>) => {
3130
3197
  // safeguard against any rogue values like empty string
3131
3198
  const value = Array.isArray(_value) ? _value : []
3132
3199
 
@@ -3201,7 +3268,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3201
3268
  <Grid item xs={4}>
3202
3269
  <TextField label="Phone Number" size="small" fullWidth
3203
3270
  InputProps={defaultInputProps}
3204
- value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value } : v), field.id)}
3271
+ value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value.trim() } : v), field.id)}
3205
3272
  />
3206
3273
  </Grid>
3207
3274
  }
@@ -3255,7 +3322,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3255
3322
  }
3256
3323
 
3257
3324
  <Grid item sx={{ my: 0.75 }}>
3258
- <Button variant="outlined" onClick={() => setEditing(-1)} size="small">
3325
+ <Button variant="outlined" onClick={() => setEditing(-1)} size="small" disabled={!!errorMessage || !!parentError}>
3259
3326
  Save Contact
3260
3327
  </Button>
3261
3328
  </Grid>