@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
@@ -1,11 +1,11 @@
1
1
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import axios from "axios"
3
- import { Autocomplete, Box, Button, Checkbox, Chip, Collapse, Divider, FormControl, FormControlLabel, FormLabel, Grid, IconButton as MuiIconButton, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
3
+ import { Autocomplete, Box, Button, Checkbox, Chip, Divider, FormControl, Grid, InputLabel, MenuItem, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
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"
8
- import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
7
+ import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, replace_enduser_template_values, responses_satisfy_conditions, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
+ import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, 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';
11
11
  import LinearProgress from '@mui/material/LinearProgress';
@@ -21,9 +21,7 @@ import heic2any from "heic2any"
21
21
  import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
22
22
  import LanguageIcon from '@mui/icons-material/Language';
23
23
 
24
- import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
25
- import { loadStripe } from '@stripe/stripe-js';
26
- import { CheckCircleOutline, Delete, Edit, ExpandMore, UploadFile } from "@mui/icons-material"
24
+ import { CheckCircleOutline, Delete, Edit, UploadFile } from "@mui/icons-material"
27
25
  import { WYSIWYG } from "./wysiwyg"
28
26
 
29
27
  export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
@@ -1511,7 +1509,6 @@ export const FilesInput = ({ value, onChange, field, existingFileName, uploading
1511
1509
  export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: FormInputProps<'multiple_choice'>) => {
1512
1510
  const value = typeof _value === 'string' ? [_value] : _value // if loading existingResponses, allows them to be a string
1513
1511
  const { choices, radio, other, optionDetails } = field.options as MultipleChoiceOptions
1514
- const [expandedDescriptions, setExpandedDescriptions] = useState<Record<number, boolean>>({})
1515
1512
 
1516
1513
  // current other string
1517
1514
  const enteringOtherStringRef = React.useRef('') // if typing otherString as prefix of a checkbox value, don't auto-select
@@ -1524,13 +1521,6 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1524
1521
  return optionDetails?.find(detail => detail.option === choice)?.description
1525
1522
  }, [optionDetails])
1526
1523
 
1527
- const toggleDescription = useCallback((index: number) => {
1528
- setExpandedDescriptions(prev => ({
1529
- ...prev,
1530
- [index]: !prev[index]
1531
- }))
1532
- }, [])
1533
-
1534
1524
  return (
1535
1525
  <Grid container alignItems="center" rowGap={1.5}>
1536
1526
  {radio
@@ -1544,7 +1534,6 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1544
1534
  {(choices ?? []).map((c, i) => {
1545
1535
  const description = getDescriptionForChoice(c)
1546
1536
  const hasDescription = !!description
1547
- const isExpanded = expandedDescriptions[i]
1548
1537
  const isSelected = !!value?.includes(c) && c !== otherString
1549
1538
 
1550
1539
  return (
@@ -1558,7 +1547,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1558
1547
  borderColor: 'primary.main',
1559
1548
  borderRadius: 1,
1560
1549
  padding: '16px 16px',
1561
- marginBottom: '12px',
1550
+ marginBottom: hasDescription ? '8px' : '12px',
1562
1551
  cursor: 'pointer',
1563
1552
  backgroundColor: 'transparent',
1564
1553
  boxSizing: 'border-box',
@@ -1569,32 +1558,13 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1569
1558
  onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)}
1570
1559
  >
1571
1560
  <Typography component="span" sx={{ flex: 1, color: 'primary.main', fontSize: 13, fontWeight: 600 }}>{c}</Typography>
1572
- {hasDescription && (
1573
- <MuiIconButton
1574
- className="expand-button"
1575
- size="small"
1576
- onClick={(e: React.MouseEvent) => {
1577
- e.stopPropagation()
1578
- toggleDescription(i)
1579
- }}
1580
- sx={{
1581
- transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
1582
- transition: 'transform 0.2s',
1583
- ml: 1
1584
- }}
1585
- >
1586
- <ExpandMore fontSize="small" />
1587
- </MuiIconButton>
1588
- )}
1589
1561
  </Box>
1590
1562
  {hasDescription && (
1591
- <Collapse in={isExpanded}>
1592
- <Box sx={{ pl: 2, pr: 2, pb: 1, pt: 1 }}>
1593
- <Typography variant="body2" color="text.secondary">
1594
- {description}
1595
- </Typography>
1596
- </Box>
1597
- </Collapse>
1563
+ <Box sx={{ pl: 2, pr: 2, pb: 1, mb: 1 }}>
1564
+ <Typography style={{ fontSize: 14, color: '#00000099' }}>
1565
+ {description}
1566
+ </Typography>
1567
+ </Box>
1598
1568
  )}
1599
1569
  </Box>
1600
1570
  )
@@ -1605,7 +1575,6 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1605
1575
  (choices ?? []).map((c, i) => {
1606
1576
  const description = getDescriptionForChoice(c)
1607
1577
  const hasDescription = !!description
1608
- const isExpanded = expandedDescriptions[i]
1609
1578
 
1610
1579
  return (
1611
1580
  <Grid xs={12} key={i}>
@@ -1619,10 +1588,6 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1619
1588
  boxSizing: 'border-box'
1620
1589
  }}
1621
1590
  onClick={(e) => {
1622
- // Don't trigger selection if clicking on the expand button
1623
- if ((e.target as HTMLElement).closest('.expand-button')) {
1624
- return
1625
- }
1626
1591
  onChange(
1627
1592
  (
1628
1593
  value?.includes(c)
@@ -1647,32 +1612,13 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1647
1612
  inputProps={{ 'aria-label': 'primary checkbox' }}
1648
1613
  />
1649
1614
  <Typography component="span" sx={{ flex: 1 }}>{c}</Typography>
1650
- {hasDescription && (
1651
- <MuiIconButton
1652
- className="expand-button"
1653
- size="small"
1654
- onClick={(e: React.MouseEvent) => {
1655
- e.stopPropagation()
1656
- toggleDescription(i)
1657
- }}
1658
- sx={{
1659
- transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
1660
- transition: 'transform 0.2s',
1661
- ml: 1
1662
- }}
1663
- >
1664
- <ExpandMore fontSize="small" />
1665
- </MuiIconButton>
1666
- )}
1667
1615
  </Box>
1668
1616
  {hasDescription && (
1669
- <Collapse in={isExpanded}>
1670
- <Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
1671
- <Typography variant="body2" color="text.secondary">
1672
- {description}
1673
- </Typography>
1674
- </Box>
1675
- </Collapse>
1617
+ <Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
1618
+ <Typography style={{ fontSize: 14, color: '#00000099' }}>
1619
+ {description}
1620
+ </Typography>
1621
+ </Box>
1676
1622
  )}
1677
1623
  </Box>
1678
1624
  </Grid>
@@ -1718,339 +1664,12 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1718
1664
  )
1719
1665
  }
1720
1666
 
1721
- export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }: FormInputProps<'Stripe'> & {
1722
- setCustomerId: React.Dispatch<React.SetStateAction<string | undefined>>,
1723
- }) => {
1724
- const session = useResolvedSession()
1725
- const [clientSecret, setClientSecret] = useState('')
1726
- const [businessName, setBusinessName] = useState('')
1727
- const [isCheckout, setIsCheckout] = useState(false)
1728
- const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe>>()
1729
- const [answertext, setAnswertext] = useState('')
1730
- const [error, setError] = useState('')
1731
- const [selectedProducts, setSelectedProducts] = useState<string[]>([])
1732
- const [showProductSelection, setShowProductSelection] = useState(false)
1733
- const [availableProducts, setAvailableProducts] = useState<any[]>([])
1734
- const [loadingProducts, setLoadingProducts] = useState(false)
1735
-
1736
- const fetchRef = useRef(false)
1737
- useEffect(() => {
1738
- if (fetchRef.current) return
1739
- if (value && (session.userInfo as any)?.stripeCustomerId) {
1740
- return setCustomerId(c => c ? c : (session.userInfo as any)?.stripeCustomerId) // already paid or saved card
1741
- }
1742
-
1743
- // Check if product selection mode is enabled
1744
- if (field.options?.stripeProductSelectionMode && (field.options?.productIds || []).length > 1) {
1745
- setShowProductSelection(true)
1746
- setLoadingProducts(true)
1747
-
1748
- // Fetch product data with real-time Stripe pricing via proxy_read
1749
- const productIds = (field.options.productIds || []).join(',')
1750
- session.api.integrations.proxy_read({
1751
- integration: 'Stripe',
1752
- type: 'product-prices',
1753
- id: productIds,
1754
- query: field.options.stripeKey
1755
- })
1756
- .then(({ data }) => {
1757
- setAvailableProducts(data.products || [])
1758
- setLoadingProducts(false)
1759
- })
1760
- .catch((e: any) => {
1761
- console.error('Error loading product data:', e)
1762
- const errorMessage = e?.message?.includes?.('Stripe pricing error:')
1763
- ? e.message.replace('Stripe pricing error: ', '')
1764
- : 'Failed to load product information from Stripe'
1765
- setError(`Product configuration error: ${errorMessage}`)
1766
- setLoadingProducts(false)
1767
- })
1768
- return
1769
- }
1770
-
1771
- fetchRef.current = true
1772
-
1773
- session.api.form_responses.stripe_details({ fieldId: field.id, enduserId })
1774
- .then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
1775
- setAnswertext(answerText || '')
1776
- setIsCheckout(!!isCheckout)
1777
- setClientSecret(clientSecret)
1778
- setStripePromise(loadStripe(publishableKey, { stripeAccount }))
1779
- setBusinessName(businessName)
1780
- setCustomerId(customerId)
1781
- })
1782
- .catch((e: any) => {
1783
- console.error(e)
1784
- if (typeof e?.message === 'string') {
1785
- setError(e.message)
1786
- }
1787
- })
1788
- }, [session, value, field.id, enduserId])
1789
-
1790
- const cost = (
1791
- showProductSelection
1792
- ? selectedProducts.reduce((total, productId) => {
1793
- const product = availableProducts.find(p => p._id === productId)
1794
- if (product?.currentPrice) {
1795
- return total + (product.currentPrice.amount || 0)
1796
- }
1797
- return total + (product?.cost?.amount || 0)
1798
- }, 0)
1799
- : 0 // Will be calculated by existing Stripe flow when not in selection mode
1800
- )
1801
-
1802
- // Handle product selection step
1803
- if (showProductSelection) {
1804
- if (error) {
1805
- return (
1806
- <Grid container direction="column" spacing={2} alignItems="center">
1807
- <Grid item>
1808
- <Typography color="error" variant="h6">
1809
- Product Configuration Error
1810
- </Typography>
1811
- </Grid>
1812
- <Grid item>
1813
- <Typography color="error" sx={{ textAlign: 'center' }}>
1814
- {error}
1815
- </Typography>
1816
- </Grid>
1817
- </Grid>
1818
- )
1819
- }
1820
-
1821
- if (loadingProducts) {
1822
- return (
1823
- <Grid container direction="column" spacing={2} alignItems="center">
1824
- <Grid item>
1825
- <LinearProgress />
1826
- </Grid>
1827
- <Grid item>
1828
- <Typography>Loading product information...</Typography>
1829
- </Grid>
1830
- </Grid>
1831
- )
1832
- }
1833
- const isSingleSelection = field.options?.radio === true
1834
-
1835
- const handleProductSelection = (productId: string) => {
1836
- if (isSingleSelection) {
1837
- setSelectedProducts([productId])
1838
- } else {
1839
- setSelectedProducts(prev =>
1840
- prev.includes(productId)
1841
- ? prev.filter(id => id !== productId)
1842
- : [...prev, productId]
1843
- )
1844
- }
1845
- }
1846
-
1847
- const handleContinueToPayment = () => {
1848
- if (selectedProducts.length === 0) return
1849
- setShowProductSelection(false)
1850
- fetchRef.current = true
1851
-
1852
- // Now fetch Stripe details with selected products
1853
- session.api.form_responses.stripe_details({
1854
- fieldId: field.id,
1855
- enduserId,
1856
- ...(selectedProducts.length > 0 && { selectedProductIds: selectedProducts }) // Pass selected products to Stripe checkout
1857
- } as any)
1858
- .then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
1859
- setAnswertext(answerText || '')
1860
- setIsCheckout(!!isCheckout)
1861
- setClientSecret(clientSecret)
1862
- setStripePromise(loadStripe(publishableKey, { stripeAccount }))
1863
- setBusinessName(businessName)
1864
- setCustomerId(customerId)
1865
- })
1866
- .catch((e: any) => {
1867
- console.error(e)
1868
- if (typeof e?.message === 'string') {
1869
- setError(e.message)
1870
- }
1871
- })
1872
- }
1873
-
1874
- return (
1875
- <Grid container direction="column" spacing={2}>
1876
- <Grid item>
1877
- <Typography variant="h6">Select Product{isSingleSelection ? '' : 's'}</Typography>
1878
- </Grid>
1879
-
1880
- {availableProducts.map((product) => {
1881
- // Use real-time Stripe pricing if available, fallback to Tellescope pricing
1882
- const price = product.currentPrice || product.cost
1883
- const priceAmount = price?.amount || 0
1884
- const priceCurrency = price?.currency || 'USD'
1885
-
1886
- return (
1887
- <Grid item key={product._id}>
1888
- <FormControlLabel
1889
- control={
1890
- isSingleSelection ? (
1891
- <Radio
1892
- checked={selectedProducts.includes(product._id)}
1893
- onChange={() => handleProductSelection(product._id)}
1894
- />
1895
- ) : (
1896
- <Checkbox
1897
- checked={selectedProducts.includes(product._id)}
1898
- onChange={() => handleProductSelection(product._id)}
1899
- />
1900
- )
1901
- }
1902
- label={
1903
- <Box>
1904
- <Typography variant="body1" fontWeight="bold">
1905
- {product.title}
1906
- </Typography>
1907
- {product.description && (
1908
- <Typography variant="body2" color="textSecondary">
1909
- {product.description}
1910
- </Typography>
1911
- )}
1912
- <Typography variant="body2" color="primary">
1913
- ${(priceAmount / 100).toFixed(2)} {priceCurrency.toUpperCase()}
1914
- {product.currentPrice?.isSubscription && (
1915
- <Typography component="span" variant="caption" sx={{ ml: 0.5 }}>
1916
- {format_stripe_subscription_interval(product.currentPrice?.interval, product.currentPrice?.interval_count)}
1917
- </Typography>
1918
- )}
1919
- </Typography>
1920
- </Box>
1921
- }
1922
- />
1923
- </Grid>
1924
- )
1925
- })}
1926
-
1927
- <Grid item>
1928
- <Button
1929
- variant="contained"
1930
- onClick={handleContinueToPayment}
1931
- disabled={selectedProducts.length === 0}
1932
- sx={{ mt: 2 }}
1933
- >
1934
- Continue to Payment
1935
- </Button>
1936
- </Grid>
1937
- </Grid>
1938
- )
1939
- }
1940
-
1941
- if (error) {
1942
- return (
1943
- <Typography color="error">
1944
- {error}
1945
- </Typography>
1946
- )
1947
- }
1948
- if (value) {
1949
- return (
1950
- <Grid container alignItems="center" wrap="nowrap">
1951
- <CheckCircleOutline color="success" />
1952
-
1953
- <Typography sx={{ ml: 1, fontSize: 20 }}>
1954
- {field.options?.chargeImmediately ? 'Your purchase was successful' : "Your payment details have been saved!"}
1955
- </Typography>
1956
- </Grid>
1957
- )
1958
- }
1959
- if (!(clientSecret && stripePromise)) return <LinearProgress />
1960
- if (isCheckout && stripePromise) return (
1961
- <EmbeddedCheckoutProvider stripe={stripePromise}
1962
- options={{
1963
- clientSecret,
1964
- onComplete: () => onChange(answertext || 'Completed checkout', field.id),
1965
- }}
1966
- >
1967
- <EmbeddedCheckout />
1968
- </EmbeddedCheckoutProvider>
1969
- )
1970
- return (
1971
- <Elements stripe={stripePromise} options={{
1972
- clientSecret,
1973
- }}>
1974
- <StripeForm businessName={businessName} onSuccess={() => onChange(answertext || 'Saved card details', field.id)}
1975
- cost={cost}
1976
- field={field}
1977
- />
1978
- </Elements>
1979
- )
1980
- }
1981
-
1982
- const StripeForm = ({ businessName, onSuccess, field, cost } : { businessName: string, onSuccess: () => void, field: FormField, cost: number }) => {
1983
- const stripe = useStripe();
1984
- const elements = useElements()
1985
-
1986
- const [ready, setReady] = useState(false)
1987
- const [errorMessage, setErrorMessage] = useState('');
1988
1667
 
1989
- const handleSubmit = async (event: any) => {
1990
- // We don't want to let default form submission happen here,
1991
- // which would refresh the page.
1992
- event?.preventDefault();
1993
-
1994
- if (!stripe || !elements) {
1995
- // Stripe.js hasn't yet loaded.
1996
- // Make sure to disable form submission until Stripe.js has loaded.
1997
- return null;
1998
- }
1999
-
2000
- const {error} = await (field.options?.chargeImmediately ? stripe.confirmPayment : stripe.confirmSetup)({
2001
- //`Elements` instance that was used to create the Payment Element
2002
- elements,
2003
- confirmParams: {
2004
- return_url: window.location.href,
2005
- },
2006
- redirect: 'if_required', // ensures the redirect url won't be used, unless the Bank redirect payment type is enabled (it's not, just card)
2007
- });
2008
-
2009
- if (error) {
2010
- // This point will only be reached if there is an immediate error when
2011
- // confirming the payment. Show error to your customer (for example, payment
2012
- // details incomplete)
2013
- setErrorMessage(error?.message ?? '');
2014
- } else {
2015
- onSuccess()
2016
- // Your customer will be redirected to your `return_url`. For some payment
2017
- // methods like iDEAL, your customer will be redirected to an intermediate
2018
- // site first to authorize the payment, then redirected to the `return_url`.
2019
- }
2020
- };
2021
-
2022
- return (
2023
- <form onSubmit={handleSubmit}>
2024
- <PaymentElement onReady={() => setReady(true)}
2025
- options={{
2026
- business: { name: businessName },
2027
- }}
2028
- />
2029
- <Button variant="contained" color="primary" type="submit" sx={{ mt: 1 }}
2030
- disabled={!(stripe && ready)}
2031
- >
2032
- {field.options?.chargeImmediately ? 'Make Payment' : 'Save Payment Details'}
2033
- </Button>
2034
-
2035
- {cost > 0 &&
2036
- <Typography sx={{ mt: 0.5 }}>
2037
- {
2038
- field.options?.customPriceMessage
2039
- ? field.options.customPriceMessage.replaceAll('{{PRICE}}', `$${(cost / 100).toFixed(2)}`)
2040
- : `You will be charged $${(cost / 100).toFixed(2)} ${field.options?.chargeImmediately ? '' : 'on form submission'}`
2041
- }
2042
- </Typography>
2043
- }
2044
-
2045
- {/* Show error message to your customers */}
2046
- {errorMessage &&
2047
- <Typography color="error" sx={{ mt: 0.5 }}>
2048
- {errorMessage}
2049
- </Typography>
2050
- }
2051
- </form>
2052
- )
2053
- }
1668
+ // StripeInput is shared between v1 and v2 forms
1669
+ // Both versions use the same implementation from inputs.tsx to ensure consistent behavior
1670
+ // and avoid code duplication. Re-exporting here maintains the pattern where forms.v2.tsx
1671
+ // only imports from inputs.v2.tsx
1672
+ export { StripeInput } from './inputs'
2054
1673
 
2055
1674
  export const Progress = ({ numerator, denominator, style, color } : { numerator: number, denominator: number, color?: string } & Styled) => (
2056
1675
  <Box sx={{ display: 'flex', alignItems: 'center', ...style }}>
@@ -3159,7 +2778,7 @@ export const contact_is_valid = (e: Partial<Enduser>) => {
3159
2778
  }
3160
2779
  }
3161
2780
 
3162
- export const RelatedContactsInput = ({ field, value: _value, onChange, ...props }: FormInputProps<'Related Contacts'>) => {
2781
+ export const RelatedContactsInput = ({ field, value: _value, onChange, error: parentError, ...props }: FormInputProps<'Related Contacts'>) => {
3163
2782
  // safeguard against any rogue values like empty string
3164
2783
  const value = Array.isArray(_value) ? _value : []
3165
2784
 
@@ -3234,7 +2853,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3234
2853
  <Grid item xs={4}>
3235
2854
  <TextField label="Phone Number" size="small" fullWidth
3236
2855
  InputProps={defaultInputProps}
3237
- value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value } : v), field.id)}
2856
+ value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value.trim() } : v), field.id)}
3238
2857
  />
3239
2858
  </Grid>
3240
2859
  }
@@ -3288,7 +2907,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3288
2907
  }
3289
2908
 
3290
2909
  <Grid item sx={{ my: 0.75 }}>
3291
- <Button variant="outlined" onClick={() => setEditing(-1)} size="small">
2910
+ <Button variant="outlined" onClick={() => setEditing(-1)} size="small" disabled={!!errorMessage || !!parentError}>
3292
2911
  Save Contact
3293
2912
  </Button>
3294
2913
  </Grid>