11import { useFocusEffect } from '@react-navigation/core' ;
2- import React , { useCallback , useRef , useState } from 'react' ;
2+ import React , { useCallback , useEffect , useRef , useState } from 'react' ;
33import { Alert , AppState , StyleSheet , View } from 'react-native' ;
44import type { LayoutRectangle } from 'react-native' ;
55import ReactNativeBlobUtil from 'react-native-blob-util' ;
@@ -34,6 +34,7 @@ import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath';
3434import Log from '@libs/Log' ;
3535import Navigation from '@libs/Navigation/Navigation' ;
3636import navigationRef from '@libs/Navigation/navigationRef' ;
37+ import { cancelSpan , endSpan , startSpan } from '@libs/telemetry/activeSpans' ;
3738import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper' ;
3839import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound' ;
3940import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound' ;
@@ -92,6 +93,59 @@ function IOURequestStepScan({
9293
9394 const [ policyCategories ] = useOnyx ( `${ ONYXKEYS . COLLECTION . POLICY_CATEGORIES } ${ report ?. policyID } ` ) ;
9495
96+ // Track camera init telemetry
97+ const cameraInitSpanStarted = useRef ( false ) ;
98+ const cameraInitialized = useRef ( false ) ;
99+
100+ // Start camera init span when permission is granted and camera is ready
101+ useEffect ( ( ) => {
102+ if ( cameraInitSpanStarted . current || cameraPermissionStatus !== RESULTS . GRANTED || device == null ) {
103+ return ;
104+ }
105+ startSpan ( CONST . TELEMETRY . SPAN_CAMERA_INIT , {
106+ name : CONST . TELEMETRY . SPAN_CAMERA_INIT ,
107+ op : CONST . TELEMETRY . SPAN_CAMERA_INIT ,
108+ } ) ;
109+ cameraInitSpanStarted . current = true ;
110+ } , [ cameraPermissionStatus , device ] ) ;
111+
112+ // Cancel spans when permission is denied/blocked/unavailable
113+ useEffect ( ( ) => {
114+ if ( cameraPermissionStatus !== RESULTS . BLOCKED && cameraPermissionStatus !== RESULTS . UNAVAILABLE && cameraPermissionStatus !== RESULTS . DENIED ) {
115+ return ;
116+ }
117+ cancelSpan ( CONST . TELEMETRY . SPAN_OPEN_CREATE_EXPENSE ) ;
118+ } , [ cameraPermissionStatus ] ) ;
119+
120+ // Cancel spans on unmount if camera never initialized
121+ useEffect ( ( ) => {
122+ return ( ) => {
123+ // If camera initialized successfully, spans were already ended
124+ if ( cameraInitialized . current ) {
125+ return ;
126+ }
127+ // Cancel camera init span if it was started
128+ if ( cameraInitSpanStarted . current ) {
129+ cancelSpan ( CONST . TELEMETRY . SPAN_CAMERA_INIT ) ;
130+ }
131+ // Always cancel the create expense span if camera never initialized
132+ cancelSpan ( CONST . TELEMETRY . SPAN_OPEN_CREATE_EXPENSE ) ;
133+ } ;
134+ } , [ ] ) ;
135+
136+ const handleCameraInitialized = useCallback ( ( ) => {
137+ // Prevent duplicate span endings if callback fires multiple times
138+ if ( cameraInitialized . current ) {
139+ return ;
140+ }
141+ cameraInitialized . current = true ;
142+ // Only end camera init span if it was actually started
143+ if ( cameraInitSpanStarted . current ) {
144+ endSpan ( CONST . TELEMETRY . SPAN_CAMERA_INIT ) ;
145+ }
146+ endSpan ( CONST . TELEMETRY . SPAN_OPEN_CREATE_EXPENSE ) ;
147+ } , [ ] ) ;
148+
95149 const askForPermissions = useCallback ( ( ) => {
96150 // There's no way we can check for the BLOCKED status without requesting the permission first
97151 // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670
@@ -165,6 +219,7 @@ function IOURequestStepScan({
165219
166220 return ( ) => {
167221 subscription . remove ( ) ;
222+ cancelSpan ( CONST . TELEMETRY . SPAN_SHUTTER_TO_CONFIRMATION ) ;
168223
169224 if ( isLoaderVisible ) {
170225 setIsLoaderVisible ( false ) ;
@@ -235,8 +290,24 @@ function IOURequestStepScan({
235290
236291 const viewfinderLayout = useRef < LayoutRectangle > ( null ) ;
237292
293+ const maybeCancelShutterSpan = useCallback ( ( ) => {
294+ if ( isMultiScanEnabled ) {
295+ return ;
296+ }
297+
298+ cancelSpan ( CONST . TELEMETRY . SPAN_SHUTTER_TO_CONFIRMATION ) ;
299+ } , [ isMultiScanEnabled ] ) ;
300+
238301 const capturePhoto = useCallback ( ( ) => {
302+ if ( ! isMultiScanEnabled ) {
303+ startSpan ( CONST . TELEMETRY . SPAN_SHUTTER_TO_CONFIRMATION , {
304+ name : CONST . TELEMETRY . SPAN_SHUTTER_TO_CONFIRMATION ,
305+ op : CONST . TELEMETRY . SPAN_SHUTTER_TO_CONFIRMATION ,
306+ } ) ;
307+ }
308+
239309 if ( ! camera . current && ( cameraPermissionStatus === RESULTS . DENIED || cameraPermissionStatus === RESULTS . BLOCKED ) ) {
310+ maybeCancelShutterSpan ( ) ;
240311 askForPermissions ( ) ;
241312 return ;
242313 }
@@ -246,10 +317,13 @@ function IOURequestStepScan({
246317 } ;
247318
248319 if ( ! camera . current ) {
320+ maybeCancelShutterSpan ( ) ;
249321 showCameraAlert ( ) ;
322+ return ;
250323 }
251324
252325 if ( didCapturePhoto ) {
326+ maybeCancelShutterSpan ( ) ;
253327 return ;
254328 }
255329
@@ -323,6 +397,7 @@ function IOURequestStepScan({
323397 } )
324398 . catch ( ( error : string ) => {
325399 setDidCapturePhoto ( false ) ;
400+ maybeCancelShutterSpan ( ) ;
326401 showCameraAlert ( ) ;
327402 Log . warn ( 'Error taking photo' , error ) ;
328403 } ) ;
@@ -416,6 +491,7 @@ function IOURequestStepScan({
416491 cameraTabIndex = { 1 }
417492 onLayout = { ( e ) => ( viewfinderLayout . current = e . nativeEvent . layout ) }
418493 forceInactive = { isAttachmentPickerActive }
494+ onInitialized = { handleCameraInitialized }
419495 />
420496 < Animated . View style = { [ styles . cameraFocusIndicator , cameraFocusIndicatorAnimatedStyle ] } />
421497 < Animated . View
0 commit comments