Skip to content

Commit e593c2a

Browse files
committed
feat:volume check
Signed-off-by: Bryce McMath <bryce.j.mcmath@gmail.com>
1 parent 2227b95 commit e593c2a

File tree

15 files changed

+219
-155
lines changed

15 files changed

+219
-155
lines changed

app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { ProvinceCode } from '@/bcsc-theme/utils/address-utils'
2+
import { isAxiosError } from 'axios'
13
import { useCallback, useMemo } from 'react'
4+
import { createDeviceSignedJWT } from 'react-native-bcsc-core'
25
import apiClient from '../client'
36
import { withAccount } from './withAccountGuard'
4-
import { createDeviceSignedJWT } from 'react-native-bcsc-core'
5-
import { isAxiosError } from 'axios'
6-
import { ProvinceCode } from '@/bcsc-theme/utils/address-utils'
77

88
const INVALID_REGISTRATION_REQUEST = 'invalid_registration_request'
99

@@ -31,7 +31,7 @@ export interface VerifyUnknownBCSCResponseData {
3131
expires_in: number
3232
}
3333

34-
interface AuthorizeDeviceUnknownBCSCConfig {
34+
export interface AuthorizeDeviceUnknownBCSCConfig {
3535
firstName: string
3636
lastName: string
3737
birthdate: string

app/src/bcsc-theme/components/AppBanner.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const AppBannerSection: React.FC<AppBannerSectionProps> = ({ title, type,
5252
alignItems: 'center',
5353
flexWrap: 'wrap',
5454
padding: Spacing.md,
55+
flexShrink: 1,
5556
},
5657
icon: {
5758
marginRight: Spacing.md,
@@ -108,7 +109,10 @@ export const AppBannerSection: React.FC<AppBannerSectionProps> = ({ title, type,
108109
/>
109110
<ThemedText
110111
variant={'bold'}
111-
style={{ color: type === 'warning' ? ColorPalette.brand.secondaryBackground : ColorPalette.grayscale.white }}
112+
style={{
113+
flex: 1,
114+
color: type === 'warning' ? ColorPalette.brand.secondaryBackground : ColorPalette.grayscale.white,
115+
}}
112116
testID={testIdWithKey(`text-${type}`)}
113117
>
114118
{title}

app/src/bcsc-theme/features/verify/live-call/LiveCallScreen.tsx

Lines changed: 50 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ import useVideoCallFlow from '@/bcsc-theme/features/verify/live-call/hooks/useVi
44
import { VideoCallFlowState } from '@/bcsc-theme/features/verify/live-call/types/live-call'
55
import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@/bcsc-theme/types/navigators'
66
import { BCDispatchAction, BCState } from '@/store'
7-
import { Button, ButtonType, testIdWithKey, ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core'
7+
import { ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core'
88
import { CommonActions } from '@react-navigation/native'
99
import { StackNavigationProp } from '@react-navigation/stack'
1010
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1111
import { useTranslation } from 'react-i18next'
1212
import { StyleSheet, useWindowDimensions, View } from 'react-native'
1313
import InCallManager from 'react-native-incall-manager'
1414
import { SafeAreaView } from 'react-native-safe-area-context'
15-
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
16-
import VolumeManager from 'react-native-volume-manager'
15+
import { VolumeManager } from 'react-native-volume-manager'
1716
import { MediaStreamTrack, RTCView } from 'react-native-webrtc'
17+
import CallErrorView from './components/CallErrorView'
1818
import CallIconButton from './components/CallIconButton'
1919
import CallLoadingView from './components/CallLoadingView'
20+
import CallProcessingView from './components/CallProcessingView'
21+
import { cropDelayMs } from './constants'
22+
import { clearIntervalIfExists, clearTimeoutIfExists } from './utils/clearTimeoutIfExists'
2023
import { formatCallTime } from './utils/formatCallTime'
2124

2225
type LiveCallScreenProps = {
@@ -35,17 +38,12 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
3538
const [callTimer, setCallTimer] = useState<string>('')
3639
const [systemVolume, setSystemVolume] = useState<number>(1)
3740
const timerIntervalRef = useRef<NodeJS.Timeout | null>(null)
41+
const cropDelayTimeoutRef = useRef<NodeJS.Timeout | null>(null)
3842
const { token } = useApi()
3943
const [logger] = useServices([TOKENS.UTIL_LOGGER])
4044

45+
// check if verified, save token if so, and then navigate accordingly
4146
const leaveCall = useCallback(async () => {
42-
if (timerIntervalRef.current) {
43-
clearInterval(timerIntervalRef.current)
44-
timerIntervalRef.current = null
45-
}
46-
setCallStartTime(null)
47-
setCallTimer('')
48-
4947
try {
5048
if (!store.bcsc.deviceCode || !store.bcsc.userCode) {
5149
throw new Error('Missing device or user code')
@@ -73,6 +71,9 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
7371
})
7472
)
7573
} catch {
74+
// TODO (bm): as of Sept 10th 2025, the API throws if the user is not
75+
// verified even though it isn't truly an error. We should check for
76+
// this case specifically and only throw if it's some other error
7677
logger.info('User not verified')
7778
navigation.dispatch(
7879
CommonActions.reset({
@@ -87,6 +88,7 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
8788
}
8889
}, [store.bcsc.deviceCode, store.bcsc.userCode, token, dispatch, navigation, logger])
8990

91+
// we pass the leaveCall function to the hook so it can use it when the other side disconnects as well
9092
const {
9193
flowState,
9294
videoCallError,
@@ -99,18 +101,29 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
99101
setCallEnded,
100102
} = useVideoCallFlow(leaveCall)
101103

102-
const inCall = useMemo(() => flowState === VideoCallFlowState.IN_CALL, [flowState])
103-
104+
// start crop delay timeout when call starts. the crop delay is to match the
105+
// current BCSC where the timer doesn't start until after 11 seconds. In
106+
// future we can use this 11 seconds to crop the bottom part of the remote video that
107+
// shows the users video for the first ten seconds (we don't want that extra
108+
// feed of the users video since we are already showing it ourselves)
104109
useEffect(() => {
105110
if (flowState === VideoCallFlowState.IN_CALL && !callStartTime) {
106-
const startTime = Date.now()
107-
setCallStartTime(startTime)
111+
cropDelayTimeoutRef.current = setTimeout(() => {
112+
const startTime = Date.now()
113+
setCallStartTime(startTime)
114+
}, cropDelayMs)
108115
} else if (flowState !== VideoCallFlowState.IN_CALL && callStartTime) {
109116
setCallStartTime(null)
110117
setCallTimer('')
111118
}
119+
120+
return () => {
121+
clearTimeoutIfExists(cropDelayTimeoutRef)
122+
}
112123
}, [flowState, callStartTime])
113124

125+
// when call start time is first set, begin updating the user-facing
126+
// display of the call length
114127
useEffect(() => {
115128
if (callStartTime) {
116129
const updateTimer = () => {
@@ -122,31 +135,16 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
122135
updateTimer()
123136

124137
timerIntervalRef.current = setInterval(updateTimer, 1000)
125-
126-
return () => {
127-
if (timerIntervalRef.current) {
128-
clearInterval(timerIntervalRef.current)
129-
timerIntervalRef.current = null
130-
}
131-
}
132-
} else {
133-
setCallTimer('')
134-
if (timerIntervalRef.current) {
135-
clearInterval(timerIntervalRef.current)
136-
timerIntervalRef.current = null
137-
}
138138
}
139139
}, [callStartTime])
140140

141141
useEffect(() => {
142142
return () => {
143-
if (timerIntervalRef.current) {
144-
clearInterval(timerIntervalRef.current)
145-
}
143+
clearIntervalIfExists(timerIntervalRef)
146144
}
147145
}, [])
148146

149-
// Monitor device volume changes
147+
// setup volume detection
150148
useEffect(() => {
151149
const getInitialVolume = async () => {
152150
try {
@@ -168,26 +166,25 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
168166
}
169167
}, [logger])
170168

169+
// Determine which banner notice to show in an order of priority
171170
const banner: { type: 'warning' | 'error'; title: string } | null = useMemo(() => {
172171
if (isInBackground) {
173-
return { type: 'warning', title: t('Unified.VideoCall.VideoWillResume') }
172+
return { type: 'warning', title: t('Unified.VideoCall.Banners.VideoWillResume') }
174173
}
175174
if (videoHidden) {
176-
return { type: 'error', title: t('Unified.VideoCall.AgentCantSeeYou') }
177-
}
178-
if (systemVolume === 0) {
179-
return { type: 'error', title: 'Please turn up the volume to hear the agent' }
175+
return { type: 'error', title: t('Unified.VideoCall.Banners.AgentCantSeeYou') }
180176
}
181177
if (onMute) {
182-
return { type: 'error', title: t('Unified.VideoCall.AgentCantHearYou') }
178+
return { type: 'error', title: t('Unified.VideoCall.Banners.AgentCantHearYou') }
183179
}
184180
if (systemVolume < 0.2) {
185-
return { type: 'warning', title: 'Your volume is low, you may need to turn it up to hear the agent' }
181+
return { type: 'warning', title: t('Unified.VideoCall.Banners.VolumeLow') }
186182
}
187183

188184
return null
189-
}, [isInBackground, onMute, videoHidden, inCall, systemVolume, t])
185+
}, [isInBackground, onMute, videoHidden, systemVolume, t])
190186

187+
// whenever mute choice changes, update the audio tracks accordingly
191188
useEffect(() => {
192189
if (localStream) {
193190
const audioTracks = localStream.getAudioTracks()
@@ -197,6 +194,7 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
197194
}
198195
}, [onMute, localStream])
199196

197+
// whenever hide video choice changes, update the video tracks accordingly
200198
useEffect(() => {
201199
if (localStream) {
202200
const videoTracks = localStream.getVideoTracks()
@@ -214,14 +212,15 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
214212
setVideoHidden((prev) => !prev)
215213
}, [])
216214

215+
// kick off the process only once (flow state doesn't go back to idle)
217216
useEffect(() => {
218-
// only start call automatically once (flow state doesn't go back to idle)
219217
if (flowState === VideoCallFlowState.IDLE) {
220218
startVideoCall()
221219
InCallManager.start({ media: 'video', auto: true })
222220
}
223221
}, [flowState, startVideoCall])
224222

223+
// loading / error user-facing state message
225224
const stateMessage = useMemo(() => {
226225
switch (flowState) {
227226
case VideoCallFlowState.CREATING_SESSION:
@@ -239,50 +238,30 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
239238
}
240239
}, [flowState, videoCallError, t])
241240

241+
// when the user presses the end call button
242242
const handleEndCall = useCallback(async () => {
243243
try {
244-
await cleanup()
244+
logger.info('User initiated call end')
245245
setCallEnded()
246+
await cleanup()
246247
await leaveCall()
247248
} catch (error) {
248249
logger.error('Error while leaving video call', error as Error)
249250
}
250-
}, [cleanup, leaveCall, logger])
251+
}, [setCallEnded, cleanup, leaveCall, logger])
251252

252253
if (flowState === VideoCallFlowState.ERROR) {
253254
return (
254-
<SafeAreaView style={{ flex: 1, backgroundColor: ColorPalette.brand.primaryBackground, padding: Spacing.md }}>
255-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
256-
<Icon name="alert-circle" size={64} color={ColorPalette.semantic.error} />
257-
<ThemedText variant={'headingTwo'} style={{ marginTop: Spacing.lg, textAlign: 'center' }}>
258-
{t('Unified.VideoCall.ConnectionError')}
259-
</ThemedText>
260-
<ThemedText style={{ marginTop: Spacing.md, textAlign: 'center' }}>{stateMessage}</ThemedText>
261-
</View>
262-
<View style={{ gap: Spacing.sm }}>
263-
{videoCallError?.retryable && (
264-
<Button
265-
buttonType={ButtonType.Primary}
266-
onPress={retryConnection}
267-
title={t('Unified.VideoCall.TryAgain')}
268-
accessibilityLabel={t('Unified.VideoCall.TryAgain')}
269-
testID={testIdWithKey('TryAgain')}
270-
/>
271-
)}
272-
<Button
273-
buttonType={ButtonType.Secondary}
274-
onPress={() => navigation.goBack()}
275-
title={t('Unified.VideoCall.GoBack')}
276-
accessibilityLabel={t('Unified.VideoCall.GoBack')}
277-
testID={testIdWithKey('GoBack')}
278-
/>
279-
</View>
280-
</SafeAreaView>
255+
<CallErrorView
256+
message={stateMessage || t('Unified.VideoCall.Errors.GenericError')}
257+
onGoBack={() => navigation.goBack()}
258+
onRetry={videoCallError?.retryable ? retryConnection : undefined}
259+
/>
281260
)
282261
}
283262

284263
if (flowState === VideoCallFlowState.CALL_ENDED) {
285-
return <CallLoadingView message={t('Unified.VideoCall.CallStates.CallEnded')} />
264+
return <CallProcessingView message={t('Unified.VideoCall.CallStates.CallEnded')} />
286265
}
287266

288267
if (flowState !== VideoCallFlowState.IN_CALL) {
@@ -337,7 +316,7 @@ const LiveCallScreen = ({ navigation }: LiveCallScreenProps) => {
337316
<SafeAreaView edges={['left', 'right']} style={{ flex: 1, justifyContent: 'space-between' }}>
338317
<View style={styles.upperContainer}>
339318
<View style={styles.timeAndLabelContainer}>
340-
<ThemedText>{inCall ? 'Service BC' : 'In Queue'}</ThemedText>
319+
<ThemedText>{t('Unified.VideoCall.ServiceBC')}</ThemedText>
341320
{callTimer ? <ThemedText>{callTimer}</ThemedText> : null}
342321
</View>
343322
{banner ? <BannerSection type={banner.type} title={banner.title} dismissible={false} /> : null}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Button, ButtonType, testIdWithKey, ThemedText, useTheme } from '@bifold/core'
2+
import { useTranslation } from 'react-i18next'
3+
import { View } from 'react-native'
4+
import { SafeAreaView } from 'react-native-safe-area-context'
5+
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
6+
7+
type CallErrorViewProps = {
8+
message: string
9+
onRetry?: () => void
10+
onGoBack: () => void
11+
}
12+
13+
const CallErrorView = ({ message, onRetry, onGoBack }: CallErrorViewProps) => {
14+
const { ColorPalette, Spacing } = useTheme()
15+
const { t } = useTranslation()
16+
return (
17+
<SafeAreaView style={{ flex: 1, backgroundColor: ColorPalette.brand.primaryBackground, padding: Spacing.md }}>
18+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
19+
<Icon name="alert-circle" size={64} color={ColorPalette.semantic.error} />
20+
<ThemedText variant={'headingTwo'} style={{ marginTop: Spacing.lg, textAlign: 'center' }}>
21+
{t('Unified.VideoCall.ConnectionError')}
22+
</ThemedText>
23+
<ThemedText style={{ marginTop: Spacing.md, textAlign: 'center' }}>{message}</ThemedText>
24+
</View>
25+
<View style={{ gap: Spacing.sm }}>
26+
{onRetry && (
27+
<Button
28+
buttonType={ButtonType.Primary}
29+
onPress={onRetry}
30+
title={t('Unified.VideoCall.TryAgain')}
31+
accessibilityLabel={t('Unified.VideoCall.TryAgain')}
32+
testID={testIdWithKey('TryAgain')}
33+
/>
34+
)}
35+
<Button
36+
buttonType={ButtonType.Secondary}
37+
onPress={onGoBack}
38+
title={t('Unified.VideoCall.GoBack')}
39+
accessibilityLabel={t('Unified.VideoCall.GoBack')}
40+
testID={testIdWithKey('GoBack')}
41+
/>
42+
</View>
43+
</SafeAreaView>
44+
)
45+
}
46+
47+
export default CallErrorView

app/src/bcsc-theme/features/verify/live-call/components/CallLoadingView.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { StyleSheet, View } from 'react-native'
77
import { SafeAreaView } from 'react-native-safe-area-context'
88

99
type CallLoadingViewProps = {
10-
onCancel?: () => void
10+
onCancel: () => void
1111
message?: string
1212
}
1313

@@ -79,15 +79,13 @@ const CallLoadingView = ({ onCancel, message }: CallLoadingViewProps) => {
7979
</View>
8080
</View>
8181
<View style={styles.controlsContainer}>
82-
{onCancel ? (
83-
<Button
84-
buttonType={ButtonType.Primary}
85-
onPress={onCancel}
86-
title={t('Global.Cancel')}
87-
accessibilityLabel={t('Global.Cancel')}
88-
testID={testIdWithKey('Cancel')}
89-
/>
90-
) : null}
82+
<Button
83+
buttonType={ButtonType.Primary}
84+
onPress={onCancel}
85+
title={t('Global.Cancel')}
86+
accessibilityLabel={t('Global.Cancel')}
87+
testID={testIdWithKey('Cancel')}
88+
/>
9189
</View>
9290
</SafeAreaView>
9391
)

0 commit comments

Comments
 (0)