@tellescope/react-components 1.226.0 → 1.228.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.
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +34 -22
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/inputs.d.ts +1 -1
- package/lib/cjs/Forms/inputs.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.js +189 -28
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +34 -22
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +1 -1
- package/lib/esm/Forms/inputs.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.js +192 -31
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/hooks.tsx +13 -0
- package/src/Forms/inputs.tsx +356 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.228.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.
|
|
51
|
-
"@tellescope/sdk": "^1.
|
|
52
|
-
"@tellescope/types-client": "^1.
|
|
53
|
-
"@tellescope/types-models": "^1.
|
|
54
|
-
"@tellescope/types-utilities": "^1.
|
|
55
|
-
"@tellescope/utilities": "^1.
|
|
56
|
-
"@tellescope/validation": "^1.
|
|
50
|
+
"@tellescope/constants": "^1.228.0",
|
|
51
|
+
"@tellescope/sdk": "^1.228.0",
|
|
52
|
+
"@tellescope/types-client": "^1.228.0",
|
|
53
|
+
"@tellescope/types-models": "^1.228.0",
|
|
54
|
+
"@tellescope/types-utilities": "^1.228.0",
|
|
55
|
+
"@tellescope/utilities": "^1.228.0",
|
|
56
|
+
"@tellescope/validation": "^1.228.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": "
|
|
86
|
+
"gitHead": "1bcfbd5c10a10553ce350df51799ef2bb1582f3d",
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
|
89
89
|
}
|
package/src/Forms/hooks.tsx
CHANGED
|
@@ -1003,6 +1003,19 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1003
1003
|
if (typeof value.answer.value?.inches !== 'number' || isNaN(value.answer.value?.inches)) {
|
|
1004
1004
|
return "Inches must be provided (enter 0 for no inches)"
|
|
1005
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
// Convert height to total inches for min/max validation
|
|
1008
|
+
const totalInches = ((value.answer.value?.feet || 0) * 12) + (value.answer.value?.inches || 0)
|
|
1009
|
+
if (field.options?.min !== undefined && field.options.min !== -Infinity && totalInches < field.options.min) {
|
|
1010
|
+
const minFeet = Math.floor(field.options.min / 12)
|
|
1011
|
+
const minInches = field.options.min % 12
|
|
1012
|
+
return `Height must be at least ${minFeet}' ${minInches}"`
|
|
1013
|
+
}
|
|
1014
|
+
if (field.options?.max !== undefined && field.options.max !== Infinity && totalInches > field.options.max) {
|
|
1015
|
+
const maxFeet = Math.floor(field.options.max / 12)
|
|
1016
|
+
const maxInches = field.options.max % 12
|
|
1017
|
+
return `Height must be no more than ${maxFeet}' ${maxInches}"`
|
|
1018
|
+
}
|
|
1006
1019
|
}
|
|
1007
1020
|
|
|
1008
1021
|
if (value.answer.type === 'Related Contacts') {
|
package/src/Forms/inputs.tsx
CHANGED
|
@@ -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, Divider, FormControl, FormControlLabel, FormLabel, Grid, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
|
|
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"
|
|
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
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, 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"
|
|
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';
|
|
11
11
|
import LinearProgress from '@mui/material/LinearProgress';
|
|
@@ -23,7 +23,7 @@ import LanguageIcon from '@mui/icons-material/Language';
|
|
|
23
23
|
|
|
24
24
|
import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
|
|
25
25
|
import { loadStripe } from '@stripe/stripe-js';
|
|
26
|
-
import { CheckCircleOutline, Delete, Edit } from "@mui/icons-material"
|
|
26
|
+
import { CheckCircleOutline, Delete, Edit, ExpandMore } from "@mui/icons-material"
|
|
27
27
|
import { WYSIWYG } from "./wysiwyg"
|
|
28
28
|
|
|
29
29
|
export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
|
|
@@ -1504,12 +1504,24 @@ const multipleChoiceItemSx: SxProps = {
|
|
|
1504
1504
|
|
|
1505
1505
|
export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: FormInputProps<'multiple_choice'>) => {
|
|
1506
1506
|
const value = typeof _value === 'string' ? [_value] : _value // if loading existingResponses, allows them to be a string
|
|
1507
|
-
const { choices, radio, other } = field.options as MultipleChoiceOptions
|
|
1507
|
+
const { choices, radio, other, optionDetails } = field.options as MultipleChoiceOptions
|
|
1508
|
+
const [expandedDescriptions, setExpandedDescriptions] = useState<Record<number, boolean>>({})
|
|
1508
1509
|
|
|
1509
1510
|
// current other string
|
|
1510
1511
|
const enteringOtherStringRef = React.useRef('') // if typing otherString as prefix of a checkbox value, don't auto-select
|
|
1511
1512
|
const otherString = value?.find(v => v === enteringOtherStringRef.current || !(choices ?? [])?.find(c => c === v)) ?? ''
|
|
1512
1513
|
|
|
1514
|
+
const getDescriptionForChoice = useCallback((choice: string) => {
|
|
1515
|
+
return optionDetails?.find(detail => detail.option === choice)?.description
|
|
1516
|
+
}, [optionDetails])
|
|
1517
|
+
|
|
1518
|
+
const toggleDescription = useCallback((index: number) => {
|
|
1519
|
+
setExpandedDescriptions(prev => ({
|
|
1520
|
+
...prev,
|
|
1521
|
+
[index]: !prev[index]
|
|
1522
|
+
}))
|
|
1523
|
+
}, [])
|
|
1524
|
+
|
|
1513
1525
|
return (
|
|
1514
1526
|
<Grid container alignItems="center">
|
|
1515
1527
|
{radio
|
|
@@ -1523,48 +1535,134 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
|
|
|
1523
1535
|
defaultValue="female"
|
|
1524
1536
|
name={`radio-group-${field.id}`}
|
|
1525
1537
|
>
|
|
1526
|
-
{(choices ?? []).map((c, i) =>
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1538
|
+
{(choices ?? []).map((c, i) => {
|
|
1539
|
+
const description = getDescriptionForChoice(c)
|
|
1540
|
+
const hasDescription = !!description
|
|
1541
|
+
const isExpanded = expandedDescriptions[i]
|
|
1542
|
+
|
|
1543
|
+
return (
|
|
1544
|
+
<Box key={i} sx={{ width: '100%' }}>
|
|
1545
|
+
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
|
1546
|
+
<FormControlLabel
|
|
1547
|
+
sx={{ ...multipleChoiceItemSx, flex: 1, marginLeft: '0px' }}
|
|
1548
|
+
checked={!!value?.includes(c) && c !== otherString}
|
|
1549
|
+
control={
|
|
1550
|
+
<Radio onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)} />
|
|
1551
|
+
}
|
|
1552
|
+
label={
|
|
1553
|
+
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
|
1554
|
+
<Typography component="span" sx={{ flex: 1 }}>{c}</Typography>
|
|
1555
|
+
{hasDescription && (
|
|
1556
|
+
<MuiIconButton
|
|
1557
|
+
size="small"
|
|
1558
|
+
onClick={(e: React.MouseEvent) => {
|
|
1559
|
+
e.stopPropagation()
|
|
1560
|
+
toggleDescription(i)
|
|
1561
|
+
}}
|
|
1562
|
+
sx={{
|
|
1563
|
+
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
1564
|
+
transition: 'transform 0.2s',
|
|
1565
|
+
ml: 1
|
|
1566
|
+
}}
|
|
1567
|
+
>
|
|
1568
|
+
<ExpandMore fontSize="small" />
|
|
1569
|
+
</MuiIconButton>
|
|
1570
|
+
)}
|
|
1571
|
+
</Box>
|
|
1572
|
+
}
|
|
1573
|
+
/>
|
|
1574
|
+
</Box>
|
|
1575
|
+
{hasDescription && (
|
|
1576
|
+
<Collapse in={isExpanded}>
|
|
1577
|
+
<Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
|
|
1578
|
+
<Typography variant="body2" color="text.secondary">
|
|
1579
|
+
{description}
|
|
1580
|
+
</Typography>
|
|
1581
|
+
</Box>
|
|
1582
|
+
</Collapse>
|
|
1583
|
+
)}
|
|
1584
|
+
</Box>
|
|
1585
|
+
)
|
|
1586
|
+
})}
|
|
1536
1587
|
</RadioGroup>
|
|
1537
1588
|
</FormControl>
|
|
1538
1589
|
) : (
|
|
1539
|
-
(choices ?? []).map((c, i) =>
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1590
|
+
(choices ?? []).map((c, i) => {
|
|
1591
|
+
const description = getDescriptionForChoice(c)
|
|
1592
|
+
const hasDescription = !!description
|
|
1593
|
+
const isExpanded = expandedDescriptions[i]
|
|
1594
|
+
|
|
1595
|
+
return (
|
|
1596
|
+
<Grid xs={12} key={i}>
|
|
1597
|
+
<Box sx={{ width: '100%' }}>
|
|
1598
|
+
<Box
|
|
1599
|
+
sx={{
|
|
1600
|
+
...multipleChoiceItemSx,
|
|
1601
|
+
display: 'flex',
|
|
1602
|
+
alignItems: 'center',
|
|
1603
|
+
cursor: 'pointer',
|
|
1604
|
+
width: '100%'
|
|
1605
|
+
}}
|
|
1606
|
+
onClick={(e) => {
|
|
1607
|
+
// Don't trigger selection if clicking on the expand button
|
|
1608
|
+
if ((e.target as HTMLElement).closest('.expand-button')) {
|
|
1609
|
+
return
|
|
1610
|
+
}
|
|
1611
|
+
onChange(
|
|
1612
|
+
(
|
|
1613
|
+
value?.includes(c)
|
|
1614
|
+
? (
|
|
1615
|
+
(radio || field.options?.radioChoices?.includes(c))
|
|
1616
|
+
? []
|
|
1617
|
+
: value.filter(v => v !== c)
|
|
1618
|
+
)
|
|
1619
|
+
: (
|
|
1620
|
+
(radio || field.options?.radioChoices?.includes(c))
|
|
1621
|
+
? [c]
|
|
1622
|
+
: [...(value ?? []).filter(x => !field.options?.radioChoices?.includes(x)), c]
|
|
1623
|
+
)
|
|
1624
|
+
),
|
|
1625
|
+
field.id,
|
|
1626
|
+
)
|
|
1627
|
+
}}
|
|
1628
|
+
>
|
|
1629
|
+
<Checkbox
|
|
1630
|
+
color="primary"
|
|
1631
|
+
checked={!!value?.includes(c) && c !== otherString}
|
|
1632
|
+
inputProps={{ 'aria-label': 'primary checkbox' }}
|
|
1633
|
+
/>
|
|
1634
|
+
<Typography component="span" sx={{ flex: 1 }}>{c}</Typography>
|
|
1635
|
+
{hasDescription && (
|
|
1636
|
+
<MuiIconButton
|
|
1637
|
+
className="expand-button"
|
|
1638
|
+
size="small"
|
|
1639
|
+
onClick={(e: React.MouseEvent) => {
|
|
1640
|
+
e.stopPropagation()
|
|
1641
|
+
toggleDescription(i)
|
|
1642
|
+
}}
|
|
1643
|
+
sx={{
|
|
1644
|
+
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
1645
|
+
transition: 'transform 0.2s',
|
|
1646
|
+
ml: 1
|
|
1647
|
+
}}
|
|
1648
|
+
>
|
|
1649
|
+
<ExpandMore fontSize="small" />
|
|
1650
|
+
</MuiIconButton>
|
|
1651
|
+
)}
|
|
1652
|
+
</Box>
|
|
1653
|
+
{hasDescription && (
|
|
1654
|
+
<Collapse in={isExpanded}>
|
|
1655
|
+
<Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
|
|
1656
|
+
<Typography variant="body2" color="text.secondary">
|
|
1657
|
+
{description}
|
|
1658
|
+
</Typography>
|
|
1659
|
+
</Box>
|
|
1660
|
+
</Collapse>
|
|
1661
|
+
)}
|
|
1662
|
+
</Box>
|
|
1663
|
+
</Grid>
|
|
1664
|
+
)
|
|
1665
|
+
})
|
|
1568
1666
|
)
|
|
1569
1667
|
}
|
|
1570
1668
|
{other &&
|
|
@@ -1613,9 +1711,12 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
|
|
|
1613
1711
|
const [businessName, setBusinessName] = useState('')
|
|
1614
1712
|
const [isCheckout, setIsCheckout] = useState(false)
|
|
1615
1713
|
const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe>>()
|
|
1616
|
-
const [, { findById: findProduct }] = useProducts({ dontFetch: true })
|
|
1617
1714
|
const [answertext, setAnswertext] = useState('')
|
|
1618
1715
|
const [error, setError] = useState('')
|
|
1716
|
+
const [selectedProducts, setSelectedProducts] = useState<string[]>([])
|
|
1717
|
+
const [showProductSelection, setShowProductSelection] = useState(false)
|
|
1718
|
+
const [availableProducts, setAvailableProducts] = useState<any[]>([])
|
|
1719
|
+
const [loadingProducts, setLoadingProducts] = useState(false)
|
|
1619
1720
|
|
|
1620
1721
|
const fetchRef = useRef(false)
|
|
1621
1722
|
useEffect(() => {
|
|
@@ -1623,6 +1724,35 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
|
|
|
1623
1724
|
if (value && (session.userInfo as any)?.stripeCustomerId) {
|
|
1624
1725
|
return setCustomerId(c => c ? c : (session.userInfo as any)?.stripeCustomerId) // already paid or saved card
|
|
1625
1726
|
}
|
|
1727
|
+
|
|
1728
|
+
// Check if product selection mode is enabled
|
|
1729
|
+
if (field.options?.stripeProductSelectionMode && (field.options?.productIds || []).length > 1) {
|
|
1730
|
+
setShowProductSelection(true)
|
|
1731
|
+
setLoadingProducts(true)
|
|
1732
|
+
|
|
1733
|
+
// Fetch product data with real-time Stripe pricing via proxy_read
|
|
1734
|
+
const productIds = (field.options.productIds || []).join(',')
|
|
1735
|
+
session.api.integrations.proxy_read({
|
|
1736
|
+
integration: 'Stripe',
|
|
1737
|
+
type: 'product-prices',
|
|
1738
|
+
id: productIds,
|
|
1739
|
+
query: field.options.stripeKey
|
|
1740
|
+
})
|
|
1741
|
+
.then(({ data }) => {
|
|
1742
|
+
setAvailableProducts(data.products || [])
|
|
1743
|
+
setLoadingProducts(false)
|
|
1744
|
+
})
|
|
1745
|
+
.catch((e: any) => {
|
|
1746
|
+
console.error('Error loading product data:', e)
|
|
1747
|
+
const errorMessage = e?.message?.includes?.('Stripe pricing error:')
|
|
1748
|
+
? e.message.replace('Stripe pricing error: ', '')
|
|
1749
|
+
: 'Failed to load product information from Stripe'
|
|
1750
|
+
setError(`Product configuration error: ${errorMessage}`)
|
|
1751
|
+
setLoadingProducts(false)
|
|
1752
|
+
})
|
|
1753
|
+
return
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1626
1756
|
fetchRef.current = true
|
|
1627
1757
|
|
|
1628
1758
|
session.api.form_responses.stripe_details({ fieldId: field.id, enduserId })
|
|
@@ -1643,10 +1773,156 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
|
|
|
1643
1773
|
}, [session, value, field.id, enduserId])
|
|
1644
1774
|
|
|
1645
1775
|
const cost = (
|
|
1646
|
-
|
|
1647
|
-
|
|
1776
|
+
showProductSelection
|
|
1777
|
+
? selectedProducts.reduce((total, productId) => {
|
|
1778
|
+
const product = availableProducts.find(p => p._id === productId)
|
|
1779
|
+
if (product?.currentPrice) {
|
|
1780
|
+
return total + (product.currentPrice.amount || 0)
|
|
1781
|
+
}
|
|
1782
|
+
return total + (product?.cost?.amount || 0)
|
|
1783
|
+
}, 0)
|
|
1784
|
+
: 0 // Will be calculated by existing Stripe flow when not in selection mode
|
|
1648
1785
|
)
|
|
1649
1786
|
|
|
1787
|
+
// Handle product selection step
|
|
1788
|
+
if (showProductSelection) {
|
|
1789
|
+
if (error) {
|
|
1790
|
+
return (
|
|
1791
|
+
<Grid container direction="column" spacing={2} alignItems="center">
|
|
1792
|
+
<Grid item>
|
|
1793
|
+
<Typography color="error" variant="h6">
|
|
1794
|
+
Product Configuration Error
|
|
1795
|
+
</Typography>
|
|
1796
|
+
</Grid>
|
|
1797
|
+
<Grid item>
|
|
1798
|
+
<Typography color="error" sx={{ textAlign: 'center' }}>
|
|
1799
|
+
{error}
|
|
1800
|
+
</Typography>
|
|
1801
|
+
</Grid>
|
|
1802
|
+
</Grid>
|
|
1803
|
+
)
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
if (loadingProducts) {
|
|
1807
|
+
return (
|
|
1808
|
+
<Grid container direction="column" spacing={2} alignItems="center">
|
|
1809
|
+
<Grid item>
|
|
1810
|
+
<LinearProgress />
|
|
1811
|
+
</Grid>
|
|
1812
|
+
<Grid item>
|
|
1813
|
+
<Typography>Loading product information...</Typography>
|
|
1814
|
+
</Grid>
|
|
1815
|
+
</Grid>
|
|
1816
|
+
)
|
|
1817
|
+
}
|
|
1818
|
+
const isSingleSelection = field.options?.radio === true
|
|
1819
|
+
|
|
1820
|
+
const handleProductSelection = (productId: string) => {
|
|
1821
|
+
if (isSingleSelection) {
|
|
1822
|
+
setSelectedProducts([productId])
|
|
1823
|
+
} else {
|
|
1824
|
+
setSelectedProducts(prev =>
|
|
1825
|
+
prev.includes(productId)
|
|
1826
|
+
? prev.filter(id => id !== productId)
|
|
1827
|
+
: [...prev, productId]
|
|
1828
|
+
)
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const handleContinueToPayment = () => {
|
|
1833
|
+
if (selectedProducts.length === 0) return
|
|
1834
|
+
setShowProductSelection(false)
|
|
1835
|
+
fetchRef.current = true
|
|
1836
|
+
|
|
1837
|
+
// Now fetch Stripe details with selected products
|
|
1838
|
+
session.api.form_responses.stripe_details({
|
|
1839
|
+
fieldId: field.id,
|
|
1840
|
+
enduserId,
|
|
1841
|
+
...(selectedProducts.length > 0 && { selectedProductIds: selectedProducts }) // Pass selected products to Stripe checkout
|
|
1842
|
+
} as any)
|
|
1843
|
+
.then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
|
|
1844
|
+
setAnswertext(answerText || '')
|
|
1845
|
+
setIsCheckout(!!isCheckout)
|
|
1846
|
+
setClientSecret(clientSecret)
|
|
1847
|
+
setStripePromise(loadStripe(publishableKey, { stripeAccount }))
|
|
1848
|
+
setBusinessName(businessName)
|
|
1849
|
+
setCustomerId(customerId)
|
|
1850
|
+
})
|
|
1851
|
+
.catch((e: any) => {
|
|
1852
|
+
console.error(e)
|
|
1853
|
+
if (typeof e?.message === 'string') {
|
|
1854
|
+
setError(e.message)
|
|
1855
|
+
}
|
|
1856
|
+
})
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
return (
|
|
1860
|
+
<Grid container direction="column" spacing={2}>
|
|
1861
|
+
<Grid item>
|
|
1862
|
+
<Typography variant="h6">Select Product{isSingleSelection ? '' : 's'}</Typography>
|
|
1863
|
+
</Grid>
|
|
1864
|
+
|
|
1865
|
+
{availableProducts.map((product) => {
|
|
1866
|
+
// Use real-time Stripe pricing if available, fallback to Tellescope pricing
|
|
1867
|
+
const price = product.currentPrice || product.cost
|
|
1868
|
+
const priceAmount = price?.amount || 0
|
|
1869
|
+
const priceCurrency = price?.currency || 'USD'
|
|
1870
|
+
|
|
1871
|
+
return (
|
|
1872
|
+
<Grid item key={product._id}>
|
|
1873
|
+
<FormControlLabel
|
|
1874
|
+
control={
|
|
1875
|
+
isSingleSelection ? (
|
|
1876
|
+
<Radio
|
|
1877
|
+
checked={selectedProducts.includes(product._id)}
|
|
1878
|
+
onChange={() => handleProductSelection(product._id)}
|
|
1879
|
+
/>
|
|
1880
|
+
) : (
|
|
1881
|
+
<Checkbox
|
|
1882
|
+
checked={selectedProducts.includes(product._id)}
|
|
1883
|
+
onChange={() => handleProductSelection(product._id)}
|
|
1884
|
+
/>
|
|
1885
|
+
)
|
|
1886
|
+
}
|
|
1887
|
+
label={
|
|
1888
|
+
<Box>
|
|
1889
|
+
<Typography variant="body1" fontWeight="bold">
|
|
1890
|
+
{product.title}
|
|
1891
|
+
</Typography>
|
|
1892
|
+
{product.description && (
|
|
1893
|
+
<Typography variant="body2" color="textSecondary">
|
|
1894
|
+
{product.description}
|
|
1895
|
+
</Typography>
|
|
1896
|
+
)}
|
|
1897
|
+
<Typography variant="body2" color="primary">
|
|
1898
|
+
${(priceAmount / 100).toFixed(2)} {priceCurrency.toUpperCase()}
|
|
1899
|
+
{product.currentPrice?.isSubscription && (
|
|
1900
|
+
<Typography component="span" variant="caption" sx={{ ml: 0.5 }}>
|
|
1901
|
+
/month
|
|
1902
|
+
</Typography>
|
|
1903
|
+
)}
|
|
1904
|
+
</Typography>
|
|
1905
|
+
</Box>
|
|
1906
|
+
}
|
|
1907
|
+
/>
|
|
1908
|
+
</Grid>
|
|
1909
|
+
)
|
|
1910
|
+
})}
|
|
1911
|
+
|
|
1912
|
+
<Grid item>
|
|
1913
|
+
<Button
|
|
1914
|
+
variant="contained"
|
|
1915
|
+
onClick={handleContinueToPayment}
|
|
1916
|
+
disabled={selectedProducts.length === 0}
|
|
1917
|
+
sx={{ mt: 2 }}
|
|
1918
|
+
>
|
|
1919
|
+
Continue to Payment
|
|
1920
|
+
</Button>
|
|
1921
|
+
</Grid>
|
|
1922
|
+
</Grid>
|
|
1923
|
+
)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1650
1926
|
if (error) {
|
|
1651
1927
|
return (
|
|
1652
1928
|
<Typography color="error">
|
|
@@ -1954,7 +2230,7 @@ export interface AddToDatabaseProps {
|
|
|
1954
2230
|
onAdd: (record: DatabaseRecord) => void
|
|
1955
2231
|
}
|
|
1956
2232
|
|
|
1957
|
-
export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onChange, onDatabaseSelect, responses, size, disabled }: FormInputProps<'Database Select'> & {
|
|
2233
|
+
export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onChange, onDatabaseSelect, responses, size, disabled, enduser }: FormInputProps<'Database Select'> & {
|
|
1958
2234
|
responses: FormResponseValue[],
|
|
1959
2235
|
AddToDatabase?: React.JSXElementConstructor<AddToDatabaseProps>,
|
|
1960
2236
|
}) => {
|
|
@@ -1988,6 +2264,23 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
|
|
|
1988
2264
|
: undefined
|
|
1989
2265
|
), [responses, field.options?.databaseFilter])
|
|
1990
2266
|
|
|
2267
|
+
// State filtering logic similar to Insurance component
|
|
2268
|
+
const addressQuestion = useMemo(() => responses?.find(r => {
|
|
2269
|
+
if (r.answer.type !== 'Address') return false
|
|
2270
|
+
if (r.field.intakeField !== 'Address') return false
|
|
2271
|
+
|
|
2272
|
+
// make sure state is actually defined (in case of multiple address questions, where 1+ are blank)
|
|
2273
|
+
if (!r.answer.value?.state) return false
|
|
2274
|
+
|
|
2275
|
+
return true
|
|
2276
|
+
}), [responses])
|
|
2277
|
+
|
|
2278
|
+
const state = useMemo(() => (
|
|
2279
|
+
field.options?.filterByEnduserState
|
|
2280
|
+
? ((addressQuestion?.answer?.type === 'Address' ? addressQuestion?.answer?.value?.state : undefined) || enduser?.state)
|
|
2281
|
+
: undefined
|
|
2282
|
+
), [enduser?.state, addressQuestion, field.options?.filterByEnduserState])
|
|
2283
|
+
|
|
1991
2284
|
const filteredChoicesWithPotentialDuplicates = useMemo(() => {
|
|
1992
2285
|
if (!choices) return []
|
|
1993
2286
|
if (!filterResponse) return choices
|
|
@@ -2030,17 +2323,29 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
|
|
|
2030
2323
|
: false
|
|
2031
2324
|
)
|
|
2032
2325
|
}
|
|
2033
|
-
|
|
2326
|
+
|
|
2034
2327
|
return false
|
|
2035
2328
|
})
|
|
2036
2329
|
)
|
|
2037
2330
|
}, [choices, filterResponse, field.options?.databaseFilter, value])
|
|
2038
2331
|
|
|
2332
|
+
// Apply state filtering as a secondary filter (doesn't modify existing logic)
|
|
2333
|
+
const stateFilteredChoices = useMemo(() => {
|
|
2334
|
+
if (!field.options?.filterByEnduserState || !state) {
|
|
2335
|
+
return filteredChoicesWithPotentialDuplicates
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
return filteredChoicesWithPotentialDuplicates.filter(c => {
|
|
2339
|
+
const recordState = c.values.find(v => v.label?.trim()?.toLowerCase() === 'state')?.value?.toString() || ''
|
|
2340
|
+
return !recordState || recordState === state
|
|
2341
|
+
})
|
|
2342
|
+
}, [filteredChoicesWithPotentialDuplicates, field.options?.filterByEnduserState, state])
|
|
2343
|
+
|
|
2039
2344
|
const filteredChoices = useMemo(() => {
|
|
2040
|
-
const filtered = []
|
|
2345
|
+
const filtered = []
|
|
2041
2346
|
|
|
2042
2347
|
const uniques = new Set<string>([])
|
|
2043
|
-
for (const c of
|
|
2348
|
+
for (const c of stateFilteredChoices) {
|
|
2044
2349
|
const text = label_for_database_record(field, c)
|
|
2045
2350
|
if (uniques.has(text)) continue // duplicate found
|
|
2046
2351
|
|
|
@@ -2049,7 +2354,7 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
|
|
|
2049
2354
|
}
|
|
2050
2355
|
|
|
2051
2356
|
return filtered
|
|
2052
|
-
}, [field,
|
|
2357
|
+
}, [field, stateFilteredChoices])
|
|
2053
2358
|
|
|
2054
2359
|
if (!doneLoading) return <LinearProgress />
|
|
2055
2360
|
return (
|