@tellescope/react-components 1.252.2 → 1.253.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 (56) hide show
  1. package/lib/cjs/CMS/components.d.ts +0 -1
  2. package/lib/cjs/CMS/components.d.ts.map +1 -1
  3. package/lib/cjs/Forms/forms.d.ts +3 -3
  4. package/lib/cjs/Forms/forms.v2.d.ts +3 -3
  5. package/lib/cjs/Forms/inputs.d.ts +2 -2
  6. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  7. package/lib/cjs/Forms/inputs.js +28 -103
  8. package/lib/cjs/Forms/inputs.js.map +1 -1
  9. package/lib/cjs/Forms/inputs.native.d.ts +0 -1
  10. package/lib/cjs/Forms/inputs.native.d.ts.map +1 -1
  11. package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
  12. package/lib/cjs/TwilioVideo/TwilioControls.js +60 -43
  13. package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
  14. package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
  15. package/lib/cjs/TwilioVideo/TwilioParticipant.js +33 -18
  16. package/lib/cjs/TwilioVideo/TwilioParticipant.js.map +1 -1
  17. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +4 -1
  18. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  19. package/lib/cjs/TwilioVideo/TwilioVideoContext.js +45 -9
  20. package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
  21. package/lib/cjs/TwilioVideo/hooks.d.ts +1 -1
  22. package/lib/cjs/TwilioVideo/index.d.ts +1 -1
  23. package/lib/cjs/TwilioVideo/index.d.ts.map +1 -1
  24. package/lib/cjs/TwilioVideo/index.js +2 -1
  25. package/lib/cjs/TwilioVideo/index.js.map +1 -1
  26. package/lib/cjs/controls.d.ts +2 -2
  27. package/lib/cjs/inputs.d.ts +1 -1
  28. package/lib/cjs/inputs.native.d.ts +0 -1
  29. package/lib/cjs/inputs.native.d.ts.map +1 -1
  30. package/lib/cjs/state.d.ts +330 -330
  31. package/lib/cjs/theme.native.d.ts +0 -1
  32. package/lib/cjs/theme.native.d.ts.map +1 -1
  33. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  34. package/lib/esm/Forms/inputs.js +28 -103
  35. package/lib/esm/Forms/inputs.js.map +1 -1
  36. package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
  37. package/lib/esm/TwilioVideo/TwilioControls.js +62 -45
  38. package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
  39. package/lib/esm/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
  40. package/lib/esm/TwilioVideo/TwilioParticipant.js +33 -18
  41. package/lib/esm/TwilioVideo/TwilioParticipant.js.map +1 -1
  42. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +4 -1
  43. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  44. package/lib/esm/TwilioVideo/TwilioVideoContext.js +45 -9
  45. package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
  46. package/lib/esm/TwilioVideo/index.d.ts +1 -1
  47. package/lib/esm/TwilioVideo/index.d.ts.map +1 -1
  48. package/lib/esm/TwilioVideo/index.js +1 -1
  49. package/lib/esm/TwilioVideo/index.js.map +1 -1
  50. package/lib/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +9 -9
  52. package/src/Forms/inputs.tsx +22 -101
  53. package/src/TwilioVideo/TwilioControls.tsx +53 -3
  54. package/src/TwilioVideo/TwilioParticipant.tsx +27 -16
  55. package/src/TwilioVideo/TwilioVideoContext.tsx +39 -3
  56. package/src/TwilioVideo/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.252.2",
3
+ "version": "1.253.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,13 +51,13 @@
51
51
  "@reduxjs/toolkit": "1.9.0",
52
52
  "@stripe/react-stripe-js": "2.9.0",
53
53
  "@stripe/stripe-js": "1.52.1",
54
- "@tellescope/constants": "1.252.2",
55
- "@tellescope/sdk": "1.252.2",
56
- "@tellescope/types-client": "1.252.2",
57
- "@tellescope/types-models": "1.252.2",
58
- "@tellescope/types-utilities": "1.252.2",
59
- "@tellescope/utilities": "1.252.2",
60
- "@tellescope/validation": "1.252.2",
54
+ "@tellescope/constants": "1.253.0",
55
+ "@tellescope/sdk": "1.253.0",
56
+ "@tellescope/types-client": "1.253.0",
57
+ "@tellescope/types-models": "1.253.0",
58
+ "@tellescope/types-utilities": "1.253.0",
59
+ "@tellescope/utilities": "1.253.0",
60
+ "@tellescope/validation": "1.253.0",
61
61
  "@twilio/video-processors": "3.2.0",
62
62
  "css-to-react-native": "3.0.0",
63
63
  "draft-js": "0.11.7",
@@ -85,7 +85,7 @@
85
85
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
86
86
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
87
87
  },
88
- "gitHead": "139cc97a480c33133853a01d1e864f56863425c9",
88
+ "gitHead": "b99e2fae181b67bab736660f9e4b8f47acc276da",
89
89
  "publishConfig": {
90
90
  "access": "public"
91
91
  }
@@ -1439,18 +1439,9 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1439
1439
  }) => {
1440
1440
  const session = useResolvedSession()
1441
1441
  const [loading, setLoading] = useState(false)
1442
- const [polling, setPolling] = useState(false)
1443
1442
  const [error, setError] = useState<string>()
1444
1443
 
1445
1444
  const isEnduserSession = session.type === 'enduser'
1446
- const pollTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
1447
-
1448
- // Clean up polling timeout on unmount
1449
- useEffect(() => {
1450
- return () => {
1451
- if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current)
1452
- }
1453
- }, [])
1454
1445
 
1455
1446
  // Extract payerId from Insurance question response
1456
1447
  const [payerId, memberId, payerName] = useMemo(() => {
@@ -1470,7 +1461,7 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1470
1461
  setError(undefined)
1471
1462
 
1472
1463
  try {
1473
- // Step 1: Initiate eligibility check (creates patient → coverage → check)
1464
+ // Single synchronous eligibility check
1474
1465
  const { data } = await session.api.integrations.proxy_read({
1475
1466
  id: enduserId,
1476
1467
  integration: CANDID_TITLE,
@@ -1484,76 +1475,17 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1484
1475
  }),
1485
1476
  })
1486
1477
 
1487
- const coverageId = data?.coverageId
1488
- const checkId = data?.checkId
1489
- const initialStatus = data?.status
1490
-
1491
- if (!coverageId || !checkId) {
1492
- throw new Error('No coverage ID or check ID returned from eligibility check')
1493
- }
1494
-
1495
- // If already completed, update answer immediately
1496
- if (initialStatus === 'COMPLETED' || initialStatus === 'FAILED' || initialStatus === 'UNKNOWN') {
1497
- onChange({
1498
- payerId,
1499
- status: initialStatus,
1500
- coverageId,
1501
- }, field.id)
1502
- setLoading(false)
1503
- return
1504
- }
1505
-
1506
- // Step 2: Poll for results
1478
+ onChange({
1479
+ payerId,
1480
+ status: data?.status,
1481
+ benefits: data?.benefits,
1482
+ planMetadata: data?.planMetadata,
1483
+ }, field.id)
1507
1484
  setLoading(false)
1508
- setPolling(true)
1509
-
1510
- const maxAttempts = 60 // 2 minutes at 2s intervals
1511
- let attempts = 0
1512
-
1513
- const pollForResult = async (): Promise<void> => {
1514
- if (attempts >= maxAttempts) {
1515
- setError('Eligibility check timed out. Please try again.')
1516
- setPolling(false)
1517
- return
1518
- }
1519
-
1520
- attempts++
1521
-
1522
- try {
1523
- const { data: pollData } = await session.api.integrations.proxy_read({
1524
- id: coverageId,
1525
- integration: CANDID_TITLE,
1526
- type: 'candid-eligibility-poll',
1527
- query: JSON.stringify({ checkId }),
1528
- })
1529
-
1530
- const status = pollData?.status
1531
- // Terminal statuses: COMPLETED, FAILED, or UNKNOWN (Candid returns UNKNOWN when eligibility cannot be determined)
1532
- if (status === 'COMPLETED' || status === 'FAILED' || status === 'UNKNOWN') {
1533
- onChange({
1534
- payerId,
1535
- status,
1536
- coverageId,
1537
- benefits: pollData?.benefits,
1538
- }, field.id)
1539
- setPolling(false)
1540
- return
1541
- }
1542
-
1543
- // Still pending, poll again
1544
- pollTimeoutRef.current = setTimeout(pollForResult, 2000)
1545
- } catch (err: any) {
1546
- setError(err?.message || 'Failed to check eligibility status')
1547
- setPolling(false)
1548
- }
1549
- }
1550
-
1551
- pollForResult()
1552
1485
  } catch (err: any) {
1553
1486
  setError(err?.message || 'Failed to check eligibility')
1554
1487
  console.error('Candid eligibility check failed:', err)
1555
1488
  setLoading(false)
1556
- setPolling(false)
1557
1489
  }
1558
1490
  }, [session, field, payerId, memberId, payerName, onChange, enduserId])
1559
1491
 
@@ -1608,43 +1540,40 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1608
1540
  </Grid>
1609
1541
  <Grid item>
1610
1542
  <Typography variant="body1">
1611
- {polling ? 'Verifying eligibility with insurance...' : 'Checking eligibility...'}
1543
+ Checking eligibility...
1612
1544
  </Typography>
1613
1545
  </Grid>
1614
1546
  <Grid item>
1615
1547
  <Typography variant="body2" color="textSecondary">
1616
- {polling ? 'This usually takes 15-30 seconds' : 'This may take a few moments'}
1548
+ This may take a few moments
1617
1549
  </Typography>
1618
1550
  </Grid>
1619
1551
  </Grid>
1620
- ), [polling])
1552
+ ), [])
1621
1553
 
1622
1554
  const resultsComponent = useMemo(() => {
1623
- const isCompleted = value?.status === 'COMPLETED'
1624
- const isFailed = value?.status === 'FAILED'
1555
+ const isActive = value?.status === 'ACTIVE'
1625
1556
  return (
1626
1557
  <Grid container spacing={2} direction="column">
1627
1558
  <Grid item>
1628
1559
  <Paper style={{
1629
1560
  padding: 16,
1630
- backgroundColor: isCompleted ? '#e8f5e9' : '#ffebee',
1631
- border: `2px solid ${isCompleted ? '#4caf50' : '#f44336'}`
1561
+ backgroundColor: isActive ? '#e8f5e9' : '#fff8e1',
1562
+ border: `2px solid ${isActive ? '#4caf50' : '#ffa000'}`
1632
1563
  }}>
1633
1564
  <Grid container spacing={2} direction="column" alignItems="center">
1634
1565
  <Grid item>
1635
- {isCompleted ? (
1566
+ {isActive ? (
1636
1567
  <CheckCircleOutline style={{ fontSize: 48, color: '#4caf50' }} />
1637
1568
  ) : (
1638
- <Typography variant="h2" style={{ color: '#f44336' }}>!</Typography>
1569
+ <Typography variant="h2" style={{ color: '#ffa000' }}>!</Typography>
1639
1570
  )}
1640
1571
  </Grid>
1641
1572
  <Grid item>
1642
1573
  <Typography variant="h6" align="center">
1643
- {isCompleted
1574
+ {isActive
1644
1575
  ? `${payerName || 'Insurance'} eligibility verified`
1645
- : isFailed
1646
- ? 'Eligibility check failed'
1647
- : 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
1576
+ : 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
1648
1577
  }
1649
1578
  </Typography>
1650
1579
  </Grid>
@@ -1655,9 +1584,9 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1655
1584
  )
1656
1585
  }, [value, payerName])
1657
1586
 
1658
- // Loading/polling state for enduser sessions
1587
+ // Loading state for enduser sessions
1659
1588
  if (isEnduserSession) {
1660
- if (loading || polling) { return checkingEligibilityComponent }
1589
+ if (loading) { return checkingEligibilityComponent }
1661
1590
  if (error) {
1662
1591
  return errorComponent
1663
1592
  }
@@ -1687,23 +1616,15 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
1687
1616
  </Grid>
1688
1617
  )}
1689
1618
 
1690
- {polling && (
1691
- <Grid item>
1692
- <Typography variant="body2" color="primary">
1693
- {form_display_text_for_language(form, "Polling for results... (this may take 15-30 seconds)")}
1694
- </Typography>
1695
- </Grid>
1696
- )}
1697
-
1698
1619
  <Grid item container spacing={2}>
1699
1620
  <Grid item>
1700
1621
  <LoadingButton
1701
1622
  variant="outlined"
1702
1623
  onClick={checkEligibility}
1703
1624
  submitText={form_display_text_for_language(form, "Check Eligibility")}
1704
- submittingText={polling ? form_display_text_for_language(form, "Polling...") : form_display_text_for_language(form, "Checking...")}
1705
- submitting={loading || polling}
1706
- disabled={loading || polling}
1625
+ submittingText={form_display_text_for_language(form, "Checking...")}
1626
+ submitting={loading}
1627
+ disabled={loading}
1707
1628
  />
1708
1629
  </Grid>
1709
1630
  </Grid>
@@ -1,5 +1,16 @@
1
- import React from 'react'
2
- import { Box, IconButton, Button, CircularProgress } from '@mui/material'
1
+ import React, { useState } from 'react'
2
+ import {
3
+ Box,
4
+ IconButton,
5
+ Button,
6
+ CircularProgress,
7
+ Dialog,
8
+ DialogTitle,
9
+ DialogContent,
10
+ DialogActions,
11
+ FormControlLabel,
12
+ Checkbox,
13
+ } from '@mui/material'
3
14
  import {
4
15
  Mic as MicIcon,
5
16
  MicOff as MicOffIcon,
@@ -43,6 +54,24 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
43
54
  room,
44
55
  } = useTwilioVideo()
45
56
 
57
+ const [shareDialogOpen, setShareDialogOpen] = useState(false)
58
+ const [shareAudio, setShareAudio] = useState(true)
59
+
60
+ const handleScreenShareClick = () => {
61
+ if (isScreenSharing) {
62
+ // Already sharing — stop immediately, no dialog
63
+ toggleScreenShare()
64
+ } else {
65
+ setShareDialogOpen(true)
66
+ }
67
+ }
68
+
69
+ const handleConfirmShare = () => {
70
+ // This click is a user gesture, so getDisplayMedia is allowed
71
+ toggleScreenShare({ shareAudio })
72
+ setShareDialogOpen(false)
73
+ }
74
+
46
75
  const handleLeave = () => {
47
76
  disconnect()
48
77
  onLeave?.()
@@ -58,6 +87,7 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
58
87
  && typeof navigator.mediaDevices.getDisplayMedia === 'function'
59
88
 
60
89
  return (
90
+ <>
61
91
  <Box
62
92
  sx={{
63
93
  display: 'flex',
@@ -115,7 +145,7 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
115
145
 
116
146
  {showScreenShareProp && supportsScreenShare && (
117
147
  <IconButton
118
- onClick={toggleScreenShare}
148
+ onClick={handleScreenShareClick}
119
149
  disabled={!room}
120
150
  sx={{
121
151
  color: isScreenSharing ? '#4caf50' : 'white',
@@ -162,5 +192,25 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
162
192
  </Button>
163
193
  )}
164
194
  </Box>
195
+
196
+ <Dialog open={shareDialogOpen} onClose={() => setShareDialogOpen(false)}>
197
+ <DialogTitle>Share your screen</DialogTitle>
198
+ <DialogContent>
199
+ <FormControlLabel
200
+ control={(
201
+ <Checkbox
202
+ checked={shareAudio}
203
+ onChange={e => setShareAudio(e.target.checked)}
204
+ />
205
+ )}
206
+ label="Also share audio"
207
+ />
208
+ </DialogContent>
209
+ <DialogActions>
210
+ <Button onClick={() => setShareDialogOpen(false)}>Cancel</Button>
211
+ <Button variant="contained" onClick={handleConfirmShare}>Share</Button>
212
+ </DialogActions>
213
+ </Dialog>
214
+ </>
165
215
  )
166
216
  }
@@ -4,6 +4,7 @@ import {
4
4
  LocalParticipant,
5
5
  VideoTrack,
6
6
  AudioTrack,
7
+ RemoteAudioTrack,
7
8
  RemoteTrackPublication,
8
9
  LocalTrackPublication,
9
10
  } from 'twilio-video'
@@ -20,6 +21,23 @@ export interface TwilioParticipantProps {
20
21
  showScreenShare?: boolean
21
22
  }
22
23
 
24
+ /** Renders a single remote audio track into its own <audio> element. */
25
+ const RemoteAudioTrackElement: React.FC<{ track: AudioTrack }> = ({ track }) => {
26
+ const audioRef = useRef<HTMLAudioElement>(null)
27
+
28
+ useEffect(() => {
29
+ if (audioRef.current) {
30
+ const el = audioRef.current
31
+ track.attach(el)
32
+ return () => {
33
+ track.detach(el)
34
+ }
35
+ }
36
+ }, [track])
37
+
38
+ return <audio ref={audioRef} autoPlay />
39
+ }
40
+
23
41
  export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
24
42
  participant,
25
43
  isLocal = false,
@@ -28,10 +46,9 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
28
46
  showScreenShare = false,
29
47
  }) => {
30
48
  const videoRef = useRef<HTMLVideoElement>(null)
31
- const audioRef = useRef<HTMLAudioElement>(null)
32
49
  const [cameraTrack, setCameraTrack] = useState<VideoTrack | null>(null)
33
50
  const [screenTrack, setScreenTrack] = useState<VideoTrack | null>(null)
34
- const [audioTrack, setAudioTrack] = useState<AudioTrack | null>(null)
51
+ const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([])
35
52
 
36
53
  useEffect(() => {
37
54
  const handleTrackSubscribed = (track: VideoTrack | AudioTrack) => {
@@ -42,7 +59,9 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
42
59
  setCameraTrack(track as VideoTrack)
43
60
  }
44
61
  } else if (track.kind === 'audio') {
45
- setAudioTrack(track as AudioTrack)
62
+ // Support multiple simultaneous audio tracks (e.g. microphone + screen-share audio).
63
+ // Add by identity, deduping so the same track isn't attached twice.
64
+ setAudioTracks(prev => (prev.includes(track as AudioTrack) ? prev : [...prev, track as AudioTrack]))
46
65
  }
47
66
  }
48
67
 
@@ -54,7 +73,7 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
54
73
  setCameraTrack(null)
55
74
  }
56
75
  } else if (track.kind === 'audio') {
57
- setAudioTrack(null)
76
+ setAudioTracks(prev => prev.filter(t => t !== (track as AudioTrack)))
58
77
  }
59
78
  }
60
79
 
@@ -107,17 +126,6 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
107
126
  }
108
127
  }, [activeVideoTrack])
109
128
 
110
- // Attach audio track (not for local participant to avoid echo)
111
- useEffect(() => {
112
- if (audioTrack && audioRef.current && !isLocal) {
113
- const el = audioRef.current
114
- audioTrack.attach(el)
115
- return () => {
116
- audioTrack.detach(el)
117
- }
118
- }
119
- }, [audioTrack, isLocal])
120
-
121
129
  const shouldMirror = isLocal && !showScreenShare
122
130
 
123
131
  return (
@@ -144,7 +152,10 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
144
152
  transform: shouldMirror ? 'scaleX(-1)' : 'none',
145
153
  }}
146
154
  />
147
- {!isLocal && <audio ref={audioRef} autoPlay />}
155
+ {/* Render one audio element per track so simultaneous tracks (mic + screen-share audio) all play */}
156
+ {!isLocal && audioTracks.map(track => (
157
+ <RemoteAudioTrackElement key={(track as RemoteAudioTrack).sid} track={track} />
158
+ ))}
148
159
  {(() => {
149
160
  const label = resolveIdentity(participant.identity)
150
161
  if (!label && !isLocal) return null
@@ -11,6 +11,7 @@ import Video, {
11
11
  import type { GaussianBlurBackgroundProcessor as GaussianBlurBackgroundProcessorType } from '@twilio/video-processors'
12
12
 
13
13
  export const SCREEN_SHARE_TRACK_NAME = 'screen-share'
14
+ export const SCREEN_SHARE_AUDIO_TRACK_NAME = 'screen-share-audio'
14
15
 
15
16
  export const BLUR_BACKGROUND_STORAGE_KEY = 'tellescope.twilio.blurBackground'
16
17
  export const BLUR_BACKGROUND_ASSETS_PATH = '/twilio-video-processors'
@@ -64,7 +65,7 @@ export interface TwilioVideoActions {
64
65
  disconnect: () => void
65
66
  toggleVideo: () => Promise<void>
66
67
  toggleAudio: () => void
67
- toggleScreenShare: () => Promise<void>
68
+ toggleScreenShare: (options?: { shareAudio?: boolean }) => Promise<void>
68
69
  toggleBlur: () => void
69
70
  setIsHost: (isHost: boolean) => void
70
71
  }
@@ -103,6 +104,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
103
104
  const [isBlurLoading, setIsBlurLoading] = useState(false)
104
105
 
105
106
  const localTracksRef = useRef<(LocalVideoTrack | LocalAudioTrack)[]>([])
107
+ const screenAudioTrackRef = useRef<LocalAudioTrack | null>(null)
106
108
  const blurProcessorRef = useRef<GaussianBlurBackgroundProcessorType | null>(null)
107
109
  const blurAttachedTrackRef = useRef<LocalVideoTrack | null>(null)
108
110
 
@@ -170,6 +172,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
170
172
  track.stop()
171
173
  })
172
174
  localTracksRef.current = []
175
+ screenAudioTrackRef.current = null
173
176
  setRoom(null)
174
177
  setLocalVideoTrack(null)
175
178
  setLocalAudioTrack(null)
@@ -197,6 +200,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
197
200
  track.stop()
198
201
  })
199
202
  localTracksRef.current = []
203
+ screenAudioTrackRef.current = null
200
204
 
201
205
  setRoom(null)
202
206
  setLocalVideoTrack(null)
@@ -239,16 +243,27 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
239
243
  setLocalScreenTrack(null)
240
244
  setIsScreenSharing(false)
241
245
  }
246
+
247
+ // Tear down the accompanying screen-share audio track, if any
248
+ if (screenAudioTrackRef.current) {
249
+ const audioTrack = screenAudioTrackRef.current
250
+ if (room) {
251
+ room.localParticipant.unpublishTrack(audioTrack)
252
+ }
253
+ audioTrack.stop()
254
+ localTracksRef.current = localTracksRef.current.filter(t => t !== audioTrack)
255
+ screenAudioTrackRef.current = null
256
+ }
242
257
  }, [localScreenTrack, room])
243
258
 
244
- const toggleScreenShare = useCallback(async () => {
259
+ const toggleScreenShare = useCallback(async (options?: { shareAudio?: boolean }) => {
245
260
  if (isScreenSharing) {
246
261
  stopScreenShare()
247
262
  return
248
263
  }
249
264
 
250
265
  try {
251
- const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })
266
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: !!options?.shareAudio })
252
267
  const mediaStreamTrack = stream.getVideoTracks()[0]
253
268
  const screenTrack = new LocalVideoTrack(mediaStreamTrack, { name: SCREEN_SHARE_TRACK_NAME })
254
269
 
@@ -260,6 +275,18 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
260
275
  setLocalScreenTrack(screenTrack)
261
276
  setIsScreenSharing(true)
262
277
 
278
+ // Capture & publish screen/tab audio if the browser provided an audio track
279
+ const audioMediaStreamTrack = stream.getAudioTracks()[0]
280
+ let screenAudioTrack: LocalAudioTrack | null = null
281
+ if (audioMediaStreamTrack) {
282
+ screenAudioTrack = new LocalAudioTrack(audioMediaStreamTrack, { name: SCREEN_SHARE_AUDIO_TRACK_NAME })
283
+ if (room) {
284
+ await room.localParticipant.publishTrack(screenAudioTrack)
285
+ }
286
+ localTracksRef.current.push(screenAudioTrack)
287
+ screenAudioTrackRef.current = screenAudioTrack
288
+ }
289
+
263
290
  // Handle browser "Stop sharing" button
264
291
  mediaStreamTrack.onended = () => {
265
292
  if (room) {
@@ -269,6 +296,15 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
269
296
  localTracksRef.current = localTracksRef.current.filter(t => t !== screenTrack)
270
297
  setLocalScreenTrack(null)
271
298
  setIsScreenSharing(false)
299
+
300
+ if (screenAudioTrack) {
301
+ if (room) {
302
+ room.localParticipant.unpublishTrack(screenAudioTrack)
303
+ }
304
+ screenAudioTrack.stop()
305
+ localTracksRef.current = localTracksRef.current.filter(t => t !== screenAudioTrack)
306
+ screenAudioTrackRef.current = null
307
+ }
272
308
  }
273
309
  } catch (err) {
274
310
  // User cancelled the screen share picker — not an error
@@ -2,6 +2,7 @@ export {
2
2
  TwilioVideoProvider,
3
3
  useTwilioVideo,
4
4
  SCREEN_SHARE_TRACK_NAME,
5
+ SCREEN_SHARE_AUDIO_TRACK_NAME,
5
6
  type TwilioVideoState,
6
7
  type TwilioVideoActions,
7
8
  type TwilioVideoContextType,