diff --git a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md index 1e950bbf1a6..f2a2efca1f5 100644 --- a/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md +++ b/contributingGuides/HOW_TO_BECOME_A_CONTRIBUTOR_PLUS.md @@ -24,4 +24,4 @@ C+ are contributors who are experienced at working with Expensify and have gaine ## How to join? -Email contributors@expensify.com and include "C+ Team Application" in the subject line if you’re interested in joining. +Email contributors@expensify.com and include "C+ Team Application" in the subject line if you’re interested in joining. Please include your GitHub username and a link to the PRs you've authored that have been merged. ie. `https://github.com/Expensify/App/pulls?q=is%3Apr+author%3Aparasharrajat+is%3Amerged` diff --git a/package-lock.json b/package-lock.json index f0e3a21cea5..d6c5edbdd98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#f7efbd084536c140e65b49cd15f67ad8a2a10675", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1247a822328011083a13abc769ba1911c3586338", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -27495,8 +27495,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#f7efbd084536c140e65b49cd15f67ad8a2a10675", - "integrity": "sha512-aLrSTuLNmp9yNJUhik/Uia5KaNVBKp7m+DStcPoed4hSe+tMBDT8HMez7PqO5uRC6GPFrbBr77Aa2AQOB7DVqQ==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1247a822328011083a13abc769ba1911c3586338", + "integrity": "sha512-YqFwbL3B5XzdQ+4wmZqWcdAbRerGvP0ZvcJV2YTHETxlZdjc39AZ2gviaJhqmz8E4kTQIfgO6ii3n4bWSYKt5A==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 092aa4dabcc..507c58763b8 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#f7efbd084536c140e65b49cd15f67ad8a2a10675", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1247a822328011083a13abc769ba1911c3586338", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", diff --git a/src/CONST.ts b/src/CONST.ts index 0fa1c64be44..f4c7ecb5215 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -59,6 +59,9 @@ const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], + + // Note: Group and Self-DM excluded as these are not tied to a Workspace + WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e83b499de33..7050360c2e8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -281,10 +281,6 @@ const ROUTES = { route: ':iouType/new/participants/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const, }, - MONEY_REQUEST_CONFIRMATION: { - route: ':iouType/new/confirmation/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const, - }, MONEY_REQUEST_CURRENCY: { route: ':iouType/new/currency/:reportID?', getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const, @@ -307,8 +303,9 @@ const ROUTES = { `${action}/${iouType}/start/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_CONFIRMATION: { - route: 'create/:iouType/confirmation/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}` as const, + route: ':action/:iouType/confirmation/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string) => + `${action}/${iouType}/confirmation/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { route: ':action/:iouType/amount/:transactionID/:reportID', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6a14cb33349..c732594cdcb 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -150,7 +150,6 @@ const SCREENS = { STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', PARTICIPANTS: 'Money_Request_Participants', - CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 465a4f747bc..d918007e575 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -55,6 +55,9 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { ); } + const hasStrikethroughStyle = 'textDecorationLine' in parentStyle && parentStyle.textDecorationLine === 'line-through'; + const textDecorationLineStyle = hasStrikethroughStyle ? styles.underlineLineThrough : {}; + return ( Link.openLink(attrHref, environmentURL, isAttachment) : undefined} > - + { + if (props.childTnode.tagName === 'br') { + return {'\n'}; + } + if (props.childTnode.type === 'text') { + return ( + + {props.childTnode.data} + + ); + } + return props.childElement; + }} + /> ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 0327b6bc6f5..0c7c82534bb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -35,12 +35,12 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona const htmlAttributeAccountID = tnode.attributes.accountid; let accountID: number; - let displayNameOrLogin: string; + let mentionDisplayText: string; let navigationRoute: Route; const tnodeClone = cloneDeep(tnode); - const getMentionDisplayText = (displayText: string, userAccountID: string, userLogin = '') => { + const getShortMentionIfFound = (displayText: string, userAccountID: string, userLogin = '') => { // If the userAccountID does not exist, this is an email-based mention so the displayText must be an email. // If the userAccountID exists but userLogin is different from displayText, this means the displayText is either user display name, Hidden, or phone number, in which case we should return it as is. if (userAccountID && userLogin !== displayText) { @@ -59,18 +59,18 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona if (!isEmpty(htmlAttribAccountID)) { const user = personalDetails[htmlAttribAccountID]; accountID = parseInt(htmlAttribAccountID, 10); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - displayNameOrLogin = PersonalDetailsUtils.getDisplayNameOrDefault(user, LocalePhoneNumber.formatPhoneNumber(user?.login ?? '')); + mentionDisplayText = LocalePhoneNumber.formatPhoneNumber(user?.login ?? '') || PersonalDetailsUtils.getDisplayNameOrDefault(user); + mentionDisplayText = getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID, user?.login ?? ''); navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); } else if ('data' in tnodeClone && !isEmptyObject(tnodeClone.data)) { // We need to remove the LTR unicode and leading @ from data as it is not part of the login - displayNameOrLogin = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1); + mentionDisplayText = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1); // We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below - asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, Str.removeSMSDomain(getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID))); + asMutable(tnodeClone).data = tnodeClone.data.replace(mentionDisplayText, Str.removeSMSDomain(getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID))); - accountID = PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])?.[0]; - navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); - displayNameOrLogin = Str.removeSMSDomain(displayNameOrLogin); + accountID = PersonalDetailsUtils.getAccountIDsByLogins([mentionDisplayText])?.[0]; + navigationRoute = ROUTES.DETAILS.getRoute(mentionDisplayText); + mentionDisplayText = Str.removeSMSDomain(mentionDisplayText); } else { // If neither an account ID or email is provided, don't render anything return null; @@ -97,7 +97,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona - {htmlAttribAccountID ? `@${displayNameOrLogin}` : } + {htmlAttribAccountID ? `@${mentionDisplayText}` : } diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index c72cdd1fd89..90ccff47b2b 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -154,14 +154,14 @@ function OptionRow({ } return ( - - - {(hovered) => ( + + {(hovered) => ( + )} - )} - - + + )} + ); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 8b95474bf2f..2581359b454 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -127,6 +127,7 @@ function ReportPreview({ const isApproved = ReportUtils.isReportApproved(iouReport); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport); + const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => TransactionUtils.isPending(transaction) && TransactionUtils.isCardTransaction(transaction)).length; @@ -138,14 +139,16 @@ function ReportPreview({ const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions[0]) : null; + const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions[0]) : null; + if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; } const previewSubtitle = // Formatted merchant can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formattedMerchant || + (formattedMerchant ?? formattedDescription) || translate('iou.requestCount', { count: numberOfRequests - numberOfScanningReceipts - numberOfPendingRequests, scanningReceipts: numberOfScanningReceipts, @@ -222,13 +225,14 @@ function ReportPreview({ /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; - - There is only one money request, it has a receipt and is not being smart scanned – in this case, the request merchant is shown; + - There is only one money request, it has a receipt and is not being smart scanned – in this case, the request merchant or description is shown; * There is an edge case when there is only one distance request with a pending route and amount = 0. - In this case, we don't want to show the merchant because it says: "Pending route...", which is already displayed in the amount field. + In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field. */ - const shouldShowSingleRequestMerchant = numberOfRequests === 1 && !!formattedMerchant && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); - const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchant || numberOfRequests > 1); + const shouldShowSingleRequestMerchantOrDescription = + numberOfRequests === 1 && (!!formattedMerchant || !!formattedDescription) && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); + const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchantOrDescription || numberOfRequests > 1); return ( { + if (isGroupChat) { + return; + } + if (icon.type === CONST.ICON_TYPE_WORKSPACE) { Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID)); return; diff --git a/src/components/VideoPopoverMenu/index.tsx b/src/components/VideoPopoverMenu/index.tsx index aad6364f91d..23f3447cf49 100644 --- a/src/components/VideoPopoverMenu/index.tsx +++ b/src/components/VideoPopoverMenu/index.tsx @@ -35,7 +35,6 @@ function VideoPopoverMenu({ anchorPosition={anchorPosition} menuItems={menuItems} anchorRef={videoPlayerMenuRef} - withoutOverlay /> ); } diff --git a/src/components/withNavigationTransitionEnd.tsx b/src/components/withNavigationTransitionEnd.tsx new file mode 100644 index 00000000000..417d8828c1e --- /dev/null +++ b/src/components/withNavigationTransitionEnd.tsx @@ -0,0 +1,39 @@ +import {useNavigation} from '@react-navigation/native'; +import type {StackNavigationProp} from '@react-navigation/stack'; +import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; +import React, {useEffect, useState} from 'react'; +import getComponentDisplayName from '@libs/getComponentDisplayName'; +import type {RootStackParamList} from '@libs/Navigation/types'; + +type WithNavigationTransitionEndProps = {didScreenTransitionEnd: boolean}; + +export default function (WrappedComponent: ComponentType>): React.ComponentType> { + function WithNavigationTransitionEnd(props: TProps, ref: ForwardedRef) { + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const navigation = useNavigation>(); + + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { + setDidScreenTransitionEnd(true); + }); + + return unsubscribeTransitionEnd; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); + } + + WithNavigationTransitionEnd.displayName = `WithNavigationTransitionEnd(${getComponentDisplayName(WrappedComponent)})`; + + return React.forwardRef(WithNavigationTransitionEnd); +} + +export type {WithNavigationTransitionEndProps}; diff --git a/src/languages/en.ts b/src/languages/en.ts index d30b62fc50b..31f98b63774 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -518,7 +518,7 @@ export default { asCopilot: 'as copilot for', }, mentionSuggestions: { - hereAlternateText: 'Notify everyone online in this room', + hereAlternateText: 'Notify everyone in this conversation', }, newMessages: 'New messages', reportTypingIndicator: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 30e4717bdf5..43380221232 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -514,7 +514,7 @@ export default { asCopilot: 'como copiloto de', }, mentionSuggestions: { - hereAlternateText: 'Notificar a todos los que estén en linea de esta sala', + hereAlternateText: 'Notificar a todos en esta conversación', }, newMessages: 'Mensajes nuevos', reportTypingIndicator: { diff --git a/src/libs/API/parameters/StartSplitBillParams.ts b/src/libs/API/parameters/StartSplitBillParams.ts index 30d21697ac6..623f291eb69 100644 --- a/src/libs/API/parameters/StartSplitBillParams.ts +++ b/src/libs/API/parameters/StartSplitBillParams.ts @@ -12,6 +12,7 @@ type StartSplitBillParams = { isFromGroupDM: boolean; createdReportActionID?: string; billable: boolean; + chatType?: string; }; export default StartSplitBillParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index a85a5d18eb1..55c58290b1c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -87,7 +87,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepTag').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../../pages/iou/HoldReasonPage').default as React.ComponentType, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index a6b6899aaf5..f272ae24973 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -525,7 +525,6 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, - [SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route, [SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route, [SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 356ed2e758d..dac3a98f4d7 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -347,8 +347,10 @@ type MoneyRequestNavigatorParamList = { iouType: string; reportID: string; }; - [SCREENS.MONEY_REQUEST.CONFIRMATION]: { + [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: { + action: ValueOf; iouType: string; + transactionID: string; reportID: string; }; [SCREENS.MONEY_REQUEST.CURRENCY]: { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 6f871bea7ab..0a8437a5afa 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -8,7 +8,9 @@ import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, Polic import type {PolicyFeatureName, Rate} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import Navigation from './Navigation/Navigation'; +import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; +import Navigation, {navigationRef} from './Navigation/Navigation'; +import type {RootStackParamList, State} from './Navigation/types'; type MemberEmailsToAccountIDs = Record; @@ -304,6 +306,13 @@ function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, feature return Boolean(policy?.[featureName]); } +/** + * Get the currently selected policy ID stored in the navigation state. + */ +function getPolicyIDFromNavigationState() { + return getPolicyIDFromState(navigationRef.getRootState() as State); +} + export { getActivePolicies, hasAccountingConnections, @@ -340,6 +349,7 @@ export { hasTaxRateError, getTaxByID, hasPolicyCategoriesError, + getPolicyIDFromNavigationState, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9fa28535a7a..a2bf892b96b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1193,7 +1193,8 @@ function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): * If the report is a thread and has a chat type set, it is a workspace chat. */ function isWorkspaceThread(report: OnyxEntry): boolean { - return isThread(report) && isChatReport(report) && !!getChatType(report); + const chatType = getChatType(report); + return isThread(report) && isChatReport(report) && CONST.WORKSPACE_ROOM_TYPES.some((type) => chatType === type); } /** diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 9296a81e306..83f3b2fcc15 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import type {Phrase, PhraseParameters} from '@libs/Localize'; import {getSortedTagKeys} from '@libs/PolicyUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -121,6 +122,15 @@ const ViolationsUtils = { policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): OnyxUpdate { + const isPartialTransaction = TransactionUtils.isPartialMerchant(TransactionUtils.getMerchant(updatedTransaction)) && TransactionUtils.isAmountMissing(updatedTransaction); + if (isPartialTransaction) { + return { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${updatedTransaction.transactionID}`, + value: transactionViolations, + }; + } + let newTransactionViolations = [...transactionViolations]; // Calculate client-side category violations diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a4f1efeeb8c..8393982c6b3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1,5 +1,4 @@ import type {ParamListBase, StackNavigationState} from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; import {format} from 'date-fns'; import fastMerge from 'expensify-common/lib/fastMerge'; import Str from 'expensify-common/lib/str'; @@ -45,11 +44,10 @@ import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUR import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; -import type {MoneyRequestNavigatorParamList, NavigationPartialRoute} from '@navigation/types'; +import type {NavigationPartialRoute} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant, Split} from '@src/types/onyx/IOU'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; @@ -63,8 +61,6 @@ import * as CachedPDFPaths from './CachedPDFPaths'; import * as Policy from './Policy'; import * as Report from './Report'; -type MoneyRequestRoute = StackScreenProps['route']; - type IOURequestType = ValueOf; type OneOnOneIOUReport = OnyxTypes.Report | undefined | null; @@ -2249,6 +2245,50 @@ function trackExpense( Report.notifyNewAction(activeReportID, payeeAccountID); } +function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, participants: Participant[], participantAccountIDs: number[], currentUserAccountID: number) { + // The existing chat report could be passed as reportID or exist on the sole "participant" (in this case a report option) + const existingChatReportID = existingSplitChatReportID || participants[0].reportID; + + // Check if the report is available locally if we do have one + let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; + + // If we do not have one locally then we will search for a chat with the same participants (only for 1:1 chats). + const shouldGetOrCreateOneOneDM = participants.length < 2; + if (!existingSplitChatReport && shouldGetOrCreateOneOneDM) { + existingSplitChatReport = ReportUtils.getChatByParticipants(participantAccountIDs); + } + + // We found an existing chat report we are done... + if (existingSplitChatReport) { + // Yes, these are the same, but give the caller a way to identify if we created a new report or not + return {existingSplitChatReport, splitChatReport: existingSplitChatReport}; + } + + // No existing chat by this point we need to create it + const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; + + // Create a Group Chat if we have multiple participants + if (participants.length > 1) { + const splitChatReport = ReportUtils.buildOptimisticChatReport( + allParticipantsAccountIDs, + '', + CONST.REPORT.CHAT_TYPE.GROUP, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + ); + return {existingSplitChatReport: null, splitChatReport}; + } + + // Otherwise, create a new 1:1 chat report + const splitChatReport = ReportUtils.buildOptimisticChatReport(allParticipantsAccountIDs); + return {existingSplitChatReport: null, splitChatReport}; +} + /** * Build the Onyx data and IOU split necessary for splitting a bill with 3+ users. * 1. Build the optimistic Onyx data for the group chat, i.e. chatReport and iouReportAction creating the former if it doesn't yet exist. @@ -2281,31 +2321,7 @@ function createSplitsAndOnyxData( const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); - const existingChatReportID = existingSplitChatReportID || participants[0].reportID; - let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; - if (!existingSplitChatReport) { - existingSplitChatReport = participants.length < 2 ? ReportUtils.getChatByParticipants(participantAccountIDs) : null; - } - let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; - const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; - if (!existingSplitChatReport && participants.length > 1) { - newChat = ReportUtils.buildOptimisticChatReport( - allParticipantsAccountIDs, - '', - CONST.REPORT.CHAT_TYPE.GROUP, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - ); - } - if (isEmptyObject(newChat)) { - newChat = ReportUtils.buildOptimisticChatReport(allParticipantsAccountIDs); - } - const splitChatReport = existingSplitChatReport ?? newChat; + const {splitChatReport, existingSplitChatReport} = getOrCreateOptimisticSplitChatReport(existingSplitChatReportID, participants, participantAccountIDs, currentUserAccountID); const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat; const splitTransaction = TransactionUtils.buildOptimisticTransaction( @@ -2786,11 +2802,7 @@ function startSplitBill( ) { const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); - const existingSplitChatReport = - existingSplitChatReportID || participants[0].reportID - ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingSplitChatReportID || participants[0].reportID}`] - : ReportUtils.getChatByParticipants(participantAccountIDs); - const splitChatReport = existingSplitChatReport ?? ReportUtils.buildOptimisticChatReport(participantAccountIDs); + const {splitChatReport, existingSplitChatReport} = getOrCreateOptimisticSplitChatReport(existingSplitChatReportID, participants, participantAccountIDs, currentUserAccountID); const isOwnPolicyExpenseChat = !!splitChatReport.isOwnPolicyExpenseChat; const {name: filename, source, state = CONST.IOU.RECEIPT_STATE.SCANREADY} = receipt; @@ -3048,6 +3060,7 @@ function startSplitBill( isFromGroupDM: !existingSplitChatReport, billable, ...(existingSplitChatReport ? {} : {createdReportActionID: splitChatCreatedReportAction.reportActionID}), + chatType: splitChatReport?.chatType, }; API.write(WRITE_COMMANDS.START_SPLIT_BILL, parameters, {optimisticData, successData, failureData}); @@ -5051,17 +5064,48 @@ function setShownHoldUseExplanation() { Onyx.set(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, true); } -/** - * When the money request or split bill creation flow is initialized via FAB, the reportID is not passed as a navigation - * parameter. - * Gets a report id from the first participant of the IOU object stored in Onyx. - */ -function getIOUReportID(iou?: OnyxTypes.IOU, route?: MoneyRequestRoute): string { - // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return route?.params.reportID || iou?.participants?.[0]?.reportID || ''; +<<<<<<< HEAD +======= +/** Navigates to the next IOU page based on where the IOU request was started */ +function navigateToNextPage(iou: OnyxEntry, iouType: string, report?: OnyxEntry, path = '') { + const moneyRequestID = `${iouType}${report?.reportID ?? ''}`; + const shouldReset = iou?.id !== moneyRequestID && !!report?.reportID; + + // If the money request ID in Onyx does not match the ID from params, we want to start a new request + // with the ID from params. We need to clear the participants in case the new request is initiated from FAB. + if (shouldReset) { + resetMoneyRequestInfo(moneyRequestID); + } + + // If we're adding a receipt, that means the user came from the confirmation page and we need to navigate back to it. + if (path.slice(1) === ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, report?.reportID)) { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType as ValueOf, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report?.reportID ?? '1'), + ); + return; + } + + // If a request is initiated on a report, skip the participants selection step and navigate to the confirmation page. + if (report?.reportID) { + // If the report is iou or expense report, we should get the chat report to set participant for request money + const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; + // Reinitialize the participants when the money request ID in Onyx does not match the ID from params + if (!iou?.participants?.length || shouldReset) { + const currentUserAccountID = currentUserPersonalDetails.accountID; + const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) + ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}] + : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + setMoneyRequestParticipants(participants); + } + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType as ValueOf, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID), + ); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType)); } +>>>>>>> main /** * Put money request on HOLD */ @@ -5265,7 +5309,6 @@ export { updateMoneyRequestDescription, replaceReceipt, detachReceipt, - getIOUReportID, editMoneyRequest, putOnHold, unholdRequest, diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 0c6bd7682ad..3c34e823ac9 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -780,15 +780,12 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: numbe } if (announceReport?.participantAccountIDs) { - const remainUsers = announceReport.participantAccountIDs.filter((e) => !accountIDs.includes(e)); const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, announceReport?.pendingChatMembers ?? [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); announceRoomMembers.onyxOptimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, value: { - participantAccountIDs: [...remainUsers], - visibleChatMemberAccountIDs: [...remainUsers], pendingChatMembers, }, }); @@ -797,8 +794,6 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: numbe onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, value: { - participantAccountIDs: announceReport.participantAccountIDs, - visibleChatMemberAccountIDs: announceReport.visibleChatMemberAccountIDs, pendingChatMembers: announceReport?.pendingChatMembers ?? null, }, }); diff --git a/src/pages/EditRequestMerchantPage.js b/src/pages/EditRequestMerchantPage.js deleted file mode 100644 index 33c04df39e3..00000000000 --- a/src/pages/EditRequestMerchantPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useRef} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; - -const propTypes = { - /** Transaction default merchant value */ - defaultMerchant: PropTypes.string.isRequired, - - /** Callback to fire when the Save button is pressed */ - onSubmit: PropTypes.func.isRequired, - - /** Boolean to enable validation */ - isPolicyExpenseChat: PropTypes.bool, -}; - -const defaultProps = { - isPolicyExpenseChat: false, -}; - -function EditRequestMerchantPage({defaultMerchant, onSubmit, isPolicyExpenseChat}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const merchantInputRef = useRef(null); - const isEmptyMerchant = defaultMerchant === '' || defaultMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - - const validate = useCallback( - (value) => { - const errors = {}; - if (_.isEmpty(value.merchant) && value.merchant.trim() === '' && isPolicyExpenseChat) { - errors.merchant = 'common.error.fieldRequired'; - } - return errors; - }, - [isPolicyExpenseChat], - ); - - return ( - merchantInputRef.current && merchantInputRef.current.focus()} - testID={EditRequestMerchantPage.displayName} - > - - - - (merchantInputRef.current = e)} - /> - - - - ); -} - -EditRequestMerchantPage.propTypes = propTypes; -EditRequestMerchantPage.defaultProps = defaultProps; -EditRequestMerchantPage.displayName = 'EditRequestMerchantPage'; - -export default EditRequestMerchantPage; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index f06b40af885..9093bf32b9d 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -218,6 +218,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD )} diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 66ff4e0602b..0e3a6a83494 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -50,7 +50,7 @@ const getAllParticipants = ( !!userPersonalDetail?.login && !CONST.RESTRICTED_ACCOUNT_IDS.includes(accountID) ? LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login) : translate('common.hidden'); const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail); - const pendingChatMember = report?.pendingChatMembers?.find((member) => member.accountID === accountID.toString()); + const pendingChatMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); return { alternateText: userLogin, pendingAction: pendingChatMember?.pendingAction, diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index bb3e928cb47..77b5c48d8a7 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -1,5 +1,3 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; @@ -13,12 +11,13 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; +import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; +import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; @@ -39,19 +38,17 @@ type RoomInvitePageOnyxProps = { personalDetails: OnyxEntry; }; -type RoomInvitePageProps = RoomInvitePageOnyxProps & WithReportOrNotFoundProps; +type RoomInvitePageProps = RoomInvitePageOnyxProps & WithReportOrNotFoundProps & WithNavigationTransitionEndProps; type Sections = Array>>; -function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePageProps) { +function RoomInvitePage({betas, personalDetails, report, policies, didScreenTransitionEnd}: RoomInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation: StackNavigationProp = useNavigation(); useEffect(() => { setSearchTerm(SearchInputManager.searchInput); @@ -88,18 +85,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [personalDetails, betas, searchTerm, excludedUsers]); - useEffect(() => { - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - setDidScreenTransitionEnd(true); - }); - - return () => { - unsubscribeTransitionEnd(); - }; - // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const sections = useMemo(() => { const sectionsArr: Sections = []; @@ -260,10 +245,12 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa RoomInvitePage.displayName = 'RoomInvitePage'; -export default withReportOrNotFound()( - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - })(RoomInvitePage), +export default withNavigationTransitionEnd( + withReportOrNotFound()( + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + })(RoomInvitePage), + ), ); diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index b64404f8813..d025a3bde26 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -198,7 +198,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { return; } } - const pendingChatMember = report?.pendingChatMembers?.find((member) => member.accountID === accountID.toString()); + const pendingChatMember = report?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); result.push({ keyForList: String(accountID), diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 2c2d28a0edb..d34f821b4b9 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -1,11 +1,11 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; import ScreenWrapper from '@components/ScreenWrapper'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import Performance from '@libs/Performance'; +import {getPolicyIDFromNavigationState} from '@libs/PolicyUtils'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -21,7 +21,7 @@ const startTimer = () => { function BaseSidebarScreen(props) { const styles = useThemeStyles(); - const {activeWorkspaceID} = useActiveWorkspace(); + const activeWorkspaceID = getPolicyIDFromNavigationState(); useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); Timing.start(CONST.TIMING.SIDEBAR_LOADED, true); diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js deleted file mode 100644 index f079bde6c28..00000000000 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ /dev/null @@ -1,140 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapperWithRef from '@components/Form/InputWrapper'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; -import {iouDefaultProps, iouPropTypes} from './propTypes'; - -const propTypes = { - /** Onyx Props */ - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - - /** Which field we are editing */ - field: PropTypes.string, - - /** reportID for the "transaction thread" */ - threadReportID: PropTypes.string, - }), - }).isRequired, -}; - -const defaultProps = { - iou: iouDefaultProps, -}; - -function MoneyRequestMerchantPage({iou, route}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {inputCallbackRef} = useAutoFocusInput(); - const iouType = lodashGet(route, 'params.iouType', ''); - const reportID = lodashGet(route, 'params.reportID', ''); - const isEmptyMerchant = iou.merchant === '' || iou.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - - useEffect(() => { - const moneyRequestId = `${iouType}${reportID}`; - const shouldReset = iou.id !== moneyRequestId; - if (shouldReset) { - IOU.resetMoneyRequestInfo(moneyRequestId); - } - - if (_.isEmpty(iou.participants) || (iou.amount === 0 && !iou.receiptPath) || shouldReset) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); - } - }, [iou.id, iou.participants, iou.amount, iou.receiptPath, iouType, reportID]); - - function navigateBack() { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); - } - - const validate = useCallback((value) => { - const errors = {}; - - if (_.isEmpty(value.moneyRequestMerchant)) { - errors.moneyRequestMerchant = 'common.error.fieldRequired'; - } - - return errors; - }, []); - - /** - * Sets the money request comment by saving it to Onyx. - * - * @param {Object} value - * @param {String} value.moneyRequestMerchant - */ - function updateMerchant(value) { - IOU.setMoneyRequestMerchant(value.moneyRequestMerchant); - navigateBack(); - } - - return ( - - navigateBack()} - /> - updateMerchant(value)} - validate={validate} - submitButtonText={translate('common.save')} - enabledWhenOffline - > - - - - - - ); -} - -MoneyRequestMerchantPage.propTypes = propTypes; -MoneyRequestMerchantPage.defaultProps = defaultProps; -MoneyRequestMerchantPage.displayName = 'MoneyRequestMerchantPage'; - -export default withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, -})(MoneyRequestMerchantPage); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index b0e013d85f1..9465c7e3eda 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -174,7 +174,7 @@ function IOURequestStepAmount({ // to the confirm step. if (report.reportID) { IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 435121a7602..61ae0d2b67b 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -9,6 +9,7 @@ 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'; @@ -26,7 +27,6 @@ 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 personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; @@ -46,9 +46,6 @@ const propTypes = { /** The personal details of the current user */ ...withCurrentUserPersonalDetailsPropTypes, - /** Personal details of all users */ - personalDetails: personalDetailsPropType, - /** The policy of the report */ ...policyPropTypes, @@ -75,7 +72,6 @@ const defaultProps = { }; function IOURequestStepConfirmation({ currentUserPersonalDetails, - personalDetails, policy, policyTags, policyCategories, @@ -89,6 +85,7 @@ function IOURequestStepConfirmation({ 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'); @@ -565,12 +562,6 @@ export default compose( withCurrentUserPersonalDetails, withWritableReportOrNotFound, withFullTransactionOrNotFound, - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.js b/src/pages/iou/request/step/IOURequestStepCurrency.js index c8717acd04a..51dba5858cb 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.js +++ b/src/pages/iou/request/step/IOURequestStepCurrency.js @@ -70,7 +70,10 @@ function IOURequestStepCurrency({ // are only able to handle one backTo param at a time and the user needs to go back to the amount page before going back // to the confirmation page if (pageIndex === 'confirm') { - const routeToAmountPageWithConfirmationAsBackTo = getUrlWithBackToParam(backTo, `/${ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)}`); + const routeToAmountPageWithConfirmationAsBackTo = getUrlWithBackToParam( + backTo, + `/${ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)}`, + ); Navigation.goBack(routeToAmountPageWithConfirmationAsBackTo); return; } diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 94b59970b9f..a2ed73a43be 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -135,7 +135,7 @@ function IOURequestStepDistance({ // to the confirm step. if (report.reportID) { IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 5ca465d8fb7..7ccbdb18ee0 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -128,7 +128,7 @@ function IOURequestStepParticipants({ IOU.setMoneyRequestTag(transactionID, ''); IOU.setMoneyRequestCategory(transactionID, ''); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(nextStepIOUType, transactionID, selectedReportID.current || reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, nextStepIOUType, transactionID, selectedReportID.current || reportID)); }, [iouType, transactionID, reportID], ); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 49c5f0a782f..056f68385dc 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -214,7 +214,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); const updateScanAndNavigate = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 1f72777f035..03f9c6c7969 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -188,7 +188,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); const updateScanAndNavigate = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index a45263d8bee..fb984cb801c 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -135,7 +135,7 @@ function IOURequestStepTaxAmountPage({ if (report.reportID) { // TODO: Is this really needed at all? IOU.setMoneyRequestParticipantsFromReport(transactionID, report); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js deleted file mode 100644 index 1738ac78df4..00000000000 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ /dev/null @@ -1,473 +0,0 @@ -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 {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import categoryPropTypes from '@components/categoryPropTypes'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; -import {usePersonalDetails} from '@components/OnyxProvider'; -import ScreenWrapper from '@components/ScreenWrapper'; -import tagPropTypes from '@components/tagPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; -import useInitialValue from '@hooks/useInitialValue'; -import useNetwork from '@hooks/useNetwork'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import {policyDefaultProps, 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'; - -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, - - report: reportPropTypes, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The policy of the current request */ - policy: policyPropTypes, - - policyTags: tagPropTypes, - - policyCategories: PropTypes.objectOf(categoryPropTypes), - - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - report: {}, - policyCategories: {}, - policyTags: {}, - iou: iouDefaultProps, - policy: policyDefaultProps, - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function MoneyRequestConfirmPage(props) { - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - const {windowWidth} = useWindowDimensions(); - const prevMoneyRequestId = useRef(props.iou.id); - const iouType = useInitialValue(() => lodashGet(props.route, 'params.iouType', '')); - const reportID = useInitialValue(() => lodashGet(props.route, 'params.reportID', '')); - const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, props.selectedTab); - const isScanRequest = MoneyRequestUtils.isScanRequest(props.selectedTab); - const [receiptFile, setReceiptFile] = useState(); - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; - - const participants = useMemo( - () => - _.map(props.iou.participants, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); - }), - [props.iou.participants, personalDetails], - ); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); - const isManualRequestDM = props.selectedTab === CONST.TAB_REQUEST.MANUAL && iouType === CONST.IOU.TYPE.REQUEST; - - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, props.iou.billable, props.policy]); - - const defaultBillable = lodashGet(props.policy, 'defaultBillable', false); - useEffect(() => { - IOU.setMoneyRequestBillable(defaultBillable); - }, [defaultBillable, isOffline]); - - useEffect(() => { - if (!props.iou.receiptPath || !props.iou.receiptFilename) { - return; - } - const onSuccess = (file) => { - const receipt = file; - receipt.state = file && isManualRequestDM ? CONST.IOU.RECEIPT_STATE.OPEN : CONST.IOU.RECEIPT_STATE.SCANREADY; - setReceiptFile(receipt); - }; - const onFailure = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); - }; - FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess, onFailure); - }, [props.iou.receiptPath, props.iou.receiptFilename, isManualRequestDM, iouType, reportID]); - - useEffect(() => { - // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (!isDistanceRequest && prevMoneyRequestId.current !== props.iou.id) { - // The ID is cleared on completing a request. In that case, we will do nothing. - if (props.iou.id) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); - } - return; - } - - // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType}${reportID}`; - const shouldReset = !isDistanceRequest && props.iou.id !== moneyRequestId && !_.isEmpty(reportID); - if (shouldReset) { - IOU.resetMoneyRequestInfo(moneyRequestId); - } - - if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset || ReportUtils.isArchivedRoom(props.report)) { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), true); - } - - return () => { - prevMoneyRequestId.current = props.iou.id; - }; - }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report, iouType, reportID]); - - const navigateBack = () => { - let fallback; - if (reportID) { - fallback = ROUTES.MONEY_REQUEST.getRoute(iouType, reportID); - } else { - fallback = ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(iouType); - } - Navigation.goBack(fallback); - }; - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - * @param {File} [receipt] - */ - const requestMoney = useCallback( - (selectedParticipants, trimmedComment, receipt) => { - IOU.requestMoney( - props.report, - props.iou.amount, - props.iou.currency, - props.iou.created, - props.iou.merchant, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - selectedParticipants[0], - trimmedComment, - receipt, - props.iou.category, - props.iou.tag, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ); - }, - [ - props.report, - props.iou.amount, - props.iou.currency, - props.iou.created, - props.iou.merchant, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.category, - props.iou.tag, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ], - ); - - /** - * @param {Array} selectedParticipants - * @param {String} trimmedComment - */ - const createDistanceRequest = useCallback( - (selectedParticipants, trimmedComment) => { - IOU.createDistanceRequest( - props.report, - selectedParticipants[0], - trimmedComment, - props.iou.created, - props.iou.transactionID, - props.iou.category, - props.iou.tag, - props.iou.amount, - props.iou.currency, - props.iou.merchant, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ); - }, - [ - props.report, - props.iou.created, - props.iou.transactionID, - props.iou.category, - props.iou.tag, - props.iou.amount, - props.iou.currency, - props.iou.merchant, - props.iou.billable, - props.policy, - props.policyTags, - props.policyCategories, - ], - ); - - const createTransaction = useCallback( - (selectedParticipants) => { - const trimmedComment = props.iou.comment.trim(); - - // 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 && props.iou.receiptPath) { - const existingSplitChatReportID = CONST.REGEX.NUMBER.test(reportID) ? reportID : ''; - const onSuccess = (receipt) => { - IOU.startSplitBill( - selectedParticipants, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - trimmedComment, - receipt, - existingSplitChatReportID, - ); - }; - FileUtils.readFileAsync(props.iou.receiptPath, props.iou.receiptFilename, onSuccess); - 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 && CONST.REGEX.NUMBER.test(reportID)) { - IOU.splitBill( - selectedParticipants, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.amount, - trimmedComment, - props.iou.currency, - props.iou.category, - props.iou.tag, - reportID, - props.iou.merchant, - ); - 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, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.amount, - trimmedComment, - props.iou.currency, - props.iou.category, - props.iou.tag, - props.iou.merchant, - ); - return; - } - - if (receiptFile) { - requestMoney(selectedParticipants, trimmedComment, receiptFile); - return; - } - - if (isDistanceRequest) { - createDistanceRequest(selectedParticipants, trimmedComment); - return; - } - - requestMoney(selectedParticipants, trimmedComment); - }, - [ - props.iou.amount, - props.iou.comment, - props.currentUserPersonalDetails.login, - props.currentUserPersonalDetails.accountID, - props.iou.currency, - props.iou.category, - props.iou.tag, - props.iou.receiptPath, - props.iou.receiptFilename, - isDistanceRequest, - requestMoney, - createDistanceRequest, - receiptFile, - iouType, - reportID, - props.iou.merchant, - ], - ); - - /** - * Checks if user has a GOLD wallet then creates a paid IOU report on the fly - * - * @param {String} paymentMethodType - */ - const sendMoney = useCallback( - (paymentMethodType) => { - const currency = props.iou.currency; - const trimmedComment = props.iou.comment.trim(); - const participant = participants[0]; - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { - IOU.sendMoneyElsewhere(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - return; - } - - if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - IOU.sendMoneyWithWallet(props.report, props.iou.amount, currency, trimmedComment, props.currentUserPersonalDetails.accountID, participant); - } - }, - [props.iou.amount, props.iou.comment, participants, props.iou.currency, props.currentUserPersonalDetails.accountID, props.report], - ); - - const headerTitle = () => { - if (isDistanceRequest) { - return props.translate('common.distance'); - } - - if (iouType === CONST.IOU.TYPE.SPLIT) { - return props.translate('iou.split'); - } - - if (iouType === CONST.IOU.TYPE.SEND) { - return props.translate('common.send'); - } - - if (isScanRequest) { - return props.translate('tabSelector.scan'); - } - - return props.translate('tabSelector.manual'); - }; - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT.getRoute(iouType, reportID)), - }, - ]} - /> - { - const newParticipants = _.map(props.iou.participants, (participant) => { - if (participant.accountID === option.accountID) { - return {...participant, selected: !participant.selected}; - } - return participant; - }); - IOU.setMoneyRequestParticipants(newParticipants); - }} - receiptPath={props.iou.receiptPath} - receiptFilename={props.iou.receiptFilename} - iouType={iouType} - reportID={reportID} - isPolicyExpenseChat={isPolicyExpenseChat} - // The participants can only be modified when the action is initiated from directly within a group chat and not the floating-action-button. - // This is because when there is a group of people, say they are on a trip, and you have some shared expenses with some of the people, - // but not all of them (maybe someone skipped out on dinner). Then it's nice to be able to select/deselect people from the group chat bill - // split rather than forcing the user to create a new group, just for that expense. The reportID is empty, when the action was initiated from - // the floating-action-button (since it is something that exists outside the context of a report). - canModifyParticipants={!_.isEmpty(reportID)} - policyID={props.report.policyID} - bankAccountRoute={ReportUtils.getBankAccountRoute(props.report)} - iouMerchant={props.iou.merchant} - iouCreated={props.iou.created} - isScanRequest={isScanRequest} - isDistanceRequest={isDistanceRequest} - shouldShowSmartScanFields={_.isEmpty(props.iou.receiptPath)} - /> - - )} - - ); -} - -MoneyRequestConfirmPage.displayName = 'MoneyRequestConfirmPage'; -MoneyRequestConfirmPage.propTypes = propTypes; -MoneyRequestConfirmPage.defaultProps = defaultProps; - -export default compose( - withCurrentUserPersonalDetails, - withLocalize, - withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - report: { - key: ({route, iou}) => { - const reportID = IOU.getIOUReportID(iou, route); - - return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - }, - }, - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - }), - 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'}`, - }, - }), -)(MoneyRequestConfirmPage); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index fc522816b4c..b5d11faac6c 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -87,7 +87,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { const navigateToConfirmationStep = (moneyRequestType) => { IOU.setMoneyRequestId(moneyRequestType); - Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, moneyRequestType, lodashGet(transaction, 'transactionID', 1), reportID)); }; const navigateBack = useCallback((forceFallback = false) => { diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 0a922321766..bb199ddc905 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -14,6 +14,8 @@ import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; +import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; @@ -41,7 +43,7 @@ type UseOptions = { reports: OnyxCollection; }; -type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps; +type TaskAssigneeSelectorModalProps = TaskAssigneeSelectorModalOnyxProps & WithCurrentUserPersonalDetailsProps & WithNavigationTransitionEndProps; function useOptions({reports}: UseOptions) { const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; @@ -90,7 +92,7 @@ function useOptions({reports}: UseOptions) { return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue}; } -function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalProps) { +function TaskAssigneeSelectorModal({reports, task, didScreenTransitionEnd}: TaskAssigneeSelectorModalProps) { const styles = useThemeStyles(); const route = useRoute>(); const {translate} = useLocalize(); @@ -206,26 +208,24 @@ function TaskAssigneeSelectorModal({reports, task}: TaskAssigneeSelectorModalPro includeSafeAreaPaddingBottom={false} testID={TaskAssigneeSelectorModal.displayName} > - {({didScreenTransitionEnd}) => ( - - + + + - - - - - )} + + ); } @@ -241,4 +241,4 @@ const TaskAssigneeSelectorModalWithOnyx = withOnyx = {}; if (!title) { - errors.title = 'newTaskPage.pleaseEnterTaskName'; + ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, 'newTaskPage.pleaseEnterTaskName'); + } else if (title.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, INPUT_IDS.TITLE, ['common.error.characterLimitExceedCounter', {length: title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 13e82860583..a8354cfd327 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,5 +1,4 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; @@ -12,6 +11,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {Section} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; +import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; +import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -49,7 +50,10 @@ type WorkspaceInvitePageOnyxProps = { invitedEmailsToAccountIDsDraft: OnyxEntry; }; -type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps; +type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & + WithNavigationTransitionEndProps & + WorkspaceInvitePageOnyxProps & + StackScreenProps; function WorkspaceInvitePage({ route, @@ -59,6 +63,7 @@ function WorkspaceInvitePage({ invitedEmailsToAccountIDsDraft, policy, isLoadingReportData = true, + didScreenTransitionEnd, }: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -66,8 +71,6 @@ function WorkspaceInvitePage({ const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); @@ -86,18 +89,6 @@ function WorkspaceInvitePage({ // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); - useEffect(() => { - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - setDidScreenTransitionEnd(true); - }); - - return () => { - unsubscribeTransitionEnd(); - }; - // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useNetwork({onReconnect: openWorkspaceInvitePage}); const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]); @@ -336,16 +327,18 @@ function WorkspaceInvitePage({ WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; -export default withPolicyAndFullscreenLoading( - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - invitedEmailsToAccountIDsDraft: { - key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, - }, - })(WorkspaceInvitePage), +export default withNavigationTransitionEnd( + withPolicyAndFullscreenLoading( + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + invitedEmailsToAccountIDsDraft: { + key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, + }, + })(WorkspaceInvitePage), + ), ); diff --git a/src/styles/utils/textDecorationLine.ts b/src/styles/utils/textDecorationLine.ts index e5f079150e7..5e830cbf08c 100644 --- a/src/styles/utils/textDecorationLine.ts +++ b/src/styles/utils/textDecorationLine.ts @@ -5,4 +5,8 @@ export default { textDecorationLine: 'line-through', textDecorationStyle: 'solid', }, + underlineLineThrough: { + textDecorationLine: 'underline line-through', + textDecorationStyle: 'solid', + }, } satisfies Record; diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 30e6dc738d8..8389086f872 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -1,6 +1,7 @@ import {beforeEach} from '@jest/globals'; import Onyx from 'react-native-onyx'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; @@ -84,6 +85,12 @@ describe('getViolationsOnyxData', () => { expect(result.value).not.toContainEqual(categoryOutOfPolicyViolation); }); + it('should not add a category violation when the transaction is partial', () => { + const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: undefined}; + const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).not.toContainEqual(missingCategoryViolation); + }); + it('should add categoryOutOfPolicy violation to existing violations if they exist', () => { transaction.category = 'Bananas'; transactionViolations = [ @@ -161,6 +168,12 @@ describe('getViolationsOnyxData', () => { expect(result.value).toEqual([]); }); + it('should not add a tag violation when the transaction is partial', () => { + const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, tag: undefined}; + const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).not.toContainEqual(missingTagViolation); + }); + it('should add tagOutOfPolicy violation to existing violations if transaction has tag that is not in the policy', () => { transaction.tag = 'Bananas'; transactionViolations = [