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 ) ;
@@ -128,18 +142,26 @@ function SubmitDetailsPage({
128142 // eslint-disable-next-line react-hooks/exhaustive-deps
129143 } , [ reportOrAccountID , policy , personalPolicy , report , parentReport , currentDate , currentUserPersonalDetails , hasOnlyPersonalPolicies ] ) ;
130144
131- // Set receipt on the transaction draft so isScanRequest() returns true and
132- // compact mode, "Automatic" labels, and receipt image rendering all work correctly
133- const receiptSource = currentAttachment ?. content ?? fileUri ;
134- const receiptFileName = getFileName ( currentAttachment ?. content ?? '' ) || fileName ;
135- const receiptFileType = currentAttachment ?. mimeType ?? fileType ;
145+ const sharedFileSource = currentAttachment ?. content ?? fileUri ;
146+ const sharedFileName = getFileName ( currentAttachment ?. content ?? '' ) || fileName ;
147+ const sharedFileType = currentAttachment ?. mimeType ?? fileType ;
136148
149+ // Seed the transaction draft so isScanRequest() returns true and compact mode / "Automatic" labels / receipt rendering work.
137150 useEffect ( ( ) => {
138- if ( ! receiptSource ) {
151+ if ( ! sharedFileSource ) {
139152 return ;
140153 }
141- setMoneyRequestReceipt ( CONST . IOU . OPTIMISTIC_TRANSACTION_ID , receiptSource , receiptFileName , true , receiptFileType ) ;
142- } , [ receiptSource , receiptFileName , receiptFileType ] ) ;
154+ setMoneyRequestReceipt ( CONST . IOU . OPTIMISTIC_TRANSACTION_ID , sharedFileSource , sharedFileName , true , sharedFileType ) ;
155+ } , [ sharedFileSource , sharedFileName , sharedFileType ] ) ;
156+
157+ // 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.
158+ const currentReceiptSource = typeof transaction ?. receipt ?. source === 'string' ? transaction . receipt . source : sharedFileSource ;
159+ // 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").
160+ const currentReceiptName = ( transaction ?. receipt ?. filename ?. split ( '/' ) . pop ( ) ?? '' ) || sharedFileName ;
161+ const currentReceiptType = transaction ?. receipt ?. type ?? sharedFileType ;
162+
163+ // Validate the same source that performUpload reads — so Replace/Crop to an oversized file is still caught before submit.
164+ useShareFileSizeValidation ( currentReceiptSource , setErrorTitle , setErrorMessage , ! errorTitle ) ;
143165
144166 const selectedParticipants = unknownUserDetails ? [ unknownUserDetails ] : getMoneyRequestParticipantsFromReport ( report , currentUserPersonalDetails . accountID ) ;
145167 const participants = selectedParticipants . map ( ( participant ) => {
@@ -152,7 +174,6 @@ function SubmitDetailsPage({
152174 const isPolicyExpenseChat = useMemo ( ( ) => participants ?. some ( ( participant ) => participant . isPolicyExpenseChat ) , [ participants ] ) ;
153175 const policyExpenseChatPolicyID = participants ?. find ( ( participant ) => participant . isPolicyExpenseChat ) ?. policyID ;
154176 const senderPolicyID = participants ?. find ( ( participant ) => ! ! participant && 'isSender' in participant && participant . isSender ) ?. policyID ;
155- const iouType = isSelfDM ( report ) ? CONST . IOU . TYPE . TRACK : CONST . IOU . TYPE . SUBMIT ;
156177 const { isOffline} = useNetwork ( ) ;
157178 const isCreatingTrackExpense = iouType === CONST . IOU . TYPE . TRACK ;
158179
@@ -275,51 +296,72 @@ function SubmitDetailsPage({
275296 const onSuccess = ( participant : Participant , file : File , locationPermissionGranted ?: boolean ) => {
276297 const receipt : Receipt = file ;
277298 receipt . state = file && CONST . IOU . RECEIPT_STATE . SCAN_READY ;
278- if ( locationPermissionGranted ) {
279- getCurrentPosition (
280- ( successData ) => {
281- finishRequestAndNavigate ( participant , receipt , {
282- lat : successData . coords . latitude ,
283- long : successData . coords . longitude ,
284- } ) ;
285- } ,
286- ( errorData ) => {
287- Log . info ( '[SubmitDetailsPage] getCurrentPosition failed' , false , errorData ) ;
288- finishRequestAndNavigate ( participant , receipt ) ;
289- } ,
290- ) ;
299+ if ( ! locationPermissionGranted ) {
300+ finishRequestAndNavigate ( participant , receipt ) ;
301+ return ;
302+ }
303+ // Use cached userLocation when available — avoids an extra getCurrentPosition round-trip.
304+ if ( userLocation ) {
305+ finishRequestAndNavigate ( participant , receipt , {
306+ lat : userLocation . latitude ,
307+ long : userLocation . longitude ,
308+ } ) ;
309+ return ;
310+ }
311+ getCurrentPosition (
312+ ( successData ) => {
313+ finishRequestAndNavigate ( participant , receipt , {
314+ lat : successData . coords . latitude ,
315+ long : successData . coords . longitude ,
316+ } ) ;
317+ } ,
318+ ( errorData ) => {
319+ Log . info ( '[SubmitDetailsPage] getCurrentPosition failed' , false , errorData ) ;
320+ finishRequestAndNavigate ( participant , receipt ) ;
321+ } ,
322+ ) ;
323+ } ;
324+
325+ // Extracted from onConfirm — re-entering onConfirm from the permission modal deadlocked when OS permission was pre-granted.
326+ const performUpload = ( participant : Participant , locationPermissionGranted : boolean ) => {
327+ if ( formHasBeenSubmitted . current || ! currentAttachment ) {
328+ setIsConfirming ( false ) ;
291329 return ;
292330 }
293- finishRequestAndNavigate ( participant , receipt ) ;
331+ formHasBeenSubmitted . current = true ;
332+ readFileAsync (
333+ currentReceiptSource ,
334+ currentReceiptName ,
335+ ( file ) => onSuccess ( participant , file , locationPermissionGranted ) ,
336+ ( ) => {
337+ // Allow retry after a file-read failure.
338+ formHasBeenSubmitted . current = false ;
339+ setIsConfirming ( false ) ;
340+ } ,
341+ currentReceiptType ,
342+ ) ;
294343 } ;
295344
296345 const onConfirm = ( listOfParticipants ?: Participant [ ] , gpsRequired ?: boolean ) => {
346+ setIsConfirming ( true ) ;
297347 const shouldStartLocationPermissionFlow =
298348 gpsRequired &&
299349 ( ! lastLocationPermissionPrompt ||
300350 ( DateUtils . isValidDateString ( lastLocationPermissionPrompt ?? '' ) &&
301351 DateUtils . getDifferenceInDaysFromNow ( new Date ( lastLocationPermissionPrompt ?? '' ) ) > CONST . IOU . LOCATION_PERMISSION_PROMPT_THRESHOLD_DAYS ) ) ;
302352
303353 if ( shouldStartLocationPermissionFlow ) {
354+ setSelectedParticipantList ( listOfParticipants ?? selectedParticipants ) ;
304355 setStartLocationPermissionFlow ( true ) ;
305356 return ;
306357 }
307- if ( ! currentAttachment ) {
308- return ;
309- }
310358
311359 const participant = listOfParticipants ?. at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
312360 if ( ! participant ) {
361+ setIsConfirming ( false ) ;
313362 return ;
314363 }
315-
316- readFileAsync (
317- receiptSource ,
318- receiptFileName ,
319- ( file ) => onSuccess ( participant , file , shouldStartLocationPermissionFlow ) ,
320- ( ) => { } ,
321- receiptFileType ,
322- ) ;
364+ performUpload ( participant , false ) ;
323365 } ;
324366
325367 return (
@@ -339,15 +381,30 @@ function SubmitDetailsPage({
339381 />
340382 < LocationPermissionModal
341383 startPermissionFlow = { startLocationPermissionFlow }
342- resetPermissionFlow = { ( ) => setStartLocationPermissionFlow ( false ) }
343- onGrant = { ( ) => onConfirm ( undefined , true ) }
384+ resetPermissionFlow = { ( ) => {
385+ setStartLocationPermissionFlow ( false ) ;
386+ setIsConfirming ( false ) ;
387+ } }
388+ onGrant = { ( ) => {
389+ setStartLocationPermissionFlow ( false ) ;
390+ const participant = selectedParticipantList . at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
391+ if ( ! participant ) {
392+ setIsConfirming ( false ) ;
393+ return ;
394+ }
395+ navigateAfterInteraction ( ( ) => performUpload ( participant , true ) ) ;
396+ } }
344397 onDeny = { ( ) => {
345398 updateLastLocationPermissionPrompt ( ) ;
346399 setStartLocationPermissionFlow ( false ) ;
347- navigateAfterInteraction ( ( ) => {
348- onConfirm ( undefined , false ) ;
349- } ) ;
400+ const participant = selectedParticipantList . at ( 0 ) ?? selectedParticipants . at ( 0 ) ;
401+ if ( ! participant ) {
402+ setIsConfirming ( false ) ;
403+ return ;
404+ }
405+ navigateAfterInteraction ( ( ) => performUpload ( participant , false ) ) ;
350406 } }
407+ onInitialGetLocationCompleted = { ( ) => setIsConfirming ( false ) }
351408 />
352409 < View style = { [ styles . containerWithSpaceBetween , styles . pointerEventsBoxNone ] } >
353410 < MoneyRequestConfirmationList
@@ -358,9 +415,10 @@ function SubmitDetailsPage({
358415 onToggleReimbursable = { setReimbursable }
359416 isPolicyExpenseChat = { isPolicyExpenseChat }
360417 policyID = { policy ?. id }
418+ isConfirming = { isConfirming }
361419 onConfirm = { ( updatedParticipants ) => onConfirm ( updatedParticipants , true ) }
362- receiptPath = { receiptSource }
363- receiptFilename = { receiptFileName }
420+ receiptPath = { currentReceiptSource }
421+ receiptFilename = { currentReceiptName }
364422 reportID = { reportOrAccountID }
365423 shouldShowSmartScanFields = { false }
366424 shouldDisplayReceipt
0 commit comments