diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 03b76f21d6d..60fca9fac87 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,5 +1,6 @@ import type {IsEqual, ValueOf} from 'type-fest'; import type CONST from './CONST'; +import type {IOURequestType} from './libs/actions/IOU'; // This is a file containing constants for all the routes we want to be able to go to @@ -396,7 +397,7 @@ const ROUTES = { // straight to those flows without needing to have optimistic transaction and report IDs. MONEY_REQUEST_START: { route: 'start/:iouType/:iouRequestType', - getRoute: (iouType: ValueOf, iouRequestType: ValueOf) => `start/${iouType}/${iouRequestType}` as const, + getRoute: (iouType: ValueOf, iouRequestType: IOURequestType) => `start/${iouType}/${iouRequestType}` as const, }, MONEY_REQUEST_CREATE_TAB_DISTANCE: { route: ':action/:iouType/start/:transactionID/:reportID/distance', diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 0307b67114e..f26d7c25c7e 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -21,7 +21,7 @@ type CategoryPickerProps = CategoryPickerOnyxProps & { /** It's used by withOnyx HOC */ // eslint-disable-next-line react/no-unused-prop-types policyID: string; - selectedCategory: string; + selectedCategory?: string; onSubmit: (item: ListItem) => void; }; @@ -38,7 +38,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC { name: selectedCategory, enabled: true, - accountID: null, + accountID: undefined, isSelected: true, }, ]; diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index b9d2d61efa7..ff5768efaed 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -14,7 +14,7 @@ import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/ type SelectedTagOption = { name: string; enabled: boolean; - accountID: number | null; + accountID: number | undefined; }; type TagPickerOnyxProps = { @@ -68,7 +68,7 @@ function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRe { name: selectedTag, enabled: true, - accountID: null, + accountID: undefined, }, ]; }, [selectedTag]); diff --git a/src/libs/API/parameters/RequestMoneyParams.ts b/src/libs/API/parameters/RequestMoneyParams.ts index b55f9fd7a2a..ce8fb99c3f2 100644 --- a/src/libs/API/parameters/RequestMoneyParams.ts +++ b/src/libs/API/parameters/RequestMoneyParams.ts @@ -17,7 +17,7 @@ type RequestMoneyParams = { createdChatReportActionID: string; createdIOUReportActionID: string; reportPreviewReportActionID: string; - receipt: Receipt; + receipt?: Receipt; receiptState?: ValueOf; category?: string; tag?: string; diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index f48c8666f10..9c8d9761d88 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -15,7 +15,7 @@ type TrackExpenseParams = { createdChatReportActionID: string; createdIOUReportActionID?: string; reportPreviewReportActionID?: string; - receipt: Receipt; + receipt?: Receipt; receiptState?: ValueOf; category?: string; tag?: string; diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 65390982f18..41587275024 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -3,11 +3,12 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, Transaction} from '@src/types/onyx'; +import type {IOURequestType} from './actions/IOU'; import * as CurrencyUtils from './CurrencyUtils'; import Navigation from './Navigation/Navigation'; import * as TransactionUtils from './TransactionUtils'; -function navigateToStartMoneyRequestStep(requestType: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) { +function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: ValueOf, transactionID: string, reportID: string) { // If the participants were automatically added to the transaction, then the user needs taken back to the starting step switch (requestType) { case CONST.IOU.REQUEST_TYPE.DISTANCE: diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 162a25647b2..ed34b8ee385 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -360,12 +360,6 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; - [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { - action: ValueOf; - iouType: string; - transactionID: string; - reportID: string; - }; [SCREENS.MONEY_REQUEST.CURRENCY]: { iouType: string; reportID: string; @@ -391,6 +385,7 @@ type MoneyRequestNavigatorParamList = { action: ValueOf; iouType: ValueOf; transactionID: string; + reportActionID: string; reportID: string; backTo: Routes; }; @@ -450,6 +445,14 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { + action: ValueOf; + iouType: ValueOf; + transactionID: string; + reportID: string; + pageIndex?: string; + backTo?: string; + }; }; type NewTaskNavigatorParamList = { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index e1a3e9207ad..f61f51cd535 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -169,7 +169,7 @@ type MemberForList = { keyForList: string; isSelected: boolean; isDisabled: boolean; - accountID?: number | null; + accountID?: number; login: string; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; @@ -357,7 +357,7 @@ function isPersonalDetailsReady(personalDetails: OnyxEntry) /** * Get the participant option for a report. */ -function getParticipantsOption(participant: ReportUtils.OptionData, personalDetails: OnyxEntry): Participant { +function getParticipantsOption(participant: ReportUtils.OptionData | Participant, personalDetails: OnyxEntry): Participant { const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1]; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; @@ -644,21 +644,22 @@ function createOption( const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {}; const result: ReportUtils.OptionData = { text: undefined, - alternateText: null, + alternateText: undefined, pendingAction: undefined, allReportErrors: undefined, brickRoadIndicator: null, icons: undefined, tooltipText: null, ownerAccountID: undefined, - subtitle: null, + subtitle: undefined, participantsList: undefined, accountID: 0, - login: null, + login: undefined, reportID: '', - phoneNumber: null, - keyForList: null, - searchText: null, + phoneNumber: undefined, + hasDraftComment: false, + keyForList: undefined, + searchText: undefined, isDefaultRoom: false, isPinned: false, isWaitingOnBankAccount: false, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c9241054e74..85f5c414dbe 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -379,23 +379,24 @@ type CustomIcon = { type OptionData = { text?: string; - alternateText?: string | null; + alternateText?: string; allReportErrors?: Errors; brickRoadIndicator?: ValueOf | '' | null; tooltipText?: string | null; alternateTextMaxLines?: number; boldStyle?: boolean; customIcon?: CustomIcon; - subtitle?: string | null; - login?: string | null; - accountID?: number | null; + subtitle?: string; + login?: string; + accountID?: number; pronouns?: string; status?: Status | null; - phoneNumber?: string | null; + phoneNumber?: string; isUnread?: boolean | null; isUnreadWithMention?: boolean | null; - keyForList?: string | null; - searchText?: string | null; + hasDraftComment?: boolean | null; + keyForList?: string; + searchText?: string; isIOUReportOwner?: boolean | null; isArchivedRoom?: boolean | null; shouldShowSubscript?: boolean | null; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index e1dd7ea684b..c5439d68708 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -209,19 +209,20 @@ function getOptionData({ const result: ReportUtils.OptionData = { text: '', - alternateText: null, + alternateText: undefined, allReportErrors: OptionsListUtils.getAllReportErrors(report, reportActions), brickRoadIndicator: null, tooltipText: null, - subtitle: null, - login: null, - accountID: null, + subtitle: undefined, + login: undefined, + accountID: undefined, reportID: '', - phoneNumber: null, + phoneNumber: undefined, isUnread: null, isUnreadWithMention: null, - keyForList: null, - searchText: null, + hasDraftComment: false, + keyForList: undefined, + searchText: undefined, isPinned: false, hasOutstandingChildRequest: false, isIOUReportOwner: null, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 96c5ba8c78e..9c398998588 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -3,10 +3,12 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {IOURequestType} from './actions/IOU'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; @@ -45,22 +47,23 @@ function isDistanceRequest(transaction: OnyxEntry): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } -function isScanRequest(transaction: Transaction): boolean { +function isScanRequest(transaction: OnyxEntry): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { - return transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; + return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; } return Boolean(transaction?.receipt?.source); } -function getRequestType(transaction: Transaction): ValueOf { +function getRequestType(transaction: OnyxEntry): IOURequestType { if (isDistanceRequest(transaction)) { return CONST.IOU.REQUEST_TYPE.DISTANCE; } if (isScanRequest(transaction)) { return CONST.IOU.REQUEST_TYPE.SCAN; } + return CONST.IOU.REQUEST_TYPE.MANUAL; } @@ -452,12 +455,13 @@ function getCreated(transaction: OnyxEntry, dateFormat: string = CO /** * Returns the translation key to use for the header title */ -function getHeaderTitleTranslationKey(transaction: Transaction): string { - const headerTitles = { +function getHeaderTitleTranslationKey(transaction: OnyxEntry): TranslationPaths { + const headerTitles: Record = { [CONST.IOU.REQUEST_TYPE.DISTANCE]: 'tabSelector.distance', [CONST.IOU.REQUEST_TYPE.MANUAL]: 'tabSelector.manual', [CONST.IOU.REQUEST_TYPE.SCAN]: 'tabSelector.scan', }; + return headerTitles[getRequestType(transaction)]; } @@ -539,7 +543,11 @@ function getWaypointIndex(key: string): number { /** * Filters the waypoints which are valid and returns those */ -function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = false): WaypointCollection { +function getValidWaypoints(waypoints: WaypointCollection | undefined, reArrangeIndexes = false): WaypointCollection { + if (!waypoints) { + return {}; + } + const sortedIndexes = Object.keys(waypoints) .map(getWaypointIndex) .sort((a, b) => a - b); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 463642cdecb..0382732e6f4 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -128,6 +128,11 @@ type SendMoneyParamsData = { failureData: OnyxUpdate[]; }; +type GPSPoint = { + lat: number; + long: number; +}; + let betas: OnyxTypes.Beta[] = []; Onyx.connect({ key: ONYXKEYS.BETAS, @@ -334,7 +339,7 @@ function updateMoneyRequestTypeParams(routes: StackNavigationState, reportID: string, requestType?: ValueOf) { +function startMoneyRequest(iouType: ValueOf, reportID: string, requestType?: IOURequestType) { clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); switch (requestType) { case CONST.IOU.REQUEST_TYPE.MANUAL: @@ -406,7 +411,7 @@ function setMoneyRequestBillable_temporaryForRefactor(transactionID: string, bil } // eslint-disable-next-line @typescript-eslint/naming-convention -function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[]) { +function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, participants: Participant[] = []) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } @@ -2251,16 +2256,16 @@ function updateDistanceRequest( * Request money from another user */ function requestMoney( - report: OnyxTypes.Report, + report: OnyxEntry, amount: number, currency: string, created: string, merchant: string, - payeeEmail: string, + payeeEmail: string | undefined, payeeAccountID: number, participant: Participant, comment: string, - receipt: Receipt, + receipt: Receipt | undefined, category?: string, tag?: string, taxCode = '', @@ -2269,12 +2274,12 @@ function requestMoney( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints = undefined, + gpsPoints?: GPSPoint, ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; - const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report?.chatReportID) : report; + const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { payerAccountID, @@ -2309,7 +2314,7 @@ function requestMoney( payeeEmail, moneyRequestReportID, ); - const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID; + const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID; const parameters: RequestMoneyParams = { debtorEmail: payerEmail, @@ -2342,7 +2347,9 @@ function requestMoney( API.write(WRITE_COMMANDS.REQUEST_MONEY, parameters, onyxData); resetMoneyRequestInfo(); Navigation.dismissModal(activeReportID); - Report.notifyNewAction(activeReportID, payeeAccountID); + if (activeReportID) { + Report.notifyNewAction(activeReportID, payeeAccountID); + } } /** @@ -2354,11 +2361,11 @@ function trackExpense( currency: string, created: string, merchant: string, - payeeEmail: string, + payeeEmail: string | undefined, payeeAccountID: number, participant: Participant, comment: string, - receipt: Receipt, + receipt?: Receipt, category?: string, tag?: string, taxCode = '', @@ -2367,7 +2374,7 @@ function trackExpense( policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, - gpsPoints = undefined, + gpsPoints?: GPSPoint, ) { const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; @@ -2857,25 +2864,41 @@ function createSplitsAndOnyxData( }; } +type SplitBillActionsParams = { + participants: Participant[]; + currentUserLogin: string; + currentUserAccountID: number; + amount: number; + comment: string; + currency: string; + merchant: string; + created: string; + category?: string; + tag?: string; + billable?: boolean; + iouRequestType?: IOURequestType; + existingSplitChatReportID?: string; +}; + /** * @param amount - always in smallest currency unit * @param existingSplitChatReportID - Either a group DM or a workspace chat */ -function splitBill( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - amount: number, - comment: string, - currency: string, - merchant: string, - created: string, - category: string, - tag: string, - existingSplitChatReportID = '', +function splitBill({ + participants, + currentUserLogin, + currentUserAccountID, + amount, + comment, + currency, + merchant, + created, + category = '', + tag = '', billable = false, - iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, -) { + iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + existingSplitChatReportID = '', +}: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( participants, @@ -2921,20 +2944,20 @@ function splitBill( /** * @param amount - always in the smallest currency unit */ -function splitBillAndOpenReport( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - amount: number, - comment: string, - currency: string, - merchant: string, - created: string, - category: string, - tag: string, - billable: boolean, - iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, -) { +function splitBillAndOpenReport({ + participants, + currentUserLogin, + currentUserAccountID, + amount, + comment, + currency, + merchant, + created, + category = '', + tag = '', + billable = false, + iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, +}: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( participants, @@ -2977,23 +3000,36 @@ function splitBillAndOpenReport( Report.notifyNewAction(splitData.chatReportID, currentUserAccountID); } +type StartSplitBilActionParams = { + participants: Participant[]; + currentUserLogin: string; + currentUserAccountID: number; + comment: string; + receipt: Receipt; + existingSplitChatReportID?: string; + billable?: boolean; + category: string | undefined; + tag: string | undefined; + currency: string; +}; + /** Used exclusively for starting a split bill request that contains a receipt, the split request will be completed once the receipt is scanned * or user enters details manually. * * @param existingSplitChatReportID - Either a group DM or a workspace chat */ -function startSplitBill( - participants: Participant[], - currentUserLogin: string, - currentUserAccountID: number, - comment: string, - category: string, - tag: string, - currency: string, - receipt: Receipt, +function startSplitBill({ + participants, + currentUserLogin, + currentUserAccountID, + comment, + receipt, existingSplitChatReportID = '', billable = false, -) { + category = '', + tag = '', + currency, +}: StartSplitBilActionParams) { const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); const {splitChatReport, existingSplitChatReport} = getOrCreateOptimisticSplitChatReport(existingSplitChatReportID, participants, participantAccountIDs, currentUserAccountID); @@ -4744,7 +4780,7 @@ function sendMoneyElsewhere(report: OnyxTypes.Report, amount: number, currency: * @param managerID - Account ID of the person sending the money * @param recipient - The user receiving the money */ -function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant) { +function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: string, comment: string, managerID: number, recipient: Participant | ReportUtils.OptionData) { const {params, optimisticData, successData, failureData} = getSendMoneyParams(report, amount, currency, comment, CONST.IOU.PAYMENT_TYPE.EXPENSIFY, managerID, recipient); API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData}); @@ -5406,14 +5442,14 @@ function unholdRequest(transactionID: string, reportID: string) { } // eslint-disable-next-line rulesdir/no-negated-variables function navigateToStartStepIfScanFileCannotBeRead( - receiptFilename: string, - receiptPath: string, + receiptFilename: string | undefined, + receiptPath: ReceiptSource | undefined, onSuccess: (file: File) => void, - requestType: ValueOf, + requestType: IOURequestType, iouType: ValueOf, transactionID: string, reportID: string, - receiptType: string, + receiptType: string | undefined, ) { if (!receiptFilename || !receiptPath) { return; @@ -5427,7 +5463,7 @@ function navigateToStartStepIfScanFileCannotBeRead( } IOUUtils.navigateToStartMoneyRequestStep(requestType, iouType, transactionID, reportID); }; - FileUtils.readFileAsync(receiptPath, receiptFilename, onSuccess, onFailure, receiptType); + FileUtils.readFileAsync(receiptPath.toString(), receiptFilename, onSuccess, onFailure, receiptType); } /** Save the preferred payment method for a policy */ @@ -5435,6 +5471,7 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod}); } +export type {GPSPoint as GpsPoint, IOURequestType}; export { setMoneyRequestParticipants, createDistanceRequest, diff --git a/src/pages/iou/request/step/IOURequestStepCategory.js b/src/pages/iou/request/step/IOURequestStepCategory.js deleted file mode 100644 index 4f0c77480c0..00000000000 --- a/src/pages/iou/request/step/IOURequestStepCategory.js +++ /dev/null @@ -1,191 +0,0 @@ -import lodashGet from 'lodash/get'; -import lodashIsEmpty from 'lodash/isEmpty'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import CategoryPicker from '@components/CategoryPicker'; -import categoryPropTypes from '@components/categoryPropTypes'; -import tagPropTypes from '@components/tagPropTypes'; -import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -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 reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import {policyPropTypes} from '@src/pages/workspace/withPolicy'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; -import StepScreenWrapper from './StepScreenWrapper'; -import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; -import withWritableReportOrNotFound from './withWritableReportOrNotFound'; - -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: transactionPropTypes, - - /** The draft transaction that holds data to be persisted on the current transaction */ - splitDraftTransaction: transactionPropTypes, - - /** The report attached to the transaction */ - report: reportPropTypes, - - /** The policy of the report */ - policy: policyPropTypes.policy, - - /** Collection of categories attached to a policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), - - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** The actions from the parent report */ - reportActions: PropTypes.shape(reportActionPropTypes), - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - - /** Currently logged in user email */ - email: PropTypes.string, - }).isRequired, -}; - -const defaultProps = { - report: {}, - transaction: {}, - splitDraftTransaction: {}, - policy: null, - policyTags: null, - policyCategories: null, - reportActions: {}, -}; - -function IOURequestStepCategory({ - report, - route: { - params: {transactionID, backTo, action, iouType, reportActionID}, - }, - transaction, - splitDraftTransaction, - policy, - policyTags, - policyCategories, - session, - reportActions, -}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const isEditing = action === CONST.IOU.ACTION.EDIT; - const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; - const {category: transactionCategory} = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction); - - const reportAction = reportActions[report.parentReportActionID || reportActionID]; - const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (transactionCategory || OptionsListUtils.hasEnabledOptions(_.values(policyCategories))); - const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; - const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); - // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); - - const navigateBack = () => { - Navigation.goBack(backTo); - }; - - /** - * @param {Object} category - * @param {String} category.searchText - */ - const updateCategory = (category) => { - const isSelectedCategory = category.searchText === transactionCategory; - const updatedCategory = isSelectedCategory ? '' : category.searchText; - - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value - if (isEditingSplitBill) { - IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}); - navigateBack(); - return; - } - - if (isEditing) { - IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); - navigateBack(); - return; - } - - IOU.setMoneyRequestCategory(transactionID, updatedCategory); - navigateBack(); - }; - - return ( - - {translate('iou.categorySelection')} - - - ); -} - -IOURequestStepCategory.displayName = 'IOURequestStepCategory'; -IOURequestStepCategory.propTypes = propTypes; -IOURequestStepCategory.defaultProps = defaultProps; - -export default compose( - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - splitDraftTransaction: { - key: ({route}) => { - const transactionID = lodashGet(route, 'params.transactionID', 0); - return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; - }, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, - reportActions: { - key: ({ - report, - route: { - params: {action, iouType}, - }, - }) => { - let reportID = '0'; - if (action === CONST.IOU.ACTION.EDIT) { - reportID = iouType === CONST.IOU.TYPE.SPLIT ? report.reportID : report.parentReportID; - } - return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; - }, - canEvict: false, - }, - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(IOURequestStepCategory); diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx new file mode 100644 index 00000000000..26b918e0c99 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -0,0 +1,173 @@ +import lodashIsEmpty from 'lodash/isEmpty'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import CategoryPicker from '@components/CategoryPicker'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +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 CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy, PolicyCategories, PolicyTagList, ReportActions, Session, Transaction} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; + +type IOURequestStepCategoryOnyxProps = { + /** The draft transaction that holds data to be persisted on the current transaction */ + splitDraftTransaction: OnyxEntry; + + /** The policy of the report */ + policy: OnyxEntry; + + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; + + /** The actions from the parent report */ + reportActions: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type IOURequestStepCategoryProps = IOURequestStepCategoryOnyxProps & + WithWritableReportOrNotFoundProps & + WithFullTransactionOrNotFoundProps; + +function IOURequestStepCategory({ + report, + route: { + params: {transactionID, backTo, action, iouType, reportActionID}, + }, + transaction, + splitDraftTransaction, + policy, + policyTags, + policyCategories, + reportActions, + session, +}: IOURequestStepCategoryProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const isEditing = action === CONST.IOU.ACTION.EDIT; + const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; + const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; + + // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + + const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; + const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const updateCategory = (category: ListItem) => { + const categorySearchText = category.searchText ?? ''; + const isSelectedCategory = categorySearchText === transactionCategory; + const updatedCategory = isSelectedCategory ? '' : categorySearchText; + + if (transaction) { + // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value + if (isEditingSplitBill) { + IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}); + navigateBack(); + return; + } + + if (isEditing && report) { + IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); + navigateBack(); + return; + } + } + + IOU.setMoneyRequestCategory(transactionID, updatedCategory); + + navigateBack(); + }; + + return ( + + {translate('iou.categorySelection')} + + + ); +} + +IOURequestStepCategory.displayName = 'IOURequestStepCategory'; + +const IOURequestStepCategoryWithOnyx = withOnyx({ + splitDraftTransaction: { + key: ({route}) => { + const transactionID = route?.params.transactionID ?? 0; + return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`; + }, + }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + policyCategories: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, + reportActions: { + key: ({ + report, + route: { + params: {action, iouType}, + }, + }) => { + let reportID = '0'; + if (action === CONST.IOU.ACTION.EDIT && report) { + if (iouType === CONST.IOU.TYPE.SPLIT) { + reportID = report.reportID; + } else if (report.parentReportID) { + reportID = report.parentReportID; + } + } + return `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`; + }, + canEvict: false, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(IOURequestStepCategory); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepCategoryWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepCategoryWithOnyx); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepCategoryWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepCategoryWithFullTransactionOrNotFound); +export default IOURequestStepCategoryWithWritableReportOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx similarity index 67% rename from src/pages/iou/request/step/IOURequestStepConfirmation.js rename to src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1999b7e56f3..d20a576d279 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,24 +1,20 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import categoryPropTypes from '@components/categoryPropTypes'; +import type {ValueOf} from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyTemporaryForRefactorRequestConfirmationList'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import tagPropTypes from '@components/tagPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; +import {openDraftWorkspaceRequest} from '@libs/actions/Policy'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getCurrentPosition from '@libs/getCurrentPosition'; import * as IOUUtils from '@libs/IOUUtils'; @@ -27,51 +23,35 @@ 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 reportPropTypes from '@pages/reportPropTypes'; -import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; -import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import type SCREENS from '@src/SCREENS'; +import type {Policy, PolicyCategories, PolicyTagList} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import type {Receipt} from '@src/types/onyx/Transaction'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** The personal details of the current user */ - ...withCurrentUserPersonalDetailsPropTypes, - +type IOURequestStepConfirmationOnyxProps = { /** The policy of the report */ - ...policyPropTypes, - - /** The tag configuration of the report's policy */ - policyTags: tagPropTypes, + policy: OnyxEntry; /** The category configuration of the report's policy */ - policyCategories: PropTypes.objectOf(categoryPropTypes), + policyCategories: OnyxEntry; - /** The full IOU report */ - report: reportPropTypes, - - /** The transaction object being modified in Onyx */ - transaction: transactionPropTypes, -}; -const defaultProps = { - personalDetails: {}, - policy: null, - policyCategories: null, - policyTags: null, - report: {}, - transaction: {}, - ...withCurrentUserPersonalDetailsDefaultProps, + /** The tag configuration of the report's policy */ + policyTags: OnyxEntry; }; + +type IOURequestStepConfirmationProps = IOURequestStepConfirmationOnyxProps & + WithWritableReportOrNotFoundProps & + WithFullTransactionOrNotFoundProps; + function IOURequestStepConfirmation({ - currentUserPersonalDetails, policy, policyTags, policyCategories, @@ -80,20 +60,25 @@ function IOURequestStepConfirmation({ params: {iouType, reportID, transactionID}, }, transaction, -}) { +}: IOURequestStepConfirmationProps) { + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - const [receiptFile, setReceiptFile] = useState(); - const receiptFilename = lodashGet(transaction, 'filename'); - const receiptPath = lodashGet(transaction, 'receipt.source'); - const receiptType = lodashGet(transaction, 'receipt.type'); - const foreignTaxDefault = lodashGet(policy, 'taxRates.foreignTaxDefault'); - const transactionTaxCode = transaction.taxRate ? transaction.taxRate.data.code : foreignTaxDefault; - const transactionTaxAmount = transaction.taxAmount; + const [receiptFile, setReceiptFile] = useState(); + + const receiptFilename = transaction?.filename; + const receiptPath = transaction?.receipt?.source; + const receiptType = transaction?.receipt?.type; + const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; + const transactionTaxCode = transaction?.taxRate ? transaction.taxRate.data?.code : foreignTaxDefault; + const transactionTaxAmount = transaction?.taxAmount; + const requestType = TransactionUtils.getRequestType(transaction); + const headerTitle = useMemo(() => { if (iouType === CONST.IOU.TYPE.SPLIT) { return translate('iou.split'); @@ -109,17 +94,17 @@ function IOURequestStepConfirmation({ const participants = useMemo( () => - _.map(transaction.participants, (participant) => { - const participantAccountID = lodashGet(participant, 'accountID', 0); + transaction?.participants?.map((participant) => { + const participantAccountID = participant.accountID ?? 0; return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }), - [transaction.participants, personalDetails], + [transaction?.participants, personalDetails], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); useEffect(() => { - if (!transaction || !transaction.originalCurrency) { + if (!transaction?.originalCurrency) { return; } // If user somehow lands on this page without the currency reset, then reset it here. @@ -128,32 +113,31 @@ function IOURequestStepConfirmation({ }, []); useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); + const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); + if (policyExpenseChat?.policyID) { + openDraftWorkspaceRequest(policyExpenseChat.policyID); } - }, [isOffline, participants, transaction.billable, policy, transactionID]); + }, [isOffline, participants, transaction?.billable, policy, transactionID]); - const defaultBillable = lodashGet(policy, 'defaultBillable', false); + const defaultBillable = !!policy?.defaultBillable; useEffect(() => { IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, defaultBillable); }, [transactionID, defaultBillable]); useEffect(() => { - if (!transaction.category) { + if (!transaction?.category) { return; } - if (policyCategories && policyCategories[transaction.category] && !policyCategories[transaction.category].enabled) { + if (policyCategories?.[transaction.category] && !policyCategories[transaction.category].enabled) { IOU.setMoneyRequestCategory(transactionID, ''); } - }, [policyCategories, transaction.category, transactionID]); - const defaultCategory = lodashGet( - _.find(lodashGet(policy, 'customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE), - 'defaultCategory', - '', - ); + }, [policyCategories, transaction?.category, transactionID]); + + const policyDistance = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + const defaultCategory = policyDistance?.defaultCategory ?? ''; + useEffect(() => { - if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) { + if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction?.category) { return; } IOU.setMoneyRequestCategory(transactionID, defaultCategory); @@ -164,7 +148,7 @@ function IOURequestStepConfirmation({ const navigateBack = useCallback(() => { // If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken // back to the participants step - if (!transaction.participantsAutoAssigned) { + if (!transaction?.participantsAutoAssigned) { Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } @@ -179,8 +163,8 @@ function IOURequestStepConfirmation({ // This is because until the request is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the request process. useEffect(() => { - const onSuccess = (file) => { - const receipt = file; + const onSuccess = (file: File) => { + const receipt: Receipt = file; receipt.state = file && requestType === CONST.IOU.REQUEST_TYPE.MANUAL ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; setReceiptFile(receipt); }; @@ -188,13 +172,12 @@ function IOURequestStepConfirmation({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ const requestMoney = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + if (!transaction) { + return; + } + IOU.requestMoney( report, transaction.amount, @@ -220,13 +203,11 @@ function IOURequestStepConfirmation({ [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], ); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receiptObj] - */ const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + if (!report || !transaction) { + return; + } IOU.trackExpense( report, transaction.amount, @@ -249,31 +230,14 @@ function IOURequestStepConfirmation({ gpsPoints, ); }, - [ - report, - transaction.amount, - transaction.currency, - transaction.created, - transaction.merchant, - transaction.category, - transaction.tag, - transaction.billable, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transactionTaxCode, - transactionTaxAmount, - policy, - policyTags, - policyCategories, - ], + [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, transactionTaxCode, transactionTaxAmount, policy, policyTags, policyCategories], ); - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - */ const createDistanceRequest = useCallback( - (selectedParticipants, trimmedComment) => { + (selectedParticipants: Participant[], trimmedComment: string) => { + if (!report || !transaction) { + return; + } IOU.createDistanceRequest( report, selectedParticipants[0], @@ -295,8 +259,8 @@ function IOURequestStepConfirmation({ ); const createTransaction = useCallback( - (selectedParticipants) => { - const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); + (selectedParticipants: Participant[]) => { + const trimmedComment = (transaction?.comment.comment ?? '').trim(); // Don't let the form be submitted multiple times while the navigator is waiting to take the user to a different page if (formHasBeenSubmitted.current) { @@ -307,63 +271,69 @@ function IOURequestStepConfirmation({ // If we have a receipt let's start the split bill by creating only the action, the transaction, and the group DM if needed if (iouType === CONST.IOU.TYPE.SPLIT && receiptFile) { - IOU.startSplitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - trimmedComment, - transaction.category, - transaction.tag, - transaction.currency, - receiptFile, - report.reportID, - transaction.billable, - ); + if (currentUserPersonalDetails.login && !!transaction) { + IOU.startSplitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + comment: trimmedComment, + receipt: receiptFile, + existingSplitChatReportID: report?.reportID, + billable: transaction.billable, + category: transaction.category, + tag: transaction.tag, + currency: transaction.currency, + }); + } return; } // IOUs created from a group report will have a reportID param in the route. // Since the user is already viewing the report, we don't need to navigate them to the report - if (iouType === CONST.IOU.TYPE.SPLIT && !transaction.isFromGlobalCreate) { - IOU.splitBill( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - report.reportID, - transaction.billable, - transaction.iouRequestType, - ); + if (iouType === CONST.IOU.TYPE.SPLIT && !transaction?.isFromGlobalCreate) { + if (currentUserPersonalDetails.login && !!transaction) { + IOU.splitBill({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + existingSplitChatReportID: report?.reportID, + billable: transaction.billable, + iouRequestType: transaction.iouRequestType, + }); + } return; } // If the request is created from the global create menu, we also navigate the user to the group report if (iouType === CONST.IOU.TYPE.SPLIT) { - IOU.splitBillAndOpenReport( - selectedParticipants, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - transaction.amount, - trimmedComment, - transaction.currency, - transaction.merchant, - transaction.created, - transaction.category, - transaction.tag, - transaction.billable, - transaction.iouRequestType, - ); + if (currentUserPersonalDetails.login && !!transaction) { + IOU.splitBillAndOpenReport({ + participants: selectedParticipants, + currentUserLogin: currentUserPersonalDetails.login, + currentUserAccountID: currentUserPersonalDetails.accountID, + amount: transaction.amount, + comment: trimmedComment, + currency: transaction.currency, + merchant: transaction.merchant, + created: transaction.created, + category: transaction.category, + tag: transaction.tag, + billable: !!transaction.billable, + iouRequestType: transaction.iouRequestType, + }); + } return; } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - if (receiptFile) { + if (receiptFile && transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { getCurrentPosition( @@ -397,7 +367,7 @@ function IOURequestStepConfirmation({ return; } - if (receiptFile) { + if (receiptFile && !!transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { getCurrentPosition( @@ -443,7 +413,7 @@ function IOURequestStepConfirmation({ requestMoney, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, - report.reportID, + report?.reportID, trackExpense, createDistanceRequest, ], @@ -455,12 +425,16 @@ function IOURequestStepConfirmation({ * @param {String} paymentMethodType */ const sendMoney = useCallback( - (paymentMethodType) => { - const currency = transaction.currency; + (paymentMethodType: ValueOf) => { + const currency = transaction?.currency; + + const trimmedComment = transaction?.comment?.comment ? transaction.comment.comment.trim() : ''; - const trimmedComment = transaction.comment && transaction.comment.comment ? transaction.comment.comment.trim() : ''; + const participant = participants?.[0]; - const participant = participants[0]; + if (!participant || !report || !transaction?.amount || !currency) { + return; + } if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { IOU.sendMoneyElsewhere(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); @@ -471,11 +445,11 @@ function IOURequestStepConfirmation({ IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); } }, - [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], + [transaction?.amount, transaction?.comment, transaction?.currency, participants, currentUserPersonalDetails.accountID, report], ); - const addNewParticipant = (option) => { - const newParticipants = _.map(transaction.participants, (participant) => { + const addNewParticipant = (option: Participant) => { + const newParticipants = transaction?.participants?.map((participant) => { if (participant.accountID === option.accountID) { return {...participant, selected: !participant.selected}; } @@ -484,16 +458,13 @@ function IOURequestStepConfirmation({ IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); }; - /** - * @param {Boolean} billable - */ - const setBillable = (billable) => { + const setBillable = (billable: boolean) => { IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, billable); }; // This loading indicator is shown because the transaction originalCurrency is being updated later than the component mounts. // To prevent the component from rendering with the wrong currency, we show a loading indicator until the correct currency is set. - const isLoading = !!(transaction && transaction.originalCurrency); + const isLoading = !!transaction?.originalCurrency; return ( {isLoading && } + {/* @ts-expect-error TODO: Remove this once MoneyRequestConfirmationList (https://github.com/Expensify/App/issues/36130) is migrated to TypeScript. */} @@ -556,23 +528,21 @@ function IOURequestStepConfirmation({ ); } -IOURequestStepConfirmation.propTypes = propTypes; -IOURequestStepConfirmation.defaultProps = defaultProps; IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; -export default compose( - withCurrentUserPersonalDetails, - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, - }, - policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, - }, - }), -)(IOURequestStepConfirmation); +const IOURequestStepConfirmationWithOnyx = withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + policyCategories: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + }, + policyTags: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + }, +})(IOURequestStepConfirmation); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepConfirmationWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepConfirmationWithOnyx); +/* eslint-disable rulesdir/no-negated-variables */ +const IOURequestStepConfirmationWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepConfirmationWithFullTransactionOrNotFound); +export default IOURequestStepConfirmationWithWritableReportOrNotFound; diff --git a/src/pages/iou/request/step/StepScreenDragAndDropWrapper.js b/src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx similarity index 79% rename from src/pages/iou/request/step/StepScreenDragAndDropWrapper.js rename to src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx index ceb0d5a4435..39ac12b6bdc 100644 --- a/src/pages/iou/request/step/StepScreenDragAndDropWrapper.js +++ b/src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx @@ -1,4 +1,4 @@ -import PropTypes from 'prop-types'; +import type {PropsWithChildren} from 'react'; import React, {useState} from 'react'; import {View} from 'react-native'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -7,31 +7,24 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -const propTypes = { - /** The things to display inside the screenwrapper */ - children: PropTypes.node.isRequired, - +type StepScreenDragAndDropWrapperProps = { /** The title to show in the header (should be translated already) */ - headerTitle: PropTypes.string.isRequired, + headerTitle: string; /** A function triggered when the back button is pressed */ - onBackButtonPress: PropTypes.func.isRequired, + onBackButtonPress: () => void; /** A function triggered when the entry transition is ended. Useful for auto-focusing elements. */ - onEntryTransitionEnd: PropTypes.func, + onEntryTransitionEnd?: () => void; /** Whether or not the wrapper should be shown (sometimes screens can be embedded inside another screen that already is using a wrapper) */ - shouldShowWrapper: PropTypes.bool.isRequired, + shouldShowWrapper: boolean; /** An ID used for unit testing */ - testID: PropTypes.string.isRequired, -}; - -const defaultProps = { - onEntryTransitionEnd: () => {}, + testID: string; }; -function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper}) { +function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper}: PropsWithChildren) { const styles = useThemeStyles(); const [isDraggingOver, setIsDraggingOver] = useState(false); @@ -65,7 +58,5 @@ function StepScreenDragAndDropWrapper({testID, headerTitle, onBackButtonPress, o } StepScreenDragAndDropWrapper.displayName = 'StepScreenDragAndDropWrapper'; -StepScreenDragAndDropWrapper.propTypes = propTypes; -StepScreenDragAndDropWrapper.defaultProps = defaultProps; export default StepScreenDragAndDropWrapper; diff --git a/src/pages/iou/request/step/StepScreenWrapper.js b/src/pages/iou/request/step/StepScreenWrapper.tsx similarity index 59% rename from src/pages/iou/request/step/StepScreenWrapper.js rename to src/pages/iou/request/step/StepScreenWrapper.tsx index 3739cbbcc18..e64f2792d2e 100644 --- a/src/pages/iou/request/step/StepScreenWrapper.js +++ b/src/pages/iou/request/step/StepScreenWrapper.tsx @@ -1,46 +1,46 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {PropsWithChildren} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import callOrReturn from '@src/types/utils/callOrReturn'; -const propTypes = { - /** The things to display inside the screenwrapper */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - +type StepScreenWrapperProps = { /** The title to show in the header (should be translated already) */ - headerTitle: PropTypes.string.isRequired, + headerTitle: string; /** A function triggered when the back button is pressed */ - onBackButtonPress: PropTypes.func.isRequired, + onBackButtonPress: () => void; /** A function triggered when the entry transition is ended. Useful for auto-focusing elements. */ - onEntryTransitionEnd: PropTypes.func, + onEntryTransitionEnd?: () => void; /** Whether or not the wrapper should be shown (sometimes screens can be embedded inside another screen that already is using a wrapper) */ - shouldShowWrapper: PropTypes.bool.isRequired, + shouldShowWrapper: boolean; /** Whether or not to display not found page */ - shouldShowNotFoundPage: PropTypes.bool, + shouldShowNotFoundPage?: boolean; /** An ID used for unit testing */ - testID: PropTypes.string.isRequired, + testID: string; /** Whether or not to include safe area padding */ - includeSafeAreaPaddingBottom: PropTypes.bool, -}; - -const defaultProps = { - onEntryTransitionEnd: () => {}, - includeSafeAreaPaddingBottom: false, - shouldShowNotFoundPage: false, + includeSafeAreaPaddingBottom?: boolean; }; -function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper, shouldShowNotFoundPage, includeSafeAreaPaddingBottom}) { +function StepScreenWrapper({ + testID, + headerTitle, + onBackButtonPress, + onEntryTransitionEnd, + children, + shouldShowWrapper, + shouldShowNotFoundPage, + includeSafeAreaPaddingBottom, +}: PropsWithChildren) { const styles = useThemeStyles(); if (!shouldShowWrapper) { @@ -62,14 +62,8 @@ function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTrans onBackButtonPress={onBackButtonPress} /> { - // If props.children is a function, call it to provide the insets to the children. - _.isFunction(children) - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children + // If props.children is a function, call it to provide the insets to the children + callOrReturn(children, {insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) } @@ -78,8 +72,4 @@ function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTrans ); } -StepScreenWrapper.displayName = 'StepScreenWrapper'; -StepScreenWrapper.propTypes = propTypes; -StepScreenWrapper.defaultProps = defaultProps; - export default StepScreenWrapper; diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 5f8a981ab3b..9fee88b45d0 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -21,6 +21,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT + | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION + | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE; type Route = RouteProp; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 00ebba2b56c..515f6f97f28 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -20,6 +20,8 @@ type WithWritableReportOrNotFoundOnyxProps = { type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT | typeof SCREENS.MONEY_REQUEST.STEP_DESCRIPTION + | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY + | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT; @@ -53,7 +55,7 @@ export default function , WithWritableReportOrNotFoundOnyxProps>({ report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID ?? '0'}`, + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID ?? '0'}`, }, })(forwardRef(WithWritableReportOrNotFound)); } diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 1750fa61e51..281b6b4228c 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -1,4 +1,5 @@ import type {KeysOfUnion, ValueOf} from 'type-fest'; +import type {IOURequestType} from '@libs/actions/IOU'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; @@ -133,7 +134,7 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< filename?: string; /** Used during the creation flow before the transaction is saved to the server */ - iouRequestType?: ValueOf; + iouRequestType?: IOURequestType; /** The original merchant name */ merchant: string; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index b1b597d4a6e..9f063c2be6c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1072,20 +1072,22 @@ describe('actions/IOU', () => { fetch.pause(); IOU.splitBill( // TODO: Migrate after the backend accepts accountIDs - [ - [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], - [JULES_EMAIL, String(JULES_ACCOUNT_ID)], - [VIT_EMAIL, String(VIT_ACCOUNT_ID)], - ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), - RORY_EMAIL, - RORY_ACCOUNT_ID, - amount, - comment, - CONST.CURRENCY.USD, - merchant, - '', - '', - '', + { + participants: [ + [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], + [JULES_EMAIL, String(JULES_ACCOUNT_ID)], + [VIT_EMAIL, String(VIT_ACCOUNT_ID)], + ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), + currentUserLogin: RORY_EMAIL, + currentUserAccountID: RORY_ACCOUNT_ID, + amount, + comment, + currency: CONST.CURRENCY.USD, + merchant, + created: '', + tag: '', + existingSplitChatReportID: '', + }, ); return waitForBatchedUpdates(); }) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index baefd1bd6d6..6333ee6f1bc 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -1213,22 +1213,22 @@ describe('OptionsListUtils', () => { Engineering: { enabled: false, name: 'Engineering', - accountID: null, + accountID: undefined, }, Medical: { enabled: true, name: 'Medical', - accountID: null, + accountID: undefined, }, Accounting: { enabled: true, name: 'Accounting', - accountID: null, + accountID: undefined, }, HR: { enabled: true, name: 'HR', - accountID: null, + accountID: undefined, }, }; const smallResultList: OptionsListUtils.CategorySection[] = [ @@ -1291,57 +1291,57 @@ describe('OptionsListUtils', () => { Engineering: { enabled: false, name: 'Engineering', - accountID: null, + accountID: undefined, }, Medical: { enabled: true, name: 'Medical', - accountID: null, + accountID: undefined, }, Accounting: { enabled: true, name: 'Accounting', - accountID: null, + accountID: undefined, }, HR: { enabled: true, name: 'HR', - accountID: null, + accountID: undefined, }, Food: { enabled: true, name: 'Food', - accountID: null, + accountID: undefined, }, Traveling: { enabled: false, name: 'Traveling', - accountID: null, + accountID: undefined, }, Cleaning: { enabled: true, name: 'Cleaning', - accountID: null, + accountID: undefined, }, Software: { enabled: true, name: 'Software', - accountID: null, + accountID: undefined, }, OfficeSupplies: { enabled: false, name: 'Office Supplies', - accountID: null, + accountID: undefined, }, Taxes: { enabled: true, name: 'Taxes', - accountID: null, + accountID: undefined, }, Benefits: { enabled: true, name: 'Benefits', - accountID: null, + accountID: undefined, }, }; const largeResultList: OptionsListUtils.CategorySection[] = [