diff --git a/README.md b/README.md index 026a63606db0..8adadfc9cbe6 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,16 @@ If you want to run the app on an actual physical iOS device, please follow the i ## Running the MacOS desktop app 🖥 * To run the **Development app**, run: `npm run desktop`, this will start a new Electron process running on your MacOS desktop in the `dist/Mac` folder. +## Receiving Notifications +To receive notifications on development build of the app while hitting the Staging or Production API, you need to use the production airship config. +### Android +1. Copy the [production config](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/android/app/src/main/assets/airshipconfig.properties#L1-L7) to the [development config](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/android/app/src/development/assets/airshipconfig.properties#L1-L8). +2. Rebuild the app. + +### iOS +1. Replace the [development key and secret](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/ios/AirshipConfig.plist#L7-L10) with the [production values](https://github.com/Expensify/App/blob/d7c1256f952c0020344d809ee7299b49a4c70db2/ios/AirshipConfig.plist#L11-L14). +2. Rebuild the app. + ## Troubleshooting 1. If you are having issues with **_Getting Started_**, please reference [React Native's Documentation](https://reactnative.dev/docs/environment-setup) 2. If you are running into CORS errors like (in the browser dev console) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0db4b032ec9d..b8350a8a0111 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046300 - versionName "1.4.63-0" + versionCode 1001046307 + versionName "1.4.63-7" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ddcc64604581..4523bf1c4418 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.63.0 + 1.4.63.7 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index a57ffb9500c5..d40d0fa27486 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.63.0 + 1.4.63.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 3c8e8c1fb63f..d472daa53ab7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.63 CFBundleVersion - 1.4.63.0 + 1.4.63.7 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 920fefc8242b..52a975e3a83e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.63-0", + "version": "1.4.63-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.63-0", + "version": "1.4.63-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 20d066eabebe..1cef7d94fcba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.63-0", + "version": "1.4.63-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index ec99fe359bc1..1c5097ae2ce5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -635,59 +635,59 @@ const CONST = { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts TYPE: { - ACTIONABLEJOINREQUEST: 'ACTIONABLEJOINREQUEST', - ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER', - ACTIONABLETRACKEXPENSEWHISPER: 'ACTIONABLETRACKEXPENSEWHISPER', - ADDCOMMENT: 'ADDCOMMENT', + ACTIONABLE_JOIN_REQUEST: 'ACTIONABLEJOINREQUEST', + ACTIONABLE_MENTION_WHISPER: 'ACTIONABLEMENTIONWHISPER', + ACTIONABLE_TRACK_EXPENSE_WHISPER: 'ACTIONABLETRACKEXPENSEWHISPER', + ADD_COMMENT: 'ADDCOMMENT', APPROVED: 'APPROVED', - CHANGEFIELD: 'CHANGEFIELD', // OldDot Action - CHANGEPOLICY: 'CHANGEPOLICY', // OldDot Action - CHANGETYPE: 'CHANGETYPE', // OldDot Action - CHRONOSOOOLIST: 'CHRONOSOOOLIST', + CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action + CHANGE_POLICY: 'CHANGEPOLICY', // OldDot Action + CHANGE_TYPE: 'CHANGETYPE', // OldDot Action + CHRONOS_OOO_LIST: 'CHRONOSOOOLIST', CLOSED: 'CLOSED', CREATED: 'CREATED', - DELEGATESUBMIT: 'DELEGATESUBMIT', // OldDot Action - DELETEDACCOUNT: 'DELETEDACCOUNT', // OldDot Action + DELEGATE_SUBMIT: 'DELEGATESUBMIT', // OldDot Action + DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action DONATION: 'DONATION', // OldDot Action - EXPORTEDTOCSV: 'EXPORTEDTOCSV', // OldDot Action - EXPORTEDTOINTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action - EXPORTEDTOQUICKBOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action + EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', - HOLDCOMMENT: 'HOLDCOMMENT', + HOLD_COMMENT: 'HOLDCOMMENT', IOU: 'IOU', - INTEGRATIONSMESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action - MANAGERATTACHRECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action - MANAGERDETACHRECEIPT: 'MANAGERDETACHRECEIPT', // OldDot Action - MARKEDREIMBURSED: 'MARKEDREIMBURSED', // OldDot Action - MARKREIMBURSEDFROMINTEGRATION: 'MARKREIMBURSEDFROMINTEGRATION', // OldDot Action - MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', + INTEGRATIONS_MESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action + MANAGER_ATTACH_RECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action + MANAGER_DETACH_RECEIPT: 'MANAGERDETACHRECEIPT', // OldDot Action + MARKED_REIMBURSED: 'MARKEDREIMBURSED', // OldDot Action + MARK_REIMBURSED_FROM_INTEGRATION: 'MARKREIMBURSEDFROMINTEGRATION', // OldDot Action + MODIFIED_EXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', - OUTDATEDBANKACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action - REIMBURSEMENTACHBOUNCE: 'REIMBURSEMENTACHBOUNCE', // OldDot Action - REIMBURSEMENTACHCANCELLED: 'REIMBURSEMENTACHCANCELLED', // OldDot Action - REIMBURSEMENTACCOUNTCHANGED: 'REIMBURSEMENTACCOUNTCHANGED', // OldDot Action - REIMBURSEMENTDELAYED: 'REIMBURSEMENTDELAYED', // OldDot Action - REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', - REIMBURSEMENTDEQUEUED: 'REIMBURSEMENTDEQUEUED', - REIMBURSEMENTREQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action - REIMBURSEMENTSETUP: 'REIMBURSEMENTSETUP', // OldDot Action + OUTDATED_BANK_ACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action + REIMBURSEMENT_ACH_BOUNCE: 'REIMBURSEMENTACHBOUNCE', // OldDot Action + REIMBURSEMENT_ACH_CANCELLED: 'REIMBURSEMENTACHCANCELLED', // OldDot Action + REIMBURSEMENT_ACCOUNT_CHANGED: 'REIMBURSEMENTACCOUNTCHANGED', // OldDot Action + REIMBURSEMENT_DELAYED: 'REIMBURSEMENTDELAYED', // OldDot Action + REIMBURSEMENT_QUEUED: 'REIMBURSEMENTQUEUED', + REIMBURSEMENT_DEQUEUED: 'REIMBURSEMENTDEQUEUED', + REIMBURSEMENT_REQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action + REIMBURSEMENT_SETUP: 'REIMBURSEMENTSETUP', // OldDot Action RENAMED: 'RENAMED', - REPORTPREVIEW: 'REPORTPREVIEW', - SELECTEDFORRANDOMAUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action + REPORT_PREVIEW: 'REPORTPREVIEW', + SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action SHARE: 'SHARE', // OldDot Action - STRIPEPAID: 'STRIPEPAID', // OldDot Action + STRIPE_PAID: 'STRIPEPAID', // OldDot Action SUBMITTED: 'SUBMITTED', - TAKECONTROL: 'TAKECONTROL', // OldDot Action - TASKCANCELLED: 'TASKCANCELLED', - TASKCOMPLETED: 'TASKCOMPLETED', - TASKEDITED: 'TASKEDITED', - TASKREOPENED: 'TASKREOPENED', + TAKE_CONTROL: 'TAKECONTROL', // OldDot Action + TASK_CANCELLED: 'TASKCANCELLED', + TASK_COMPLETED: 'TASKCOMPLETED', + TASK_EDITED: 'TASKEDITED', + TASK_REOPENED: 'TASKREOPENED', UNAPPROVED: 'UNAPPROVED', // OldDot Action UNHOLD: 'UNHOLD', UNSHARE: 'UNSHARE', // OldDot Action - UPDATEGROUPCHATMEMBERROLE: 'UPDATEGROUPCHATMEMBERROLE', - POLICYCHANGELOG: { + UPDATE_GROUP_CHAT_MEMBER_ROLE: 'UPDATEGROUPCHATMEMBERROLE', + POLICY_CHANGE_LOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', ADD_CATEGORY: 'POLICYCHANGELOG_ADD_CATEGORY', @@ -715,16 +715,16 @@ const CONST = { REMOVE_FROM_ROOM: 'POLICYCHANGELOG_REMOVEFROMROOM', LEAVE_ROOM: 'POLICYCHANGELOG_LEAVEROOM', REPLACE_CATEGORIES: 'POLICYCHANGELOG_REPLACE_CATEGORIES', - SET_AUTOREIMBURSEMENT: 'POLICYCHANGELOG_SET_AUTOREIMBURSEMENT', + SET_AUTO_REIMBURSEMENT: 'POLICYCHANGELOG_SET_AUTOREIMBURSEMENT', SET_AUTO_JOIN: 'POLICYCHANGELOG_SET_AUTO_JOIN', SET_CATEGORY_NAME: 'POLICYCHANGELOG_SET_CATEGORY_NAME', SHARED_BUDGET_NOTIFICATION: 'POLICYCHANGELOG_SHARED_BUDGET_NOTIFICATION', UPDATE_ACH_ACCOUNT: 'POLICYCHANGELOG_UPDATE_ACH_ACCOUNT', UPDATE_APPROVER_RULE: 'POLICYCHANGELOG_UPDATE_APPROVER_RULE', UPDATE_AUDIT_RATE: 'POLICYCHANGELOG_UPDATE_AUDIT_RATE', - UPDATE_AUTOHARVESTING: 'POLICYCHANGELOG_UPDATE_AUTOHARVESTING', - UPDATE_AUTOREIMBURSEMENT: 'POLICYCHANGELOG_UPDATE_AUTOREIMBURSEMENT', - UPDATE_AUTOREPORTING_FREQUENCY: 'POLICYCHANGELOG_UPDATE_AUTOREPORTING_FREQUENCY', + UPDATE_AUTO_HARVESTING: 'POLICYCHANGELOG_UPDATE_AUTOHARVESTING', + UPDATE_AUTO_REIMBURSEMENT: 'POLICYCHANGELOG_UPDATE_AUTOREIMBURSEMENT', + UPDATE_AUTO_REPORTING_FREQUENCY: 'POLICYCHANGELOG_UPDATE_AUTOREPORTING_FREQUENCY', UPDATE_BUDGET: 'POLICYCHANGELOG_UPDATE_BUDGET', UPDATE_CATEGORY: 'POLICYCHANGELOG_UPDATE_CATEGORY', UPDATE_CURRENCY: 'POLICYCHANGELOG_UPDATE_CURRENCY', @@ -755,7 +755,7 @@ const CONST = { UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE', LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY', }, - ROOMCHANGELOG: { + ROOM_CHANGE_LOG: { INVITE_TO_ROOM: 'INVITETOROOM', REMOVE_FROM_ROOM: 'REMOVEFROMROOM', LEAVE_ROOM: 'LEAVEROOM', @@ -1413,7 +1413,7 @@ const CONST = { ACTION: { EDIT: 'edit', CREATE: 'create', - MOVE: 'move', + REQUEST: 'request', CATEGORIZE: 'categorize', SHARE: 'share', }, @@ -1438,7 +1438,6 @@ const CONST = { DELETE: 'delete', APPROVE: 'approve', TRACK: 'track', - MOVE: 'move', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d30f680e3b8e..6d34a08af324 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,10 +310,6 @@ const ROUTES = { route: 'r/:reportID/invite/:role?', getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role}` as const, }, - MONEY_REQUEST_PARTICIPANTS: { - route: ':iouType/new/participants/:reportID?', - getRoute: (iouType: IOUType, reportID = '') => `${iouType}/new/participants/${reportID}` as const, - }, MONEY_REQUEST_HOLD_REASON: { route: ':type/edit/reason/:transactionID?', getRoute: (type: ValueOf, transactionID: string, reportID: string, backTo: string) => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b28f4bcc9439..1046b88c77f3 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -159,7 +159,6 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', - PARTICIPANTS: 'Money_Request_Participants', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 5e54458da001..ad4cf023c096 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -5,7 +5,7 @@ import RNFetchBlob from 'react-native-blob-util'; import RNDocumentPicker from 'react-native-document-picker'; import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker'; import {launchImageLibrary} from 'react-native-image-picker'; -import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker'; +import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker'; import ImageSize from 'react-native-image-size'; import type {FileObject, ImagePickerResponse as FileResponse} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -41,12 +41,11 @@ type Item = { * See https://github.com/react-native-image-picker/react-native-image-picker/#options * for ImagePicker configuration options */ -const imagePickerOptions: Partial = { +const imagePickerOptions = { includeBase64: false, saveToPhotos: false, selectionLimit: 1, includeExtra: false, - assetRepresentationMode: 'current', }; /** diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index f23c8db97f47..5168544357e6 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -334,7 +334,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -409,15 +409,11 @@ function MenuItem( } if (onPress && event) { - if (!singleExecution || !waitForNavigate) { + if (!singleExecution) { onPress(event); return; } - singleExecution( - waitForNavigate(() => { - onPress(event); - }), - )(); + singleExecution(onPress)(event); } }; diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index d82d81a7c47f..e90cb7584c43 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,43 +1,49 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; +import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {DefaultMileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import type {DefaultMileageRate} from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; +import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import * as IOU from '@userActions/IOU'; -import type {IOUType} from '@src/CONST'; +import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type {ReceiptSource} from '@src/types/onyx/Transaction'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; +import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; +import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; @@ -55,21 +61,19 @@ type MoneyRequestConfirmationListOnyxProps = { /** The policy of the report */ policy: OnyxEntry; - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: OnyxEntry; - /** The session of the logged in user */ session: OnyxEntry; - /** Unit and rate used for if the money request is a distance request */ + /** Unit and rate used for if the expense is a distance expense */ mileageRate: OnyxEntry; }; + type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Array) => void; + onConfirm?: (selectedParticipants: Participant[]) => void; - /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: IOUType | PaymentMethodType | undefined) => void; + /** Callback to parent modal to pay someone */ + onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; @@ -98,9 +102,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** IOU Category */ iouCategory?: string; - /** IOU Tag */ - iouTag?: string; - /** IOU isBillable */ iouIsBillable?: boolean; @@ -108,9 +109,9 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & onToggleBillable?: (isOn: boolean) => void; /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Array; + selectedParticipants: Participant[]; - /** Payee of the money request with login */ + /** Payee of the expense with login */ payeePersonalDetails?: OnyxEntry; /** Can the participants be modified or not */ @@ -129,7 +130,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & reportID?: string; /** File path of the receipt */ - receiptPath?: ReceiptSource; + receiptPath?: string; /** File name of the receipt */ receiptFilename?: string; @@ -137,19 +138,16 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** List styles for OptionsSelector */ listStyles?: StyleProp; - /** ID of the transaction that represents the money request */ - transactionID?: string; - - /** Transaction that represents the money request */ + /** Transaction that represents the expense */ transaction?: OnyxEntry; - /** Whether the money request is a distance request */ + /** Whether the expense is a distance expense */ isDistanceRequest?: boolean; - /** Whether the money request is a scan request */ + /** Whether the expense is a scan expense */ isScanRequest?: boolean; - /** Whether we're editing a split bill */ + /** Whether we're editing a split expense */ isEditingSplitBill?: boolean; /** Whether we should show the amount, date, and merchant fields. */ @@ -163,6 +161,14 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** The ID of the report action */ reportActionID?: string; + + /** The action to take */ + action?: IOUAction; +}; + +const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { + const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? ''; + return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); }; function MoneyRequestConfirmationList({ @@ -187,19 +193,6 @@ function MoneyRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, - iou = { - id: '', - amount: 0, - currency: CONST.CURRENCY.USD, - comment: '', - merchant: '', - category: '', - tag: '', - billable: false, - created: '', - participants: [], - receiptPath: '', - }, canModifyParticipants: canModifyParticipantsProp = false, session, isReadOnly = false, @@ -213,22 +206,20 @@ function MoneyRequestConfirmationList({ iouCreated, iouIsBillable = false, onToggleBillable, - iouTag = '', - transactionID = '', hasSmartScanFailed, reportActionID, + action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const {canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - - const isSplitWithScan = isSplitBill && isScanRequest; + const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const {unit, rate, currency} = mileageRate ?? { unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, @@ -238,32 +229,34 @@ function MoneyRequestConfirmationList({ const distance = transaction?.routes?.route0.distance ?? 0; const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = policy?.taxRates; + const transactionID = transaction?.transactionID ?? ''; // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - // Do not hide fields in case of send money request - const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; + // Do not hide fields in case of paying someone + const shouldShowAllFields = !!isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || !!isEditingSplitBill; // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item - const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; + const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; + const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && (!!iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); - // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); + // A flag for showing tax rate + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); // A flag for showing the billable field - const shouldShowBillable = !(policy?.disabledFields?.defaultBillable ?? true); - + const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; + const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); + const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( @@ -271,11 +264,9 @@ function MoneyRequestConfirmationList({ isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; - const defaultTaxKey = taxRates?.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) ${CONST.DOT_SEPARATOR} ${translate('common.default')}`) ?? ''; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing is not working when a left hand side value is '' - const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; + const previousTransactionAmount = usePrevious(transaction?.amount); const isFocused = useIsFocused(); const [formError, setFormError] = useState(''); @@ -283,6 +274,14 @@ function MoneyRequestConfirmationList({ const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); + const [merchantError, setMerchantError] = useState(false); + + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + + const navigateBack = () => { + Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); + }; + const shouldDisplayFieldError: boolean = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -292,20 +291,38 @@ function MoneyRequestConfirmationList({ }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = isPolicyExpenseChat && shouldDisplayFieldError && isMerchantEmpty; + const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; + + const isCategoryRequired = canUseViolations && !!policy?.requiresCategory; useEffect(() => { - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); + if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { return; } + if (!isMerchantEmpty && merchantError) { + setMerchantError(false); + if (formError === 'iou.error.invalidMerchant') { + setFormError(''); + } + } + }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); + + useEffect(() => { if (shouldDisplayFieldError && hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); return; } + if (shouldDisplayFieldError && didConfirmSplit) { + setFormError('iou.error.genericSmartscanFailureMessage'); + return; + } + if (merchantError) { + setFormError('iou.error.invalidMerchant'); + return; + } // reset the form error whenever the screen gains or loses focus setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); useEffect(() => { if (!shouldCalculateDistanceAmount) { @@ -313,39 +330,50 @@ function MoneyRequestConfirmationList({ } const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); - IOU.setMoneyRequestAmount(amount); - }, [shouldCalculateDistanceAmount, distance, rate, unit]); + IOU.setMoneyRequestAmount(transactionID, amount, currency ?? ''); + }, [shouldCalculateDistanceAmount, distance, rate, unit, transactionID, currency]); + + // Calculate and set tax amount in transaction draft + useEffect(() => { + const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); + + if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { + return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); + } + + IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); + }, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]); /** * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Array): Array => { - const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( - participantsList, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, iouCurrencyCode) : '', - ); + (participantsList: Participant[]) => { + const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); }, [iouAmount, iouCurrencyCode], ); - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again + // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); } - const splitOrRequestOptions: Array> = useMemo(() => { + const splitOrRequestOptions: Array> = useMemo(() => { let text; - if (isSplitBill && iouAmount === 0) { - text = translate('iou.split'); - } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.expense'); + if (isTypeTrackExpense) { + text = translate('iou.trackExpense'); + } else if (isTypeSplit && iouAmount === 0) { + text = translate('iou.splitExpense'); + } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { + text = translate('iou.submitExpense'); if (iouAmount !== 0) { text = translate('iou.submitAmount', {amount: formattedAmount}); } } else { - const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.submitAmount'; + const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; text = translate(translationKey, {amount: formattedAmount}); } return [ @@ -354,17 +382,13 @@ function MoneyRequestConfirmationList({ value: iouType, }, ]; - }, [isSplitBill, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - const selectedParticipants: Array = useMemo( - () => selectedParticipantsProp.filter((participant) => participant.selected), - [selectedParticipantsProp], - ); + const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; const shouldDisablePaidBySection = canModifyParticipants; - - const optionSelectorSections: OptionsListUtils.CategorySection[] = useMemo(() => { + const optionSelectorSections = useMemo(() => { const sections = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { @@ -398,9 +422,9 @@ function MoneyRequestConfirmationList({ }, ); } else { - const formattedSelectedParticipants = selectedParticipantsProp.map((participant) => ({ + const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), + isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); sections.push({ title: translate('common.to'), @@ -411,18 +435,18 @@ function MoneyRequestConfirmationList({ return sections; }, [ selectedParticipants, + selectedParticipantsProp, hasMultipleParticipants, iouAmount, iouCurrencyCode, getParticipantsWithAmount, - selectedParticipantsProp, payeePersonalDetails, translate, shouldDisablePaidBySection, canModifyParticipants, ]); - const selectedOptions: Array = useMemo(() => { + const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { return []; } @@ -430,7 +454,7 @@ function MoneyRequestConfirmationList({ }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); useEffect(() => { - if (!isDistanceRequest) { + if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { return; } @@ -442,9 +466,51 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestPendingFields(transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? CONST.CURRENCY.USD, translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, false); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transactionID]); + IOU.setMoneyRequestMerchant(transactionID, distanceMerchant, true); + }, [ + isDistanceRequestWithPendingRoute, + hasRoute, + distance, + unit, + rate, + currency, + translate, + toLocaleDigit, + isDistanceRequest, + transaction, + transactionID, + action, + isMovingTransactionFromTrackExpense, + ]); + // Auto select the category if there is only one enabled category and it is required + useEffect(() => { + const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); + if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { + return; + } + IOU.setMoneyRequestCategory(transactionID, enabledCategories[0].name); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldShowCategories, policyCategories, isCategoryRequired]); + + // Auto select the tag if there is only one enabled tag and it is required + useEffect(() => { + let updatedTagsString = TransactionUtils.getTag(transaction); + policyTagLists.forEach((tagList, index) => { + const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled); + const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations; + if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { + return; + } + updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); + }); + if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { + IOU.setMoneyRequestTag(transactionID, updatedTagsString); + } + }, [policyTagLists, transaction, transactionID, policyTags, canUseViolations]); + + /** + */ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. @@ -460,24 +526,37 @@ function MoneyRequestConfirmationList({ * Navigate to report details or profile of selected user */ const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { - if (option.accountID) { - const activeRoute = Navigation.getActiveRouteWithoutParams(); + const activeRoute = Navigation.getActiveRouteWithoutParams(); + + if (option.isSelfDM) { + Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); + return; + } + if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); } }; + /** + * @param {String} paymentMethod + */ const confirm = useCallback( - (paymentMethod: IOUType | PaymentMethodType | undefined) => { - if (!selectedParticipants.length) { + (paymentMethod: PaymentMethodType | undefined) => { + if (selectedParticipants.length === 0) { + return; + } + if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) { + setMerchantError(true); return; } if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { setFormError('iou.error.invalidCategoryLength'); return; } + if (iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { return; @@ -488,34 +567,39 @@ function MoneyRequestConfirmationList({ Log.info(`[IOU] Sending money via: ${paymentMethod}`); onSendMoney?.(paymentMethod); } else { - // validate the amount for distance requests + // validate the amount for distance expenses const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { setDidConfirmSplit(true); + setFormError('iou.error.genericSmartscanFailureMessage'); return; } + playSound(SOUNDS.DONE); setDidConfirm(true); onConfirm?.(selectedParticipants); } }, [ selectedParticipants, - onSendMoney, - onConfirm, - isEditingSplitBill, + isMerchantRequired, + isMerchantEmpty, + shouldDisplayFieldError, + transaction, iouType, + onSendMoney, + iouCurrencyCode, isDistanceRequest, iouCategory, isDistanceRequestWithPendingRoute, - iouCurrencyCode, iouAmount, - transaction, + isEditingSplitBill, + onConfirm, ], ); @@ -534,6 +618,7 @@ function MoneyRequestConfirmationList({ onPress={confirm} enablePaymentsRoute={ROUTES.IOU_SEND_ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} + shouldShowPersonalBankAccountOption currency={iouCurrencyCode} policyID={policyID} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} @@ -545,7 +630,6 @@ function MoneyRequestConfirmationList({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - shouldShowPersonalBankAccountOption enterKeyEventListenerPriority={1} /> ) : ( @@ -553,33 +637,281 @@ function MoneyRequestConfirmationList({ success pressOnEnter isDisabled={shouldDisableButton} - // eslint-disable-next-line @typescript-eslint/naming-convention - onPress={(_event, value) => confirm(value)} + onPress={(event, value) => confirm(value as PaymentMethodType)} options={splitOrRequestOptions} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} enterKeyEventListenerPriority={1} /> ); + return ( <> - {!!formError.length && ( + {!!formError && ( )} + {button} ); - }, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + + // An intermediate structure that helps us classify the fields as "primary" and "supplementary". + // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. + const classifiedFields = [ + { + item: ( + { + if (isDistanceRequest) { + return; + } + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem, styles.mt2]} + titleStyle={styles.moneyRequestConfirmationAmount} + disabled={didConfirm} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} + /> + ), + shouldShow: shouldShowSmartScanFields, + isSupplementary: false, + }, + { + item: ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), + ); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + numberOfLinesTitle={2} + /> + ), + shouldShow: true, + isSupplementary: false, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + // todo: handle edit for transaction while moving from track expense + interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} + /> + ), + shouldShow: isDistanceRequest, + isSupplementary: true, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={merchantError ? translate('common.error.fieldRequired') : ''} + rightLabel={isMerchantRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowMerchant, + isSupplementary: !isMerchantRequired, + }, + { + item: ( + { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} + /> + ), + shouldShow: shouldShowDate, + isSupplementary: true, + }, + { + item: ( + + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID)) + } + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isCategoryRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowCategories, + isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, + }, + ...policyTagLists.map(({name, required}, index) => { + const isTagRequired = required === undefined ? false : canUseViolations && required; + return { + item: ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), + ) + } + style={[styles.moneyRequestMenuItem]} + disabled={didConfirm} + interactive={!isReadOnly} + rightLabel={isTagRequired ? translate('common.required') : ''} + /> + ), + shouldShow: shouldShowTags, + isSupplementary: !isTagRequired, + }; + }), + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm} + interactive={!isReadOnly} + /> + ), + shouldShow: shouldShowTax, + isSupplementary: true, + }, + { + item: ( + + {translate('common.billable')} + onToggleBillable?.(isOn)} + /> + + ), + shouldShow: shouldShowBillable, + isSupplementary: true, + }, + ]; + + const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); + + const supplementaryFields = classifiedFields + .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) + .map((supplementaryField) => supplementaryField.item); const { image: receiptImage, thumbnail: receiptThumbnail, isThumbnail, fileExtension, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + isLocalFile, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + + const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); + const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); + + const receiptThumbnailContent = useMemo( + () => + isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ), + [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], + ); + return ( // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) {isDistanceRequest && ( - + )} - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} - {receiptImage || receiptThumbnail ? ( - - ) : ( - // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - /> - ) - )} - {shouldShowSmartScanFields && ( - { - if (isDistanceRequest) { - return; - } - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} - /> - )} - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - reportActionID, - ), - ); - }} - style={styles.moneyRequestMenuItem} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> + {(!isMovingTransactionFromTrackExpense || !hasRoute) && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (receiptImage || receiptThumbnail + ? receiptThumbnailContent + : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + /> + ))} + {primaryFields} {!shouldShowAllFields && ( )} - {shouldShowAllFields && ( - <> - {shouldShowDate && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - )} - {isDistanceRequest && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - )} - {shouldShowMerchant && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayMerchantError ? translate('common.error.enterMerchant') : ''} - /> - )} - {shouldShowCategories && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - reportActionID, - ), - ); - }} - style={styles.moneyRequestMenuItem} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={canUseViolations && !!policy?.requiresCategory ? translate('common.required') : ''} - /> - )} - {shouldShowTags && - policyTagLists.map(({name}, index) => ( - { - if (!isEditingSplitBill) { - return; - } - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT, - index, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - reportActionID, - ), - ); - }} - style={styles.moneyRequestMenuItem} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''} - /> - ))} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - )} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - )} - - {shouldShowBillable && ( - - {translate('common.billable')} - onToggleBillable?.(isOn)} - /> - - )} - - )} + {shouldShowAllFields && supplementaryFields} + ); } @@ -898,7 +993,4 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, - iou: { - key: ONYXKEYS.IOU, - }, })(MoneyRequestConfirmationList); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx deleted file mode 100755 index 11e6eec089f7..000000000000 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ /dev/null @@ -1,1015 +0,0 @@ -import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; -import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; -import usePrevious from '@hooks/usePrevious'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {DefaultMileageRate} from '@libs/DistanceRequestUtils'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import * as IOUUtils from '@libs/IOUUtils'; -import Log from '@libs/Log'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import * as IOU from '@userActions/IOU'; -import type {IOUAction, IOUType} from '@src/CONST'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {Participant} from '@src/types/onyx/IOU'; -import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import Button from './Button'; -import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import type {DropdownOption} from './ButtonWithDropdownMenu/types'; -import ConfirmedRoute from './ConfirmedRoute'; -import ConfirmModal from './ConfirmModal'; -import FormHelpMessage from './FormHelpMessage'; -import * as Expensicons from './Icon/Expensicons'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import OptionsSelector from './OptionsSelector'; -import PDFThumbnail from './PDFThumbnail'; -import ReceiptEmptyState from './ReceiptEmptyState'; -import ReceiptImage from './ReceiptImage'; -import SettlementButton from './SettlementButton'; -import Switch from './Switch'; -import Text from './Text'; - -type MoneyRequestConfirmationListOnyxProps = { - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; - - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; - - /** The policy of the report */ - policy: OnyxEntry; - - /** The session of the logged in user */ - session: OnyxEntry; - - /** Unit and rate used for if the expense is a distance expense */ - mileageRate: OnyxEntry; -}; - -type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { - /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Participant[]) => void; - - /** Callback to parent modal to pay someone */ - onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; - - /** Callback to inform a participant is selected */ - onSelectParticipant?: (option: Participant) => void; - - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: boolean; - - /** IOU amount */ - iouAmount: number; - - /** IOU comment */ - iouComment?: string; - - /** IOU currency */ - iouCurrencyCode?: string; - - /** IOU type */ - iouType?: IOUType; - - /** IOU date */ - iouCreated?: string; - - /** IOU merchant */ - iouMerchant?: string; - - /** IOU Category */ - iouCategory?: string; - - /** IOU isBillable */ - iouIsBillable?: boolean; - - /** Callback to toggle the billable state */ - onToggleBillable?: (isOn: boolean) => void; - - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Participant[]; - - /** Payee of the expense with login */ - payeePersonalDetails?: OnyxTypes.PersonalDetails; - - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - - /** Should the list be read only, and not editable? */ - isReadOnly?: boolean; - - /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute?: Route; - - /** The policyID of the request */ - policyID?: string; - - /** The reportID of the request */ - reportID?: string; - - /** File path of the receipt */ - receiptPath?: string; - - /** File name of the receipt */ - receiptFilename?: string; - - /** List styles for OptionsSelector */ - listStyles?: StyleProp; - - /** Transaction that represents the expense */ - transaction?: OnyxEntry; - - /** Whether the expense is a distance expense */ - isDistanceRequest?: boolean; - - /** Whether the expense is a scan expense */ - isScanRequest?: boolean; - - /** Whether we're editing a split expense */ - isEditingSplitBill?: boolean; - - /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields?: boolean; - - /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat?: boolean; - - /** Whether smart scan failed */ - hasSmartScanFailed?: boolean; - - reportActionID?: string; - - action?: IOUAction; -}; - -const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || ''; - return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); -}; - -function MoneyTemporaryForRefactorRequestConfirmationList({ - transaction = null, - onSendMoney, - onConfirm, - onSelectParticipant, - iouType = CONST.IOU.TYPE.REQUEST, - isScanRequest = false, - iouAmount, - policyCategories, - mileageRate, - isDistanceRequest = false, - policy, - isPolicyExpenseChat = false, - iouCategory = '', - shouldShowSmartScanFields = true, - isEditingSplitBill, - policyTags, - iouCurrencyCode, - iouMerchant, - hasMultipleParticipants, - selectedParticipants: pickedParticipants, - payeePersonalDetails, - canModifyParticipants = false, - session, - isReadOnly = false, - bankAccountRoute = '', - policyID = '', - reportID = '', - receiptPath = '', - iouComment, - receiptFilename = '', - listStyles, - iouCreated, - iouIsBillable = false, - onToggleBillable, - hasSmartScanFailed, - reportActionID, - action = CONST.IOU.ACTION.CREATE, -}: MoneyRequestConfirmationListProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate, toLocaleDigit} = useLocalize(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {canUseViolations} = usePermissions(); - - const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; - - const {unit, rate, currency} = mileageRate ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - rate: 0, - currency: 'USD', - }; - const distance = transaction?.routes?.route0.distance ?? 0; - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; - const taxRates = policy?.taxRates; - - // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - - // A flag and a toggler for showing the rest of the form fields - const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - - // Do not hide fields in case of paying someone - const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - - const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; - const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; - - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - - // A flag for showing the tags field - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); - - // A flag for showing tax rate - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); - - // A flag for showing the billable field - const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; - const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); - const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; - const formattedAmount = isDistanceRequestWithPendingRoute - ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, - isDistanceRequest ? currency : iouCurrencyCode, - ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; - - const previousTransactionAmount = usePrevious(transaction?.amount); - - const isFocused = useIsFocused(); - const [formError, setFormError] = useState(''); - - const [didConfirm, setDidConfirm] = useState(false); - const [didConfirmSplit, setDidConfirmSplit] = useState(false); - - const [merchantError, setMerchantError] = useState(false); - - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - - const navigateBack = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID)); - }; - - const shouldDisplayFieldError: boolean = useMemo(() => { - if (!isEditingSplitBill) { - return false; - } - - return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - - const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; - - const isCategoryRequired = canUseViolations && !!policy?.requiresCategory; - - useEffect(() => { - if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { - return; - } - if (!isMerchantEmpty && merchantError) { - setMerchantError(false); - if (formError === 'iou.error.invalidMerchant') { - setFormError(''); - } - } - }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); - - useEffect(() => { - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (merchantError) { - setFormError('iou.error.invalidMerchant'); - return; - } - // reset the form error whenever the screen gains or loses focus - setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); - - useEffect(() => { - if (!shouldCalculateDistanceAmount) { - return; - } - - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); - IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? ''); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); - - // Calculate and set tax amount in transaction draft - useEffect(() => { - const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); - const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - - if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { - return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); - } - - IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true); - }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); - - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); - }, - [iouAmount, iouCurrencyCode], - ); - - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again - if (isEditingSplitBill && didConfirm) { - setDidConfirm(false); - } - - const splitOrRequestOptions: Array> = useMemo(() => { - let text; - if (isTypeTrackExpense) { - text = translate('iou.trackExpense'); - } else if (isTypeSplit && iouAmount === 0) { - text = translate('iou.splitExpense'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.submitExpense'); - if (iouAmount !== 0) { - text = translate('iou.submitAmount', {amount: formattedAmount}); - } - } else { - const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; - text = translate(translationKey, {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: iouType, - }, - ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - - const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); - const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants); - useEffect(() => { - userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants; - }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]); - const shouldDisablePaidBySection = userCanModifyParticipants.current; - - const optionSelectorSections = useMemo(() => { - const sections = []; - const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); - if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipants) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } - - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - personalDetailsOfPayee, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); - } else { - const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, - isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - sections.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - shouldShow: true, - }); - } - return sections; - }, [ - selectedParticipants, - pickedParticipants, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - personalDetailsOfPayee, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); - - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)]; - }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]); - - useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { - return; - } - - /* - Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: - When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. - In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. - */ - IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction, action, isMovingTransactionFromTrackExpense]); - - // Auto select the category if there is only one enabled category and it is required - useEffect(() => { - const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); - if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { - return; - } - IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldShowCategories, policyCategories, isCategoryRequired]); - - // Auto select the tag if there is only one enabled tag and it is required - useEffect(() => { - let updatedTagsString = TransactionUtils.getTag(transaction); - policyTagLists.forEach((tagList, index) => { - const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled); - const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations; - if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { - return; - } - updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); - }); - if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { - IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [policyTagLists, policyTags, canUseViolations]); - - /** - */ - const selectParticipant = useCallback( - (option: Participant) => { - // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID) { - return; - } - onSelectParticipant?.(option); - }, - [session?.accountID, onSelectParticipant], - ); - - /** - * Navigate to report details or profile of selected user - */ - const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - if (option.isSelfDM) { - Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); - return; - } - - if (option.accountID) { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); - } - }; - - /** - * @param {String} paymentMethod - */ - const confirm = useCallback( - (paymentMethod: PaymentMethodType | undefined) => { - if (selectedParticipants.length === 0) { - return; - } - if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) { - setMerchantError(true); - return; - } - - if (iouType === CONST.IOU.TYPE.SEND) { - if (!paymentMethod) { - return; - } - - setDidConfirm(true); - - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney?.(paymentMethod); - } else { - // validate the amount for distance expenses - const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); - if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { - setFormError('common.error.invalidAmount'); - return; - } - - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { - setDidConfirmSplit(true); - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - - playSound(SOUNDS.DONE); - setDidConfirm(true); - onConfirm?.(selectedParticipants); - } - }, - [ - selectedParticipants, - isMerchantRequired, - isMerchantEmpty, - shouldDisplayFieldError, - transaction, - iouType, - onSendMoney, - iouCurrencyCode, - isDistanceRequest, - isDistanceRequestWithPendingRoute, - iouAmount, - isEditingSplitBill, - onConfirm, - ], - ); - - const footerContent = useMemo(() => { - if (isReadOnly) { - return; - } - - const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; - - const button = shouldShowSettlementButton ? ( - - ) : ( - confirm(value as PaymentMethodType)} - options={splitOrRequestOptions} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} - enterKeyEventListenerPriority={1} - /> - ); - - return ( - <> - {!!formError && ( - - )} - - {button} - - ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". - // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. - const classifiedFields = [ - { - item: ( - { - if (isDistanceRequest) { - return; - } - if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} - /> - ), - shouldShow: shouldShowSmartScanFields, - isSupplementary: false, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - ), - shouldShow: true, - isSupplementary: false, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - disabled={didConfirm} - // todo: handle edit for transaction while moving from track expense - interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} - /> - ), - shouldShow: isDistanceRequest, - isSupplementary: true, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={merchantError ? translate('common.error.fieldRequired') : ''} - rightLabel={isMerchantRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowMerchant, - isSupplementary: !isMerchantRequired, - }, - { - item: ( - { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - ), - shouldShow: shouldShowDate, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isCategoryRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowCategories, - isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, - }, - ...policyTagLists.map(({name, required}, index) => { - const isTagRequired = required === undefined ? false : canUseViolations && required; - return { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isTagRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowTags, - isSupplementary: !isTagRequired, - }; - }), - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - {translate('common.billable')} - onToggleBillable?.(isOn)} - /> - - ), - shouldShow: shouldShowBillable, - isSupplementary: true, - }, - ]; - - const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); - - const supplementaryFields = classifiedFields - .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) - .map((supplementaryField) => supplementaryField.item); - - const { - image: receiptImage, - thumbnail: receiptThumbnail, - isThumbnail, - fileExtension, - isLocalFile, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); - - const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); - const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - - const receiptThumbnailContent = useMemo( - () => - isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ), - [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], - ); - - return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - - {isDistanceRequest && ( - - - - )} - {(!isMovingTransactionFromTrackExpense || !hasRoute) && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (receiptImage || receiptThumbnail - ? receiptThumbnailContent - : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - /> - ))} - {primaryFields} - {!shouldShowAllFields && ( - - -