@@ -2,7 +2,7 @@ import {useEffect, useRef, useState} from 'react';
22import type { OnyxEntry } from 'react-native-onyx' ;
33import useOnyx from '@hooks/useOnyx' ;
44import { checkIfLocalFileIsAccessible } from '@libs/actions/IOU/Receipt' ;
5- import clearOdometerDraftTransactionState from '@libs/actions/OdometerTransactionUtils' ;
5+ import clearOdometerDraftTransactionState , { hydrateOdometerDraftIntoTransaction } from '@libs/actions/OdometerTransactionUtils' ;
66import { navigateToStartMoneyRequestStep } from '@libs/IOUUtils' ;
77import { getOdometerImageUri } from '@libs/OdometerImageUtils' ;
88import type { IOUType } from '@src/CONST' ;
@@ -12,25 +12,29 @@ import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft'
1212import type { Transaction } from '@src/types/onyx' ;
1313import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue' ;
1414
15- // When the component mounts, if there are odometer images or a stitched receipt, see if the files can be read from the disk.
16- // If not, redirect the user to the starting step of the flow.
17- // This is because until the request is saved, the image files are only stored in the browser's memory as blob:// URLs
18- // and if the browser is refreshed, then the images cease to exist.
19- // The best way for the user to recover from this is to start over from the start of the request process.
20- // Returns `hasVerifiedBlobs` so callers can gate dependent side-effects (e.g. odometer image stitching)
21- // until this check has confirmed the blobs are still readable. When there are no blob URLs to verify
22- // (e.g. native file:// paths or remote URLs), `hasVerifiedBlobs` is `true` as soon as Onyx has loaded.
15+ type BackupHandledArgs = {
16+ shouldResetLocalState : boolean ;
17+ } ;
18+
2319const useRestartOnOdometerImagesFailure = (
2420 transaction : OnyxEntry < Transaction > ,
2521 reportID : string ,
2622 iouType : IOUType ,
2723 backToReport : string | undefined ,
28- onBackupHandled ?: ( ) => void ,
24+ onBackupHandled ?: ( args : BackupHandledArgs ) => void ,
2925) : { hasVerifiedBlobs : boolean } => {
3026 const [ , draftTransactionsMetadata ] = useOnyx ( ONYXKEYS . COLLECTION . TRANSACTION_DRAFT , { selector : validTransactionDraftIDsSelector } ) ;
27+ const [ odometerDraft , odometerDraftStatus ] = useOnyx ( ONYXKEYS . ODOMETER_DRAFT ) ;
3128 const hasCheckedRef = useRef ( false ) ;
3229 const [ asyncVerificationPassed , setAsyncVerificationPassed ] = useState ( false ) ;
3330
31+ // Updated via useEffect (not render-time) for React Compiler. The one-render lag is benign:
32+ // a missed bail just lets recovery fire, and recovery itself rehydrates correctly.
33+ const transactionRef = useRef ( transaction ) ;
34+ useEffect ( ( ) => {
35+ transactionRef . current = transaction ;
36+ } , [ transaction ] ) ;
37+
3438 const hasBlobUrls = ( ( ) => {
3539 if ( ! transaction ) {
3640 return false ;
@@ -40,22 +44,19 @@ const useRestartOnOdometerImagesFailure = (
4044 } ) ( ) ;
4145
4246 useEffect ( ( ) => {
43- if ( ! transaction || isLoadingOnyxValue ( draftTransactionsMetadata ) ) {
47+ if ( ! transaction || isLoadingOnyxValue ( draftTransactionsMetadata ) || isLoadingOnyxValue ( odometerDraftStatus ) ) {
4448 return ;
4549 }
4650
47- // Run only once after Onyx finishes loading — blob:// URLs are ephemeral and only need
48- // to be verified on the first render after the data is available.
49- // It has to be resolved this way in order to have a complete dependency array for the useEffect hook.
5051 if ( hasCheckedRef . current ) {
5152 return ;
5253 }
5354 hasCheckedRef . current = true ;
5455
5556 const startImage = transaction . comment ?. odometerStartImage ;
5657 const endImage = transaction . comment ?. odometerEndImage ;
57- const stitchedUri = transaction . receipt ?. source ?. toString ( ) ;
5858
59+ // Source images only — the stitched receipt URL is derived and OdometerReceiptStitcher regenerates it.
5960 const urlsToCheck = [
6061 {
6162 filename : typeof startImage === 'object' ? startImage ?. name : undefined ,
@@ -67,17 +68,10 @@ const useRestartOnOdometerImagesFailure = (
6768 path : getOdometerImageUri ( endImage ) ,
6869 type : typeof endImage === 'object' ? endImage ?. type : undefined ,
6970 } ,
70- {
71- filename : transaction . receipt ?. filename ,
72- path : stitchedUri ,
73- type : undefined ,
74- } ,
7571 ] . filter ( ( { path} ) => ! ! path && path . startsWith ( 'blob:' ) ) ;
7672
77- if ( urlsToCheck . length === 0 ) {
78- return ;
79- }
80-
73+ // Empty urlsToCheck flows through Promise.all -> [].every(Boolean) === true -> marks verification passed
74+ // Later blob additions (draft hydration, fresh capture) are minted in this session and stay trusted
8175 Promise . all (
8276 urlsToCheck . map (
8377 ( { filename, path, type} ) =>
@@ -98,13 +92,29 @@ const useRestartOnOdometerImagesFailure = (
9892 return ;
9993 }
10094
101- onBackupHandled ?.( ) ;
102- clearOdometerDraftTransactionState ( transaction ) ;
95+ // Bail if another flow already replaced the images (ODOMETER_DRAFT rehydration / fresh capture).
96+ const liveStartUri = getOdometerImageUri ( transactionRef . current ?. comment ?. odometerStartImage ) ;
97+ const liveEndUri = getOdometerImageUri ( transactionRef . current ?. comment ?. odometerEndImage ) ;
98+ if ( liveStartUri !== getOdometerImageUri ( startImage ) || liveEndUri !== getOdometerImageUri ( endImage ) ) {
99+ setAsyncVerificationPassed ( true ) ;
100+ return ;
101+ }
102+
103+ // Rehydrate over the dead URLs when a draft exists — clearing first races the destination's
104+ // auto-hydrator and ends up dropping the wrong URL.
105+ if ( odometerDraft ) {
106+ onBackupHandled ?.( { shouldResetLocalState : false } ) ;
107+ hydrateOdometerDraftIntoTransaction ( transaction . transactionID , odometerDraft , transaction . comment ) ;
108+ } else {
109+ onBackupHandled ?.( { shouldResetLocalState : true } ) ;
110+ clearOdometerDraftTransactionState ( transaction ) ;
111+ }
112+
103113 navigateToStartMoneyRequestStep ( CONST . IOU . REQUEST_TYPE . DISTANCE_ODOMETER , iouType , transaction . transactionID , reportID , CONST . IOU . ACTION . CREATE , backToReport ) ;
104114 } ) ;
105- } , [ draftTransactionsMetadata , transaction , iouType , reportID , backToReport , onBackupHandled ] ) ;
115+ } , [ draftTransactionsMetadata , transaction , iouType , reportID , backToReport , onBackupHandled , odometerDraft , odometerDraftStatus ] ) ;
106116
107- const isOnyxLoading = isLoadingOnyxValue ( draftTransactionsMetadata ) ;
117+ const isOnyxLoading = isLoadingOnyxValue ( draftTransactionsMetadata , odometerDraftStatus ) ;
108118 const hasVerifiedBlobs = ! ! transaction && ! isOnyxLoading && ( ! hasBlobUrls || asyncVerificationPassed ) ;
109119
110120 return { hasVerifiedBlobs} ;
0 commit comments