11import type { StackScreenProps } from '@react-navigation/stack' ;
22import { hasSeenTourSelector } from '@selectors/Onboarding' ;
33import { validTransactionDraftsSelector } from '@selectors/TransactionDraft' ;
4- import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
4+ import React , { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
55import { View } from 'react-native' ;
66import type { OnyxEntry } from 'react-native-onyx' ;
77import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView' ;
@@ -15,6 +15,7 @@ import useNetwork from '@hooks/useNetwork';
1515import useOnyx from '@hooks/useOnyx' ;
1616import usePermissions from '@hooks/usePermissions' ;
1717import usePersonalPolicy from '@hooks/usePersonalPolicy' ;
18+ import usePolicyForTransaction from '@hooks/usePolicyForTransaction' ;
1819import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap' ;
1920import useReportAttributes from '@hooks/useReportAttributes' ;
2021import useReportIsArchived from '@hooks/useReportIsArchived' ;
@@ -51,6 +52,7 @@ import type {Report as ReportType} from '@src/types/onyx';
5152import type { Participant } from '@src/types/onyx/IOU' ;
5253import type { Receipt } from '@src/types/onyx/Transaction' ;
5354import { showErrorAlert } from './ShareRootPage' ;
55+ import useShareFileSizeValidation from './useShareFileSizeValidation' ;
5456
5557type ShareDetailsPageProps = StackScreenProps < ShareNavigatorParamList , typeof SCREENS . SHARE . SUBMIT_DETAILS > ;
5658function SubmitDetailsPage ( {
@@ -64,8 +66,16 @@ function SubmitDetailsPage({
6466 const [ personalDetails ] = useOnyx ( `${ ONYXKEYS . PERSONAL_DETAILS_LIST } ` ) ;
6567 const report : OnyxEntry < ReportType > = getReportOrDraftReport ( reportOrAccountID ) ;
6668 const [ parentReport ] = useOnyx ( `${ ONYXKEYS . COLLECTION . REPORT } ${ report ?. parentReportID } ` ) ;
67- const [ policy ] = useOnyx ( `${ ONYXKEYS . COLLECTION . POLICY } ${ report ?. policyID } ` ) ;
6869 const [ transaction ] = useOnyx ( `${ ONYXKEYS . COLLECTION . TRANSACTION_DRAFT } ${ CONST . IOU . OPTIMISTIC_TRANSACTION_ID } ` ) ;
70+ const iouType = isSelfDM ( report ) ? CONST . IOU . TYPE . TRACK : CONST . IOU . TYPE . SUBMIT ;
71+ // Self-DM has a FAKE report policyID — usePolicyForTransaction (same hook MoneyRequestConfirmationList uses) returns the active workspace for self-DM track expense, covering the upgrade-from-free flow.
72+ const { policy} = usePolicyForTransaction ( {
73+ transaction,
74+ reportPolicyID : getIOURequestPolicyID ( transaction , report ) ,
75+ action : CONST . IOU . ACTION . CREATE ,
76+ iouType,
77+ isPerDiemRequest : false ,
78+ } ) ;
6979 const [ policyCategories ] = useOnyx ( `${ ONYXKEYS . COLLECTION . POLICY_CATEGORIES } ${ getIOURequestPolicyID ( transaction , report ) } ` ) ;
7080 const [ policyTags ] = useOnyx ( `${ ONYXKEYS . COLLECTION . POLICY_TAGS } ${ getIOURequestPolicyID ( transaction , report ) } ` ) ;
7181 const [ lastLocationPermissionPrompt ] = useOnyx ( ONYXKEYS . NVP_LAST_LOCATION_PERMISSION_PROMPT ) ;
@@ -92,6 +102,10 @@ function SubmitDetailsPage({
92102 const currentUserPersonalDetails = useCurrentUserPersonalDetails ( ) ;
93103 const personalPolicy = usePersonalPolicy ( ) ;
94104 const [ startLocationPermissionFlow , setStartLocationPermissionFlow ] = useState ( false ) ;
105+ const [ selectedParticipantList , setSelectedParticipantList ] = useState < Participant [ ] > ( [ ] ) ;
106+ const [ isConfirming , setIsConfirming ] = useState ( false ) ;
107+ const formHasBeenSubmitted = useRef ( false ) ;
108+ const [ userLocation ] = useOnyx ( ONYXKEYS . USER_LOCATION ) ;
95109
96110 const [ errorTitle , setErrorTitle ] = useState < string | undefined > ( undefined ) ;
97111 const [ errorMessage , setErrorMessage ] = useState < string | undefined > ( undefined ) ;
@@ -130,18 +144,26 @@ function SubmitDetailsPage({
130144 // eslint-disable-next-line react-hooks/exhaustive-deps
131145 } , [ reportOrAccountID , policy , personalPolicy , report , parentReport , currentDate , currentUserPersonalDetails , hasOnlyPersonalPolicies ] ) ;
132146
133- // Set receipt on the transaction draft so isScanRequest() returns true and
134- // compact mode, "Automatic" labels, and receipt image rendering all work correctly
135- const receiptSource = currentAttachment ?. content ?? fileUri ;
136- const receiptFileName = getFileName ( currentAttachment ?. content ?? '' ) || fileName ;
137- const receiptFileType = currentAttachment ?. mimeType ?? fileType ;
147+ const sharedFileSource = currentAttachment ?. content ?? fileUri ;
148+ const sharedFileName = getFileName ( currentAttachment ?. content ?? '' ) || fileName ;
149+ const sharedFileType = currentAttachment ?. mimeType ?? fileType ;
138150
151+ // Seed the transaction draft so isScanRequest() returns true and compact mode / "Automatic" labels / receipt rendering work.
139152 useEffect ( ( ) => {
140- if ( ! receiptSource ) {
153+ if ( ! sharedFileSource ) {
141154 return ;
142155 }
143- setMoneyRequestReceipt ( CONST . IOU . OPTIMISTIC_TRANSACTION_ID , receiptSource , receiptFileName , true , receiptFileType ) ;
144- } , [ receiptSource , receiptFileName , receiptFileType ] ) ;
156+ setMoneyRequestReceipt ( CONST . IOU . OPTIMISTIC_TRANSACTION_ID , sharedFileSource , sharedFileName , true , sharedFileType ) ;
157+ } , [ sharedFileSource , sharedFileName , sharedFileType ] ) ;
158+
159+ // The current receipt — prefers the transaction draft (reflects Replace/Crop), falls back to the shared file; used for both display and upload so they stay in sync.
160+ const currentReceiptSource = typeof transaction ?. receipt ?. source === 'string' ? transaction . receipt . source : sharedFileSource ;
161+ // Strip filesystem path segments without URL-decoding — getFileName() decodes via decodeURIComponent and would throw on raw filenames containing a literal '%' (e.g., "Receipt 100%.jpg").
162+ const currentReceiptName = ( transaction ?. receipt ?. filename ?. split ( '/' ) . pop ( ) ?? '' ) || sharedFileName ;
163+ const currentReceiptType = transaction ?. receipt ?. type ?? sharedFileType ;
164+
165+ // Validate the same source that performUpload reads — so Replace/Crop to an oversized file is still caught before submit.
166+ useShareFileSizeValidation ( currentReceiptSource , setErrorTitle , setErrorMessage , ! errorTitle ) ;
145167
146168 const selectedParticipants = unknownUserDetails ? [ unknownUserDetails ] : getMoneyRequestParticipantsFromReport ( report , currentUserPersonalDetails . accountID ) ;
147169 const participants = selectedParticipants . map ( ( participant ) => {
@@ -154,7 +176,6 @@ function SubmitDetailsPage({
154176 const isPolicyExpenseChat = useMemo ( ( ) => participants ?. some ( ( participant ) => participant . isPolicyExpenseChat ) , [ participants ] ) ;
155177 const policyExpenseChatPolicyID = participants ?. find ( ( participant ) => participant . isPolicyExpenseChat ) ?. policyID ;
156178 const senderPolicyID = participants ?. find ( ( participant ) => ! ! participant && 'isSender' in participant && participant . isSender ) ?. policyID ;
157- const iouType = isSelfDM ( report ) ? CONST . IOU . TYPE . TRACK : CONST . IOU . TYPE . SUBMIT ;
158179 const { isOffline} = useNetwork ( ) ;
159180 const isCreatingTrackExpense = iouType === CONST . IOU . TYPE . TRACK ;
160181
@@ -277,51 +298,72 @@ function SubmitDetailsPage({
277298 const onSuccess = ( participant : Participant , file : File , locationPermissionGranted ?: boolean ) => {
278299 const receipt : Receipt = file ;
279300 receipt . state = file && CONST . IOU . RECEIPT_STATE . SCAN_READY ;
280- if ( locationPermissionGranted ) {
281- getCurrentPosition (
282- ( successData ) => {
283- finishRequestAndNavigate ( participant , receipt , {
284- lat : successData . coords . latitude ,
285- long : successData . coords . longitude ,
286- } ) ;
287- } ,
288- ( errorData ) => {
289- Log . info ( '[SubmitDetailsPage] getCurrentPosition failed' , false , errorData ) ;
290- finishRequestAndNavigate ( participant , receipt ) ;
291- } ,
292- ) ;
301+ if ( ! locationPermissionGranted ) {
302+ finishRequestAndNavigate ( participant , receipt ) ;
303+ return ;
304+ }
305+ // Use cached userLocation when available — avoids an extra getCurrentPosition round-trip.
306+ if ( userLocation ) {
307+ finishRequestAndNavigate ( participant , receipt , {
308+ lat : userLocation . latitude ,
309+ long : userLocation . longitude ,
310+ } ) ;
311+ return ;
312+ }
313+ getCurrentPosition (
314+ ( successData ) => {
315+ finishRequestAndNavigate ( participant , receipt , {
316+ lat : successData . coords . latitude ,
317+ long : successData . coords . longitude ,
318+ } ) ;
319+ } ,
320+ ( errorData ) => {
321+ Log . info ( '[SubmitDetailsPage] getCurrentPosition failed' , false , errorData ) ;
322+ finishRequestAndNavigate ( participant , receipt ) ;
323+ } ,
324+ ) ;
325+ } ;
326+
327+ // Extracted from onConfirm — re-entering onConfirm from the permission modal deadlocked when OS permission was pre-granted.
328+ const performUpload = ( participant : Participant , locationPermissionGranted : boolean ) => {
329+ if ( formHasBeenSubmitted . current || ! currentAttachment ) {
330+ setIsConfirming ( false ) ;
293331 return ;
294332 }
295- finishRequestAndNavigate ( participant , receipt ) ;
333+ formHasBeenSubmitted . current = true ;
334+ readFileAsync (
335+ currentReceiptSource ,
336+ currentReceiptName ,
337+ ( file ) => onSuccess ( participant , file , locationPermissionGranted ) ,
338+ ( ) => {
339+ // Allow retry after a file-read failure.
340+ formHasBeenSubmitted . current = false ;
341+ setIsConfirming ( false ) ;
342+ } ,
343+ currentReceiptType ,
344+ ) ;
296345 } ;
297346
298347 const onConfirm = ( listOfParticipants ?: Participant [ ] , gpsRequired ?: boolean ) => {
348+ setIsConfirming ( true ) ;
299349 const shouldStartLocationPermissionFlow =
300350 gpsRequired &&
301351 ( ! lastLocationPermissionPrompt ||
302352 ( DateUtils . isValidDateString ( lastLocationPermissionPrompt ?? '' ) &&
303353 DateUtils . getDifferenceInDaysFromNow ( new Date ( lastLocationPermissionPrompt ?? '' ) ) > CONST . IOU . LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS ) ) ;
304354
305355 if ( shouldStartLocationPermissionFlow ) {
356+ setSelectedParticipantList ( listOfParticipants ?? selectedParticipants ) ;
306357 setStartLocationPermissionFlow ( true ) ;
307358 return ;
308359 }
309- if ( ! currentAttachment ) {
310- return ;
311- }
312360
313361 const participant = listOfParticipants ?. at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
314362 if ( ! participant ) {
363+ setIsConfirming ( false ) ;
315364 return ;
316365 }
317-
318- readFileAsync (
319- receiptSource ,
320- receiptFileName ,
321- ( file ) => onSuccess ( participant , file , shouldStartLocationPermissionFlow ) ,
322- ( ) => { } ,
323- receiptFileType ,
324- ) ;
366+ performUpload ( participant , false ) ;
325367 } ;
326368
327369 return (
@@ -341,15 +383,30 @@ function SubmitDetailsPage({
341383 />
342384 < LocationPermissionModal
343385 startPermissionFlow = { startLocationPermissionFlow }
344- resetPermissionFlow = { ( ) => setStartLocationPermissionFlow ( false ) }
345- onGrant = { ( ) => onConfirm ( undefined , true ) }
386+ resetPermissionFlow = { ( ) => {
387+ setStartLocationPermissionFlow ( false ) ;
388+ setIsConfirming ( false ) ;
389+ } }
390+ onGrant = { ( ) => {
391+ setStartLocationPermissionFlow ( false ) ;
392+ const participant = selectedParticipantList . at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
393+ if ( ! participant ) {
394+ setIsConfirming ( false ) ;
395+ return ;
396+ }
397+ navigateAfterInteraction ( ( ) => performUpload ( participant , true ) ) ;
398+ } }
346399 onDeny = { ( ) => {
347400 updateLastLocationPermissionPrompt ( ) ;
348401 setStartLocationPermissionFlow ( false ) ;
349- navigateAfterInteraction ( ( ) => {
350- onConfirm ( undefined , false ) ;
351- } ) ;
402+ const participant = selectedParticipantList . at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
403+ if ( ! participant ) {
404+ setIsConfirming ( false ) ;
405+ return ;
406+ }
407+ navigateAfterInteraction ( ( ) => performUpload ( participant , false ) ) ;
352408 } }
409+ onInitialGetLocationCompleted = { ( ) => setIsConfirming ( false ) }
353410 />
354411 < View style = { [ styles . containerWithSpaceBetween , styles . pointerEventsBoxNone ] } >
355412 < MoneyRequestConfirmationList
@@ -360,9 +417,10 @@ function SubmitDetailsPage({
360417 onToggleReimbursable = { setReimbursable }
361418 isPolicyExpenseChat = { isPolicyExpenseChat }
362419 policyID = { policy ?. id }
420+ isConfirming = { isConfirming }
363421 onConfirm = { ( updatedParticipants ) => onConfirm ( updatedParticipants , true ) }
364- receiptPath = { receiptSource }
365- receiptFilename = { receiptFileName }
422+ receiptPath = { currentReceiptSource }
423+ receiptFilename = { currentReceiptName }
366424 reportID = { reportOrAccountID }
367425 shouldShowSmartScanFields = { false }
368426 shouldDisplayReceipt
0 commit comments