Skip to content

Commit 8e3971d

Browse files
mountinyOSBotify
authored andcommitted
Merge pull request #88095 from TaduJR/fix-Add-updated-automatic/scan-flow-to-native-share-sheet-creation-flow
[CP Staging] fix: share sheet upload edited receipt and keep Category after upgrade (cherry picked from commit a23ae1b) (cherry-picked to staging by mountiny)
1 parent 1be3831 commit 8e3971d

4 files changed

Lines changed: 142 additions & 61 deletions

File tree

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ function MoneyRequestConfirmationList({
323323

324324
// A flag for showing the categories field
325325
const shouldShowCategories = isTrackExpense
326-
? !policy || shouldSelectPolicy || hasEnabledOptions(Object.values(policyCategories ?? {}))
326+
? !policy || shouldSelectPolicy || !!iouCategory || hasEnabledOptions(Object.values(policyCategories ?? {}))
327327
: (isPolicyExpenseChat || isTypeInvoice) && (!!iouCategory || hasEnabledOptions(Object.values(policyCategories ?? {})));
328328

329329
const shouldShowMerchant = (shouldShowSmartScanFields || isTypeSend) && !isDistanceRequest && !isPerDiemRequest && (!isTimeRequest || action !== CONST.IOU.ACTION.CREATE);

src/pages/Share/ShareDetailsPage.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ import type SCREENS from '@src/SCREENS';
3737
import type {Report as ReportType} from '@src/types/onyx';
3838
import {isEmptyObject} from '@src/types/utils/EmptyObject';
3939
import KeyboardUtils from '@src/utils/keyboard';
40-
import getFileSize from './getFileSize';
4140
import ShareButton from './ShareButton';
4241
import {showErrorAlert} from './ShareRootPage';
42+
import useShareFileSizeValidation from './useShareFileSizeValidation';
4343

4444
type ShareDetailsPageProps = StackScreenProps<ShareNavigatorParamList, typeof SCREENS.SHARE.SHARE_DETAILS>;
4545

@@ -97,22 +97,7 @@ function ShareDetailsPage({route}: ShareDetailsPageProps) {
9797
Navigation.navigate(ROUTES.SHARE_DETAILS_ATTACHMENT);
9898
}, [reportAttachmentsContext, fileSource, validateFileName, icons.FallbackAvatar]);
9999

100-
useEffect(() => {
101-
if (!currentAttachment?.content || errorTitle || !shouldShowAttachment) {
102-
return;
103-
}
104-
getFileSize(currentAttachment?.content).then((size) => {
105-
if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
106-
setErrorTitle(translate('attachmentPicker.attachmentTooLarge'));
107-
setErrorMessage(translate('attachmentPicker.sizeExceeded'));
108-
}
109-
110-
if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
111-
setErrorTitle(translate('attachmentPicker.attachmentTooSmall'));
112-
setErrorMessage(translate('attachmentPicker.sizeNotMet'));
113-
}
114-
});
115-
}, [currentAttachment?.content, errorTitle, translate, shouldShowAttachment]);
100+
useShareFileSizeValidation(currentAttachment?.content, setErrorTitle, setErrorMessage, !errorTitle && shouldShowAttachment);
116101

117102
useEffect(() => {
118103
if (!errorTitle || !errorMessage) {

src/pages/Share/SubmitDetailsPage.tsx

Lines changed: 101 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {StackScreenProps} from '@react-navigation/stack';
22
import {hasSeenTourSelector} from '@selectors/Onboarding';
33
import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
4-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
4+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
55
import {View} from 'react-native';
66
import type {OnyxEntry} from 'react-native-onyx';
77
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
@@ -15,6 +15,7 @@ import useNetwork from '@hooks/useNetwork';
1515
import useOnyx from '@hooks/useOnyx';
1616
import usePermissions from '@hooks/usePermissions';
1717
import usePersonalPolicy from '@hooks/usePersonalPolicy';
18+
import usePolicyForTransaction from '@hooks/usePolicyForTransaction';
1819
import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap';
1920
import useReportAttributes from '@hooks/useReportAttributes';
2021
import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -51,6 +52,7 @@ import type {Report as ReportType} from '@src/types/onyx';
5152
import type {Participant} from '@src/types/onyx/IOU';
5253
import type {Receipt} from '@src/types/onyx/Transaction';
5354
import {showErrorAlert} from './ShareRootPage';
55+
import useShareFileSizeValidation from './useShareFileSizeValidation';
5456

5557
type ShareDetailsPageProps = StackScreenProps<ShareNavigatorParamList, typeof SCREENS.SHARE.SUBMIT_DETAILS>;
5658
function 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
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {useEffect} from 'react';
2+
import type {Dispatch, SetStateAction} from 'react';
3+
import useLocalize from '@hooks/useLocalize';
4+
import CONST from '@src/CONST';
5+
import getFileSize from './getFileSize';
6+
7+
type SetState = Dispatch<SetStateAction<string | undefined>>;
8+
9+
/** Validate the shared file against API min/max size limits. Pass `enabled: false` to skip (e.g., when an earlier error already took precedence). */
10+
function useShareFileSizeValidation(content: string | undefined, setErrorTitle: SetState, setErrorMessage: SetState, enabled = true) {
11+
const {translate} = useLocalize();
12+
13+
useEffect(() => {
14+
if (!content || !enabled) {
15+
return;
16+
}
17+
let ignore = false;
18+
getFileSize(content).then((size) => {
19+
if (ignore) {
20+
return;
21+
}
22+
if (size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
23+
setErrorTitle(translate('attachmentPicker.attachmentTooLarge'));
24+
setErrorMessage(translate('attachmentPicker.sizeExceeded'));
25+
}
26+
27+
if (size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
28+
setErrorTitle(translate('attachmentPicker.attachmentTooSmall'));
29+
setErrorMessage(translate('attachmentPicker.sizeNotMet'));
30+
}
31+
});
32+
return () => {
33+
ignore = true;
34+
};
35+
}, [content, enabled, setErrorTitle, setErrorMessage, translate]);
36+
}
37+
38+
export default useShareFileSizeValidation;

0 commit comments

Comments
 (0)