diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7c9247bcdbd7..819680db0e8a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -344,8 +344,7 @@ const ONYXKEYS = { TRANSACTION: 'transactions_', TRANSACTION_VIOLATIONS: 'transactionViolations_', TRANSACTION_DRAFT: 'transactionsDraft_', - - // Holds temporary transactions used during the creation and edit flow + SKIP_CONFIRMATION: 'skipConfirmation_', TRANSACTION_BACKUP: 'transactionsBackup_', SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_', PRIVATE_NOTES_DRAFT: 'privateNotesDraft_', @@ -546,6 +545,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.SKIP_CONFIRMATION]: boolean; [ONYXKEYS.COLLECTION.TRANSACTION_BACKUP]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0c8306cd9d54..806832013a4d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -301,6 +301,7 @@ function getReportPreviewAction(chatReportID: string, iouReportID: string): Onyx * @param policy * @param isFromGlobalCreate * @param iouRequestType one of manual/scan/distance + * @param skipConfirmation if true, skip confirmation step */ function initMoneyRequest(reportID: string, policy: OnyxEntry, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { // Generate a brand new transactionID @@ -351,7 +352,8 @@ function createDraftTransaction(transaction: OnyxTypes.Transaction) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); } -function clearMoneyRequest(transactionID: string) { +function clearMoneyRequest(transactionID: string, skipConfirmation = false) { + Onyx.set(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, skipConfirmation); Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null); } @@ -378,8 +380,8 @@ function updateMoneyRequestTypeParams(routes: StackNavigationState, reportID: string, requestType?: IOURequestType, skipConfirmation = false) { + clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID, skipConfirmation); switch (requestType) { case CONST.IOU.REQUEST_TYPE.MANUAL: Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID)); @@ -1720,9 +1722,9 @@ function createDistanceRequest( merchant: string, billable: boolean | undefined, validWaypoints: WaypointCollection, - policy: OnyxEntry, - policyTagList: OnyxEntry, - policyCategories: OnyxEntry, + policy?: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, ) { // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -5799,7 +5801,7 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry): Participant[] { // If the report is iou or expense report, we should get the chat report to set participant for expense const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; @@ -5810,6 +5812,8 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); + + return participants; } function setMoneyRequestId(id: string) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index f74b451c29f6..fa0d04343321 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -89,8 +89,12 @@ Onyx.connect({ /** * Clears out the task info from the store */ -function clearOutTaskInfo() { - Onyx.set(ONYXKEYS.TASK, null); +function clearOutTaskInfo(skipConfirmation = false) { + if (skipConfirmation) { + Onyx.set(ONYXKEYS.TASK, {skipConfirmation: true}); + } else { + Onyx.set(ONYXKEYS.TASK, null); + } } /** @@ -724,8 +728,8 @@ function setParentReportID(parentReportID: string) { /** * Clears out the task info from the store and navigates to the NewTaskDetails page */ -function clearOutTaskInfoAndNavigate(reportID?: string, chatReport?: OnyxEntry, accountID = 0) { - clearOutTaskInfo(); +function clearOutTaskInfoAndNavigate(reportID?: string, chatReport?: OnyxEntry, accountID = 0, skipConfirmation = false) { + clearOutTaskInfo(skipConfirmation); if (reportID && reportID !== '0') { setParentReportID(reportID); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index fe73d1dce7e0..60fed8e7af2e 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -169,28 +169,28 @@ function FloatingActionButtonAndPopover( const navigateToQuickAction = () => { switch (quickAction?.action) { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); + return; case CONST.QUICK_ACTIONS.REQUEST_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN, true); + return; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); + return; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); + return; case CONST.QUICK_ACTIONS.SPLIT_SCAN: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.SCAN, true); + return; case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: - IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.DISTANCE, true); + return; case CONST.QUICK_ACTIONS.SEND_MONEY: - IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, quickAction?.chatReportID ?? ''); - break; + IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL, true); + return; case CONST.QUICK_ACTIONS.ASSIGN_TASK: - Task.clearOutTaskInfoAndNavigate(quickAction?.chatReportID, quickActionReport, quickAction.targetAccountID ?? 0); + Task.clearOutTaskInfoAndNavigate(quickAction?.chatReportID ?? '', quickActionReport, quickAction.targetAccountID ?? 0, true); break; case CONST.QUICK_ACTIONS.TRACK_MANUAL: IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, quickAction?.chatReportID ?? '', CONST.IOU.REQUEST_TYPE.MANUAL); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index cb8e51120f01..acdd160ac013 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -1,12 +1,15 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import * as TransactionEdit from '@libs/actions/TransactionEdit'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import {getRequestType} from '@libs/TransactionUtils'; @@ -16,7 +19,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Transaction} from '@src/types/onyx'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -25,20 +29,31 @@ import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type AmountParams = { amount: string; + paymentMethod?: PaymentMethodType; }; type IOURequestStepAmountOnyxProps = { /** The draft transaction that holds data to be persisted on the current transaction */ - splitDraftTransaction: OnyxEntry; + splitDraftTransaction: OnyxEntry; + + /** Whether the confirmation step should be skipped */ + skipConfirmation: OnyxEntry; /** The draft transaction object being modified in Onyx */ - draftTransaction: OnyxEntry; + draftTransaction: OnyxEntry; + + /** Personal details of all users */ + personalDetails: OnyxEntry; + + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; }; type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & + WithCurrentUserPersonalDetailsProps & WithWritableReportOrNotFoundProps & { /** The transaction object being modified in Onyx */ - transaction: OnyxEntry; + transaction: OnyxEntry; }; function IOURequestStepAmount({ @@ -47,14 +62,19 @@ function IOURequestStepAmount({ params: {iouType, reportID, transactionID, backTo, action, currency: selectedCurrency = ''}, }, transaction, + policy, + personalDetails, + currentUserPersonalDetails, splitDraftTransaction, draftTransaction, + skipConfirmation, }: IOURequestStepAmountProps) { const {translate} = useLocalize(); const textInput = useRef(null); const focusTimeoutRef = useRef(null); const isSaveButtonPressed = useRef(false); const iouRequestType = getRequestType(transaction); + const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const isEditingSplitBill = isEditing && isSplitBill; @@ -62,6 +82,16 @@ function IOURequestStepAmount({ const {currency: originalCurrency} = ReportUtils.getTransactionDetails(isEditing ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD}; const currency = CurrencyUtils.isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency; + // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace request, as + // the user will have to add a merchant. + const shouldSkipConfirmation: boolean = useMemo(() => { + if (!skipConfirmation || !report?.reportID || iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return false; + } + + return !(ReportUtils.isArchivedRoom(report) || ReportUtils.isPolicyExpenseChat(report)); + }, [report, skipConfirmation, iouType]); + useFocusEffect( useCallback(() => { focusTimeoutRef.current = setTimeout(() => textInput.current?.focus(), CONST.ANIMATED_TRANSITION); @@ -102,7 +132,7 @@ function IOURequestStepAmount({ ); }; - const navigateToNextPage = ({amount}: AmountParams) => { + const navigateToNextPage = ({amount, paymentMethod}: AmountParams) => { isSaveButtonPressed.current = true; const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); @@ -117,7 +147,57 @@ function IOURequestStepAmount({ // If a reportID exists in the report object, it's because the user started this flow from using the + button in the composer // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. - if (report?.reportID) { + if (report?.reportID && !ReportUtils.isArchivedRoom(report)) { + const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const participants = selectedParticipants.map((participant) => { + const participantAccountID = participant?.accountID ?? 0; + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }); + const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); + + if (shouldSkipConfirmation) { + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.splitBillAndOpenReport({ + participants, + currentUserLogin: currentUserPersonalDetails.login ?? '', + currentUserAccountID: currentUserPersonalDetails.accountID ?? 0, + amount: backendAmount, + comment: '', + currency, + merchant: '', + tag: '', + category: '', + created: transaction?.created ?? '', + billable: false, + iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + }); + return; + } + if (iouType === CONST.IOU.TYPE.SEND) { + if (paymentMethod && paymentMethod === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + IOU.sendMoneyWithWallet(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants[0]); + return; + } + + IOU.sendMoneyElsewhere(report, backendAmount, currency, '', currentUserPersonalDetails.accountID, participants[0]); + return; + } + if (iouType === CONST.IOU.TYPE.REQUEST) { + IOU.requestMoney( + report, + backendAmount, + currency, + transaction?.created ?? '', + '', + currentUserPersonalDetails.login ?? '', + currentUserPersonalDetails.accountID ?? 0, + participants[0], + '', + {}, + ); + return; + } + } IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; @@ -128,9 +208,9 @@ function IOURequestStepAmount({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); }; - const saveAmountAndCurrency = ({amount}: AmountParams) => { + const saveAmountAndCurrency = ({amount, paymentMethod}: AmountParams) => { if (!isEditing) { - navigateToNextPage({amount}); + navigateToNextPage({amount, paymentMethod}); return; } @@ -164,6 +244,10 @@ function IOURequestStepAmount({ isEditing={!!backTo || isEditing} currency={currency} amount={Math.abs(transactionAmount)} + skipConfirmation={shouldSkipConfirmation ?? false} + iouType={iouType} + policyID={policy?.id ?? ''} + bankAccountRoute={ReportUtils.getBankAccountRoute(report)} ref={(e) => (textInput.current = e)} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={saveAmountAndCurrency} @@ -175,21 +259,37 @@ function IOURequestStepAmount({ IOURequestStepAmount.displayName = 'IOURequestStepAmount'; -export default withWritableReportOrNotFound( - withFullTransactionOrNotFound( - withOnyx({ - splitDraftTransaction: { - key: ({route}) => { - const transactionID = route.params.transactionID ?? 0; - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; - }, - }, - draftTransaction: { - key: ({route}) => { - const transactionID = route.params.transactionID ?? 0; - return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`; - }, - }, - })(IOURequestStepAmount), - ), -); +const IOURequestStepAmountWithOnyx = withOnyx({ + splitDraftTransaction: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; + }, + }, + draftTransaction: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`; + }, + }, + skipConfirmation: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`; + }, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, +})(IOURequestStepAmount); + +const IOURequestStepAmountWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(IOURequestStepAmountWithOnyx); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepAmountWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepAmountWithCurrentUserPersonalDetails); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepAmountWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepAmountWithWritableReportOrNotFound); + +export default IOURequestStepAmountWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index cd26b9184dfb..0602c2184365 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -11,12 +11,16 @@ import DistanceRequestFooter from '@components/DistanceRequest/DistanceRequestFo import DistanceRequestRenderItem from '@components/DistanceRequest/DistanceRequestRenderItem'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import DraggableList from '@components/DraggableList'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import * as MapboxToken from '@userActions/MapboxToken'; @@ -37,9 +41,19 @@ import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type IOURequestStepDistanceOnyxProps = { /** backup version of the original transaction */ transactionBackup: OnyxEntry; + + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; + + /** Personal details of all users */ + personalDetails: OnyxEntry; + + /** Whether the confirmation step should be skipped */ + skipConfirmation: OnyxEntry; }; type IOURequestStepDistanceProps = IOURequestStepDistanceOnyxProps & + WithCurrentUserPersonalDetailsProps & WithWritableReportOrNotFoundProps & { /** The transaction object being modified in Onyx */ transaction: OnyxEntry; @@ -47,11 +61,15 @@ type IOURequestStepDistanceProps = IOURequestStepDistanceOnyxProps & function IOURequestStepDistance({ report, + policy, route: { params: {action, iouType, reportID, transactionID, backTo}, }, transaction, transactionBackup, + personalDetails, + currentUserPersonalDetails, + skipConfirmation, }: IOURequestStepDistanceProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -84,6 +102,20 @@ function IOURequestStepDistance({ const transactionWasSaved = useRef(false); const isCreatingNewRequest = !(backTo || isEditing); + // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace + // request and the workspace requires a category or a tag + const shouldSkipConfirmation: boolean = useMemo(() => { + if (!skipConfirmation || !report?.reportID || iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return false; + } + + return !ReportUtils.isArchivedRoom(report) && !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))); + }, [report, skipConfirmation, policy, iouType]); + let buttonText = !isCreatingNewRequest ? translate('common.save') : translate('common.next'); + if (shouldSkipConfirmation) { + buttonText = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.split') : translate('iou.submitExpense'); + } + useEffect(() => { MapboxToken.init(); return MapboxToken.stop; @@ -159,7 +191,47 @@ function IOURequestStepDistance({ // If a reportID exists in the report object, it's because the user started this flow from using the + button in the composer // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. - if (report?.reportID) { + if (report?.reportID && !ReportUtils.isArchivedRoom(report)) { + const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const participants = selectedParticipants.map((participant) => { + const participantAccountID = participant?.accountID ?? 0; + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }); + if (shouldSkipConfirmation) { + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.splitBillAndOpenReport({ + participants, + currentUserLogin: currentUserPersonalDetails.login ?? '', + currentUserAccountID: currentUserPersonalDetails.accountID ?? 0, + amount: 0, + comment: '', + currency: transaction?.currency ?? 'USD', + merchant: translate('iou.routePending'), + created: transaction?.created ?? '', + category: '', + tag: '', + billable: false, + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, + }); + return; + } + IOU.setMoneyRequestPendingFields(transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + IOU.setMoneyRequestMerchant(transactionID, translate('iou.routePending'), false); + IOU.createDistanceRequest( + report, + participants[0], + '', + transaction?.created ?? '', + '', + '', + 0, + transaction?.currency ?? 'USD', + translate('iou.routePending'), + false, + TransactionUtils.getValidWaypoints(waypoints, true), + ); + return; + } IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; @@ -168,7 +240,7 @@ function IOURequestStepDistance({ // If there was no reportID, then that means the user started this flow from the global menu // and an optimistic reportID was generated. In that case, the next step is to select the participants for this expense. Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); - }, [report, iouType, reportID, transactionID, backTo]); + }, [report, iouType, reportID, transactionID, backTo, waypoints, currentUserPersonalDetails, personalDetails, shouldSkipConfirmation, transaction, translate]); const getError = () => { // Get route error if available else show the invalid number of waypoints error. @@ -311,7 +383,7 @@ function IOURequestStepDistance({ large style={[styles.w100, styles.mb4, styles.ph4, styles.flexShrink0]} onPress={submitWaypoints} - text={translate(!isCreatingNewRequest ? 'common.save' : 'common.next')} + text={buttonText} isLoading={!isOffline && (isLoadingRoute || shouldFetchRoute || isLoading)} /> @@ -322,15 +394,31 @@ function IOURequestStepDistance({ IOURequestStepDistance.displayName = 'IOURequestStepDistance'; -export default withWritableReportOrNotFound( - withFullTransactionOrNotFound( - withOnyx({ - transactionBackup: { - key: ({route}) => { - const transactionID = route.params.transactionID ?? 0; - return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`; - }, - }, - })(IOURequestStepDistance), - ), -); +const IOURequestStepDistanceWithOnyx = withOnyx({ + transactionBackup: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`; + }, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report?.policyID : '0'}`, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + skipConfirmation: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`; + }, + }, +})(IOURequestStepDistance); + +const IOURequestStepDistanceWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(IOURequestStepDistanceWithOnyx); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepDistanceWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceWithCurrentUserPersonalDetails); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepDistanceWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceWithWritableReportOrNotFound); + +export default IOURequestStepDistanceWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index c1b360f89e48..84c2137dafda 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/core'; -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Alert, AppState, InteractionManager, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; @@ -17,12 +17,15 @@ import * as Expensicons from '@components/Icon/Expensicons'; import ImageSVG from '@components/ImageSVG'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; @@ -30,17 +33,22 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera'; import type {IOURequestStepOnyxProps, IOURequestStepScanProps} from './types'; function IOURequestStepScan({ report, + policy, user, route: { params: {action, iouType, reportID, transactionID, backTo}, }, transaction, + personalDetails, + currentUserPersonalDetails, + skipConfirmation, }: IOURequestStepScanProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -54,6 +62,16 @@ function IOURequestStepScan({ const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); + // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace + // request and the workspace requires a category or a tag + const shouldSkipConfirmation: boolean = useMemo(() => { + if (!skipConfirmation || !report?.reportID || iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return false; + } + + return !ReportUtils.isArchivedRoom(report) && !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))); + }, [report, skipConfirmation, policy, iouType]); + const {translate} = useLocalize(); const askForPermissions = () => { @@ -161,24 +179,64 @@ function IOURequestStepScan({ Navigation.goBack(); }; - const navigateToConfirmationStep = useCallback(() => { - if (backTo) { - Navigation.goBack(backTo); - return; - } + const navigateToConfirmationStep = useCallback( + (file: FileObject, source: string) => { + if (backTo) { + Navigation.goBack(backTo); + return; + } - // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); - return; - } + // If the transaction was created from the global create, the person needs to select participants, so take them there. + if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && !report?.reportID) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); + return; + } - // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically - // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically + // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. + const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const participants = selectedParticipants.map((participant) => { + const participantAccountID = participant?.accountID ?? 0; + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); - }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); + if (shouldSkipConfirmation) { + const receipt: Receipt = file; + receipt.source = source; + receipt.state = CONST.IOU.RECEIPT_STATE.SCANREADY; + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.startSplitBill({ + participants, + currentUserLogin: currentUserPersonalDetails?.login ?? '', + currentUserAccountID: currentUserPersonalDetails?.accountID ?? 0, + comment: '', + receipt, + existingSplitChatReportID: reportID ?? 0, + billable: false, + category: '', + tag: '', + currency: transaction?.currency ?? 'USD', + }); + return; + } + IOU.requestMoney( + report, + 0, + transaction?.currency ?? 'USD', + transaction?.created ?? '', + '', + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + participants[0], + '', + receipt, + ); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); + }, + [iouType, report, reportID, transactionID, backTo, currentUserPersonalDetails, personalDetails, shouldSkipConfirmation, transaction], + ); const updateScanAndNavigate = useCallback( (file: FileObject, source: string) => { @@ -206,7 +264,7 @@ function IOURequestStepScan({ return; } - navigateToConfirmationStep(); + navigateToConfirmationStep(file, file.uri ?? ''); }; const capturePhoto = useCallback(() => { @@ -237,15 +295,14 @@ function IOURequestStepScan({ const source = `file://${photo.path}`; IOU.setMoneyRequestReceipt(transactionID, source, photo.path, action !== CONST.IOU.ACTION.EDIT); - if (action === CONST.IOU.ACTION.EDIT) { - FileUtils.readFileAsync(source, photo.path, (file) => { + FileUtils.readFileAsync(source, photo.path, (file) => { + if (action === CONST.IOU.ACTION.EDIT) { updateScanAndNavigate(file, source); - }); - return; - } - - setDidCapturePhoto(true); - navigateToConfirmationStep(); + return; + } + setDidCapturePhoto(true); + navigateToConfirmationStep(file, source); + }); }) .catch((error: string) => { setDidCapturePhoto(false); @@ -375,14 +432,27 @@ function IOURequestStepScan({ IOURequestStepScan.displayName = 'IOURequestStepScan'; -const IOURequestStepScanOnyxProps = withOnyx({ +const IOURequestStepScanWithOnyx = withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + skipConfirmation: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`; + }, + }, user: { key: ONYXKEYS.USER, }, })(IOURequestStepScan); +const IOURequestStepScanWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(IOURequestStepScanWithOnyx); // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScanOnyxProps); +const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScanWithCurrentUserPersonalDetails); // eslint-disable-next-line rulesdir/no-negated-variables const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 654f9e9d9f91..8bca59b11580 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -1,5 +1,6 @@ -import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import type Webcam from 'react-webcam'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; @@ -14,6 +15,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; import useTheme from '@hooks/useTheme'; @@ -22,6 +24,8 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import ReceiptDropUI from '@pages/iou/ReceiptDropUI'; import StepScreenDragAndDropWrapper from '@pages/iou/request/step/StepScreenDragAndDropWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; @@ -29,17 +33,23 @@ import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableRe import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Receipt} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import NavigationAwareCamera from './NavigationAwareCamera'; -import type {IOURequestStepScanProps} from './types'; +import type {IOURequestStepOnyxProps, IOURequestStepScanProps} from './types'; function IOURequestStepScan({ report, + policy, route: { params: {action, iouType, reportID, transactionID, backTo}, }, transaction, + personalDetails, + currentUserPersonalDetails, + skipConfirmation, }: Omit) { const theme = useTheme(); const styles = useThemeStyles(); @@ -67,6 +77,16 @@ function IOURequestStepScan({ const tabIndex = 1; const isTabActive = useTabNavigatorFocus({tabIndex}); + // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace + // request and the workspace requires a category or a tag + const shouldSkipConfirmation: boolean = useMemo(() => { + if (!skipConfirmation || !report?.reportID || iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return false; + } + + return !ReportUtils.isArchivedRoom(report) && !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))); + }, [report, skipConfirmation, policy, iouType]); + /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. @@ -199,23 +219,64 @@ function IOURequestStepScan({ Navigation.goBack(backTo); }; - const navigateToConfirmationStep = useCallback(() => { - if (backTo) { - Navigation.goBack(backTo); - return; - } + const navigateToConfirmationStep = useCallback( + (file: FileObject, source: string) => { + if (backTo) { + Navigation.goBack(backTo); + return; + } - // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); - return; - } + // If the transaction was created from the global create, the person needs to select participants, so take them there. + if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && !report?.reportID) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); + return; + } + + // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically + // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. + const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + const participants = selectedParticipants.map((participant) => { + const participantAccountID = participant?.accountID ?? 0; + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }); - // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically - // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); - }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); + if (shouldSkipConfirmation) { + const receipt: Receipt = file; + receipt.source = source; + receipt.state = CONST.IOU.RECEIPT_STATE.SCANREADY; + if (iouType === CONST.IOU.TYPE.SPLIT) { + IOU.startSplitBill({ + participants, + currentUserLogin: currentUserPersonalDetails?.login ?? '', + currentUserAccountID: currentUserPersonalDetails?.accountID ?? 0, + comment: '', + receipt, + existingSplitChatReportID: reportID ?? 0, + billable: false, + category: '', + tag: '', + currency: transaction?.currency ?? 'USD', + }); + return; + } + IOU.requestMoney( + report, + 0, + transaction?.currency ?? 'USD', + transaction?.created ?? '', + '', + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + participants[0], + '', + receipt, + ); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); + }, + [iouType, report, reportID, transactionID, backTo, currentUserPersonalDetails, personalDetails, shouldSkipConfirmation, transaction], + ); const updateScanAndNavigate = useCallback( (file: FileObject, source: string) => { @@ -242,8 +303,7 @@ function IOURequestStepScan({ updateScanAndNavigate(file, source); return; } - - navigateToConfirmationStep(); + navigateToConfirmationStep(file, source); }); }; @@ -277,7 +337,7 @@ function IOURequestStepScan({ return; } - navigateToConfirmationStep(); + navigateToConfirmationStep(file, source); }, [action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, requestCameraPermission]); const clearTorchConstraints = useCallback(() => { @@ -502,8 +562,24 @@ function IOURequestStepScan({ IOURequestStepScan.displayName = 'IOURequestStepScan'; +const IOURequestStepScanWithOnyx = withOnyx, Omit>({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + skipConfirmation: { + key: ({route}) => { + const transactionID = route.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`; + }, + }, +})(IOURequestStepScan); + +const IOURequestStepScanWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(IOURequestStepScanWithOnyx); // eslint-disable-next-line rulesdir/no-negated-variables -const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScan); +const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScanWithCurrentUserPersonalDetails); // eslint-disable-next-line rulesdir/no-negated-variables const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound); diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts index 60af94aca12e..0b130a795073 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/types.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts @@ -1,13 +1,24 @@ import type {OnyxEntry} from 'react-native-onyx'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; type IOURequestStepOnyxProps = { user: OnyxEntry; + + /** Personal details of all users */ + personalDetails: OnyxEntry; + + /** The policy which the user has access to and which the report is tied to */ + policy: OnyxEntry; + + /** Whether the confirmation step should be skipped */ + skipConfirmation: OnyxEntry; }; type IOURequestStepScanProps = IOURequestStepOnyxProps & + WithCurrentUserPersonalDetailsProps & WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index 5d603902b0f1..a5ed35374e00 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -1,12 +1,14 @@ import {useIsFocused} from '@react-navigation/core'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {ValueOf} from 'type-fest'; import BigNumberPad from '@components/BigNumberPad'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import ScrollView from '@components/ScrollView'; +import SettlementButton from '@components/SettlementButton'; import TextInputWithCurrencySymbol from '@components/TextInputWithCurrencySymbol'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; @@ -21,9 +23,12 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; import CONST from '@src/CONST'; +import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; import type {SelectedTabRequest} from '@src/types/onyx'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -type CurrentMoney = {amount: string; currency: string}; +type CurrentMoney = {amount: string; currency: string; paymentMethod?: PaymentMethodType}; type MoneyRequestAmountFormProps = { /** IOU amount saved in Onyx */ @@ -38,6 +43,18 @@ type MoneyRequestAmountFormProps = { /** Whether the amount is being edited or not */ isEditing?: boolean; + /** Whether the confirmation screen should be skipped */ + skipConfirmation?: boolean; + + /** Type of the IOU */ + iouType?: ValueOf; + + /** The policyID of the request */ + policyID?: string; + + /** Depending on expense report or personal IOU report, respective bank account route */ + bankAccountRoute?: Route; + /** Whether the currency symbol is pressable */ isCurrencyPressable?: boolean; @@ -79,6 +96,10 @@ function MoneyRequestAmountForm( currency = CONST.CURRENCY.USD, isCurrencyPressable = true, isEditing = false, + skipConfirmation = false, + iouType = CONST.IOU.TYPE.REQUEST, + policyID = '', + bankAccountRoute = '', onCurrencyButtonPress, onSubmitButtonPress, selectedTab = CONST.TAB_REQUEST.MANUAL, @@ -252,25 +273,28 @@ function MoneyRequestAmountForm( /** * Submit amount and navigate to a proper page */ - const submitAndNavigateToNextPage = useCallback(() => { - // Skip the check for tax amount form as 0 is a valid input - if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { - setFormError('iou.error.invalidAmount'); - return; - } + const submitAndNavigateToNextPage = useCallback( + (iouPaymentType?: PaymentMethodType | undefined) => { + // Skip the check for tax amount form as 0 is a valid input + if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { + setFormError('iou.error.invalidAmount'); + return; + } - if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { - setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); - return; - } + if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { + setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); + return; + } - // Update display amount string post-edit to ensure consistency with backend amount - // Reference: https://github.com/Expensify/App/issues/30505 - const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)); - initializeAmount(backendAmount); + // Update display amount string post-edit to ensure consistency with backend amount + // Reference: https://github.com/Expensify/App/issues/30505 + const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)); + initializeAmount(backendAmount); - onSubmitButtonPress({amount: currentAmount, currency}); - }, [currentAmount, taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount]); + onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); + }, + [currentAmount, taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], + ); /** * Input handler to check for a forward-delete key (or keyboard shortcut) press. @@ -290,7 +314,18 @@ function MoneyRequestAmountForm( }; const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); - const buttonText = isEditing ? translate('common.save') : translate('common.next'); + const buttonText: string = useMemo(() => { + if (skipConfirmation) { + if (currentAmount !== '') { + const currencyAmount = CurrencyUtils.convertToDisplayString(CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)), currency) ?? ''; + const text = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.splitAmount', {amount: currencyAmount}) : translate('iou.submitAmount', {amount: currencyAmount}); + return text[0].toUpperCase() + text.slice(1); + } + return iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.splitExpense') : translate('iou.submitExpense'); + } + return isEditing ? translate('common.save') : translate('common.next'); + }, [skipConfirmation, iouType, currentAmount, currency, isEditing, translate]); + const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); useEffect(() => { @@ -352,17 +387,41 @@ function MoneyRequestAmountForm( longPressHandlerStateChanged={updateLongPressHandlerState} /> ) : null} -