diff --git a/Mobile-Expensify b/Mobile-Expensify
index 7ffe8a7f1b47..ddda89a75246 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 7ffe8a7f1b471c697f9823b8cd4a2c19b200fa6f
+Subproject commit ddda89a75246cb1e2706d67f03a1485f481d1c7d
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7fd4fd9dd59d..faf2c2221b7d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009007802
- versionName "9.0.78-2"
+ versionCode 1009007902
+ versionName "9.0.79-2"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 3374f9c36b3f..750a68e41fb9 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 9.0.78
+ 9.0.79
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.78.2
+ 9.0.79.2
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 6f72c68b009d..dfe29bdcf8e8 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 9.0.78
+ 9.0.79
CFBundleSignature
????
CFBundleVersion
- 9.0.78.2
+ 9.0.79.2
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 328278e16cf3..b840c6d54ed6 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 9.0.78
+ 9.0.79
CFBundleVersion
- 9.0.78.2
+ 9.0.79.2
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index ae031453f883..d93fa91f928f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,17 @@
{
"name": "new.expensify",
- "version": "9.0.78-2",
+ "version": "9.0.79-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.78-2",
+ "version": "9.0.79-2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.209",
+ "@expensify/react-native-live-markdown": "0.1.210",
"@expo/metro-runtime": "~3.2.3",
"@firebase/app": "^0.10.10",
"@firebase/performance": "^0.6.8",
@@ -3498,9 +3498,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.209",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.209.tgz",
- "integrity": "sha512-u+RRY+Jog/llEu9T1v0okSLgRhG5jGlX9H1Je0A8HWv0439XFLnAWSvN2eQ2T7bvT8Yjdj5CcC0hkgJiB9oCQw==",
+ "version": "0.1.210",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.210.tgz",
+ "integrity": "sha512-CW9DY2yN/QJrqkD6+74s+kWQ9bhWQwd2jT+x5RCgyy5N2SdcoE8G8DGQQvmo6q94KcRkHIr/HsTVOyzACQ/nrw==",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
diff --git a/package.json b/package.json
index 3b5a25abb224..f40db686a0db 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.78-2",
+ "version": "9.0.79-2",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -76,7 +76,7 @@
},
"dependencies": {
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.209",
+ "@expensify/react-native-live-markdown": "0.1.210",
"@expo/metro-runtime": "~3.2.3",
"@firebase/app": "^0.10.10",
"@firebase/performance": "^0.6.8",
diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx
index 6be2b43c09d7..12189d22dba0 100644
--- a/src/components/AmountTextInput.tsx
+++ b/src/components/AmountTextInput.tsx
@@ -39,7 +39,7 @@ type AmountTextInputProps = {
/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;
-} & Pick;
+} & Pick;
function AmountTextInput(
{
diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx
index 5281b1c33b4b..313b5d620f42 100644
--- a/src/components/EmptySelectionListContent.tsx
+++ b/src/components/EmptySelectionListContent.tsx
@@ -48,7 +48,7 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps
);
return (
-
+
;
type Selection = {
start: number;
@@ -126,6 +127,7 @@ function MoneyRequestAmountInput(
hideFocusedState = true,
shouldKeepUserInput = false,
autoGrow = true,
+ autoGrowExtraSpace,
contentWidth,
...props
}: MoneyRequestAmountInputProps,
@@ -289,6 +291,7 @@ function MoneyRequestAmountInput(
return (
{
- return shouldRenderTooltip(tooltipName);
- }, [shouldRenderTooltip, tooltipName]);
+ return shouldShow && shouldRenderTooltip(tooltipName);
+ }, [shouldRenderTooltip, tooltipName, shouldShow]);
const hideProductTrainingTooltip = useCallback(() => {
const tooltip = TOOLTIPS[tooltipName];
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index ba0cda25d59e..86196f13d662 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -74,20 +74,20 @@ function MoneyRequestPreviewContent({
const route = useRoute>();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`);
const [session] = useOnyx(ONYXKEYS.SESSION);
- const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`);
const policy = PolicyUtils.getPolicy(iouReport?.policyID);
const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
- const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1';
+ const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined;
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const sessionAccountID = session?.accountID;
- const managerID = iouReport?.managerID ?? -1;
- const ownerAccountID = iouReport?.ownerAccountID ?? -1;
+ const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID;
+ const ownerAccountID = iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport);
const participantAccountIDs =
@@ -117,9 +117,9 @@ function MoneyRequestPreviewContent({
const isOnHold = TransactionUtils.isOnHold(transaction);
const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial;
const isPartialHold = isSettlementOrApprovalPartial && isOnHold;
- const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '-1', transactionViolations, true);
- const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '-1', transactionViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport);
- const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID ?? '-1', transactionViolations, true);
+ const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID, transactionViolations, true);
+ const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, transactionViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport);
+ const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID, transactionViolations, true);
const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction);
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction);
@@ -155,8 +155,8 @@ function MoneyRequestPreviewContent({
const shouldShowHoldMessage = !(isSettled && !isSettlementOrApprovalPartial) && !!transaction?.comment?.hold;
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`);
- const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
- const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
+ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID);
+ const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined;
/*
Show the merchant for IOUs and expenses only if:
@@ -253,10 +253,10 @@ function MoneyRequestPreviewContent({
if (TransactionUtils.isPending(transaction)) {
return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')};
}
- if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', iouReport, policy)) {
+ if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, iouReport, policy)) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')};
}
- if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) {
+ if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID, transactionViolations))) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')};
}
return {shouldShow: false};
@@ -301,12 +301,8 @@ function MoneyRequestPreviewContent({
// Clear the draft before selecting a different expense to prevent merging fields from the previous expense
// (e.g., category, tag, tax) that may be not enabled/available in the new expense's policy.
Transaction.abandonReviewDuplicateTransactions();
- const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(
- reviewingTransactionID,
- transaction?.reportID ?? '',
- transaction?.transactionID ?? reviewingTransactionID,
- );
- Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID ?? '', reportID: transaction?.reportID});
+ const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(reviewingTransactionID, transaction?.reportID, transaction?.transactionID ?? reviewingTransactionID);
+ Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID, reportID: transaction?.reportID});
if ('merchant' in comparisonResult.change) {
Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID, backTo));
@@ -349,11 +345,13 @@ function MoneyRequestPreviewContent({
!onPreviewPressed ? [styles.moneyRequestPreviewBox, containerStyles] : {},
]}
>
-
+ {!isDeleted && (
+
+ )}
{isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? (
) : (
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 79497e5fab88..a4ade8d77aa8 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -103,7 +103,7 @@ function ReportPreview({
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const [invoiceReceiverPolicy] = useOnyx(
- `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : -1}`,
+ `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : CONST.DEFAULT_NUMBER_ID}`,
);
const theme = useTheme();
const styles = useThemeStyles();
@@ -144,10 +144,10 @@ function ReportPreview({
const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);
const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(iouReport, shouldShowPayButton);
- const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? '');
- const hasHeldExpenses = ReportUtils.hasHeldExpenses(iouReport?.reportID ?? '');
+ const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID);
+ const hasHeldExpenses = ReportUtils.hasHeldExpenses(iouReport?.reportID);
- const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? 0;
+ const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport);
const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
@@ -189,9 +189,8 @@ function ReportPreview({
const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction}));
const showRTERViolationMessage =
numberOfRequests === 1 &&
- TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations));
- const shouldShowBrokenConnectionViolation =
- numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID ?? '-1', iouReport, policy);
+ TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID, transactionViolations));
+ const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID, iouReport, policy);
let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions.at(0)) : null;
const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions.at(0)) : null;
@@ -500,11 +499,13 @@ function ReportPreview({
accessibilityLabel={translate('iou.viewDetails')}
>
-
+ {lastThreeReceipts.length > 0 && (
+
+ )}
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index bb20b4abae11..464509b0a947 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -7,7 +7,6 @@ import {PickerAvoidingView} from 'react-native-picker-select';
import type {EdgeInsets} from 'react-native-safe-area-context';
import useEnvironment from '@hooks/useEnvironment';
import useInitialDimensions from '@hooks/useInitialWindowDimensions';
-import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useTackInputFocus from '@hooks/useTackInputFocus';
@@ -158,7 +157,6 @@ function ScreenWrapper(
const {initialHeight} = useInitialDimensions();
const styles = useThemeStyles();
const {isDevelopment} = useEnvironment();
- const {isOffline} = useNetwork();
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined;
@@ -244,18 +242,17 @@ function ScreenWrapper(
}
// We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked.
- const isSafeAreaBottomPaddingApplied = includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator);
- if (isSafeAreaBottomPaddingApplied) {
+ if (includeSafeAreaPaddingBottom) {
paddingStyle.paddingBottom = paddingBottom;
}
- if (isSafeAreaBottomPaddingApplied && ignoreInsetsConsumption) {
+ if (includeSafeAreaPaddingBottom && ignoreInsetsConsumption) {
paddingStyle.paddingBottom = unmodifiedPaddings.bottom;
}
const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit());
const contextValue = useMemo(
- () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied}),
- [didScreenTransitionEnd, isSafeAreaBottomPaddingApplied, isSafeAreaTopPaddingApplied],
+ () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}),
+ [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied],
);
return (
@@ -297,7 +294,14 @@ function ScreenWrapper(
}
{isSmallScreenWidth && shouldShowOfflineIndicator && (
<>
-
+
{/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */}
>
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index 4595d2098d74..7cc451809ee5 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -50,6 +50,7 @@ function BaseTextInput(
autoFocus = false,
disableKeyboard = false,
autoGrow = false,
+ autoGrowExtraSpace = 0,
autoGrowHeight = false,
maxAutoGrowHeight,
hideFocusedState = false,
@@ -256,7 +257,8 @@ function BaseTextInput(
const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([
styles.textInputContainer,
textInputContainerStyles,
- (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth),
+ !!contentWidth && StyleUtils.getWidthStyle(textInputWidth),
+ autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace),
!hideFocusedState && isFocused && styles.borderColorFocus,
(!!hasError || !!errorText) && styles.borderColorDanger,
autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined},
@@ -459,14 +461,10 @@ function BaseTextInput(
)}
{/*
Text input component doesn't support auto grow by default.
- We're using a hidden text input to achieve that.
This text view is used to calculate width or height of the input value given textStyle in this component.
This Text component is intentionally positioned out of the screen.
*/}
{(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && (
- // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value
- // https://github.com/Expensify/App/issues/8158
- // https://github.com/Expensify/App/issues/26628
;
+} & Pick;
type TextInputWithCurrencySymbolProps = Omit & {
onSelectionChange?: (start: number, end: number) => void;
diff --git a/src/hooks/useCleanupSelectedOptions/index.ts b/src/hooks/useCleanupSelectedOptions/index.ts
deleted file mode 100644
index 7451e85aef23..000000000000
--- a/src/hooks/useCleanupSelectedOptions/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {NavigationContainerRefContext, useIsFocused} from '@react-navigation/native';
-import {useContext, useEffect} from 'react';
-import NAVIGATORS from '@src/NAVIGATORS';
-
-const useCleanupSelectedOptions = (cleanupFunction?: () => void) => {
- const navigationContainerRef = useContext(NavigationContainerRefContext);
- const state = navigationContainerRef?.getState();
- const lastRoute = state?.routes.at(-1);
- const isRightModalOpening = lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
-
- const isFocused = useIsFocused();
-
- useEffect(() => {
- if (isFocused || isRightModalOpening) {
- return;
- }
- cleanupFunction?.();
- }, [isFocused, cleanupFunction, isRightModalOpening]);
-};
-
-export default useCleanupSelectedOptions;
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 9a71480019a6..e26618fd3a6d 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -203,8 +203,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry, personalDetails: OnyxEntry): Card[] {
const {cardList, ...cards} = cardsList ?? {};
return Object.values(cards).sort((cardA: Card, cardB: Card) => {
- const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {};
- const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {};
+ const userA = cardA.accountID ? personalDetails?.[cardA.accountID] ?? {} : {};
+ const userB = cardB.accountID ? personalDetails?.[cardB.accountID] ?? {} : {};
const aName = PersonalDetailsUtils.getDisplayNameOrDefault(userA);
const bName = PersonalDetailsUtils.getDisplayNameOrDefault(userB);
@@ -251,17 +251,15 @@ function isCustomFeed(feed: CompanyCardFeed): boolean {
return [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX].some((value) => value === feed);
}
-function getCompanyFeeds(cardFeeds: OnyxEntry): CompanyFeeds {
- return {...cardFeeds?.settings?.companyCards, ...cardFeeds?.settings?.oAuthAccountDetails};
-}
-
-function removeExpensifyCardFromCompanyCards(cardFeeds: OnyxEntry): CompanyFeeds {
- if (!cardFeeds) {
- return {};
- }
-
- const companyCards = getCompanyFeeds(cardFeeds);
- return Object.fromEntries(Object.entries(companyCards).filter(([key]) => key !== CONST.EXPENSIFY_CARD.BANK));
+function getCompanyFeeds(cardFeeds: OnyxEntry, shouldFilterOutRemovedFeeds = false): CompanyFeeds {
+ return Object.fromEntries(
+ Object.entries(cardFeeds?.settings?.companyCards ?? {}).filter(([key, value]) => {
+ if (shouldFilterOutRemovedFeeds && value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return false;
+ }
+ return key !== CONST.EXPENSIFY_CARD.BANK;
+ }),
+ );
}
function getCardFeedName(feedType: CompanyCardFeed): string {
@@ -348,7 +346,7 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf, cardFeeds: OnyxEntry): CompanyCardFeed | undefined {
- const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds)).at(0) as CompanyCardFeed | undefined;
+ const defaultFeed = Object.keys(getCompanyFeeds(cardFeeds, true)).at(0) as CompanyCardFeed | undefined;
return lastSelectedFeed ?? defaultFeed;
}
@@ -410,7 +408,6 @@ export {
getSelectedFeed,
getCorrectStepForSelectedBank,
getCustomOrFormattedFeedName,
- removeExpensifyCardFromCompanyCards,
getFilteredCardList,
hasOnlyOneCardToAssign,
checkIfNewFeedConnected,
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index 79271bdc03c7..fd8ad40d7501 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -468,6 +468,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
case 'iouReportID':
case 'preexistingReportID':
case 'private_isArchived':
+ case 'welcomeMessage':
return validateString(value);
case 'hasOutstandingChildRequest':
case 'hasOutstandingChildTask':
@@ -513,6 +514,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION,
pendingFields: 'object',
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE,
+ permissions: 'array',
},
'number',
);
@@ -621,6 +623,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
partial: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION,
preview: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+ welcomeMessage: CONST.RED_BRICK_ROAD_PENDING_ACTION,
});
}
}
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index c41b33873a8a..94167b382d49 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -289,11 +289,15 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number {
/**
* Returns custom unit rate ID for the distance transaction
*/
-function getCustomUnitRateID(reportID: string) {
+function getCustomUnitRateID(reportID?: string) {
+ let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
+
+ if (!reportID) {
+ return customUnitRateID;
+ }
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID);
- let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
if (isEmptyObject(policy)) {
return customUnitRateID;
diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts
index 9ed192b09233..8dc46204db3c 100644
--- a/src/libs/GetPhysicalCardUtils.ts
+++ b/src/libs/GetPhysicalCardUtils.ts
@@ -1,4 +1,3 @@
-import {Str} from 'expensify-common';
import type {OnyxEntry} from 'react-native-onyx';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
@@ -7,19 +6,16 @@ import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx';
import * as LoginUtils from './LoginUtils';
import Navigation from './Navigation/Navigation';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
-import * as PhoneNumberUtils from './PhoneNumber';
import * as UserUtils from './UserUtils';
function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route {
const {legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {};
const address = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails);
- const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber ?? '');
- const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
if (!legalFirstName && !legalLastName) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain);
}
- if (!phoneNumber || !parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
+ if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain);
}
if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) {
diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts
index 223966f26a6f..c1c2b0946935 100644
--- a/src/libs/Network/SequentialQueue.ts
+++ b/src/libs/Network/SequentialQueue.ts
@@ -128,6 +128,12 @@ function process(): Promise {
return currentRequestPromise;
}
+/**
+ * @param shouldResetPromise Determines whether the isReadyPromise should be reset.
+ * A READ request will wait until all the WRITE requests are done, using the isReadyPromise promise.
+ * Resetting can cause unresolved READ requests to hang if tied to the old promise,
+ * so some cases (e.g., unpausing) require skipping the reset to maintain proper behavior.
+ */
function flush(shouldResetPromise = true) {
// When the queue is paused, return early. This will keep an requests in the queue and they will get flushed again when the queue is unpaused
if (isQueuePaused) {
@@ -198,6 +204,11 @@ function unpause() {
const numberOfPersistedRequests = PersistedRequests.getAll().length || 0;
Log.info(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`);
isQueuePaused = false;
+
+ // When the queue is paused and then unpaused, we call flush which by defaults recreates the isReadyPromise.
+ // After all the WRITE requests are done, the isReadyPromise is resolved, but since it's a new instance of promise,
+ // the pending READ request never received the resolved callback. That's why we don't want to recreate
+ // the promise when unpausing the queue.
flush(false);
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 1d07cb576df3..027b11c516f1 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -52,7 +52,7 @@ import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/Ony
import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {Status} from '@src/types/onyx/PersonalDetails';
import type {ConnectionName} from '@src/types/onyx/Policy';
-import type {NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
+import type {InvoiceReceiverType, NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction';
import type {PendingChatMember} from '@src/types/onyx/ReportMetadata';
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
@@ -6774,7 +6774,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec
/**
* Attempts to find an invoice chat report in onyx with the provided policyID and receiverID.
*/
-function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = allReports): OnyxEntry {
+function getInvoiceChatByParticipants(receiverID: string | number, receiverType: InvoiceReceiverType, policyID?: string, reports: OnyxCollection = allReports): OnyxEntry {
return Object.values(reports ?? {}).find((report) => {
if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) {
return false;
@@ -6782,6 +6782,7 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num
const isSameReceiver =
report.invoiceReceiver &&
+ report.invoiceReceiver.type === receiverType &&
(('accountID' in report.invoiceReceiver && report.invoiceReceiver.accountID === receiverID) ||
('policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === receiverID));
@@ -7820,7 +7821,7 @@ function hasHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTra
/**
* Check if all expenses in the Report are on hold
*/
-function hasOnlyHeldExpenses(iouReportID: string, allReportTransactions?: SearchTransaction[]): boolean {
+function hasOnlyHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean {
const reportTransactions = allReportTransactions ?? reportsTransactions[iouReportID ?? ''] ?? [];
return reportTransactions.length > 0 && !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction));
}
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 8bc1dd7356cb..e40facb21c48 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -376,6 +376,11 @@ function getUpdatedTransaction({
if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') {
updatedTransaction.category = transactionChanges.category;
+ const {categoryTaxCode, categoryTaxAmount} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy);
+ if (categoryTaxCode && categoryTaxAmount !== undefined) {
+ updatedTransaction.taxCode = categoryTaxCode;
+ updatedTransaction.taxAmount = categoryTaxAmount;
+ }
}
if (Object.hasOwn(transactionChanges, 'tag') && typeof transactionChanges.tag === 'string') {
@@ -703,7 +708,7 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry):
/**
* Get all transaction violations of the transaction with given tranactionID.
*/
-function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null {
+function getTransactionViolations(transactionID: string | undefined, transactionViolations: OnyxCollection | null): TransactionViolations | null {
return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null;
}
@@ -723,7 +728,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations |
/**
* Check if there is broken connection violation.
*/
-function hasBrokenConnectionViolation(transactionID: string): boolean {
+function hasBrokenConnectionViolation(transactionID?: string): boolean {
const violations = getTransactionViolations(transactionID, allTransactionViolations);
return !!violations?.find(
(violation) =>
@@ -735,7 +740,7 @@ function hasBrokenConnectionViolation(transactionID: string): boolean {
/**
* Check if user should see broken connection violation warning.
*/
-function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean {
+function shouldShowBrokenConnectionViolation(transactionID: string | undefined, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean {
return (
hasBrokenConnectionViolation(transactionID) &&
(!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isOpenExpenseReport(report) || (ReportUtils.isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy)))
@@ -881,7 +886,7 @@ function isOnHoldByTransactionID(transactionID: string): boolean {
/**
* Checks if any violations for the provided transaction are of type 'violation'
*/
-function hasViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+function hasViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some(
(violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && (showInReview === undefined || showInReview === (violation.showInReview ?? false)),
);
@@ -890,7 +895,7 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti
/**
* Checks if any violations for the provided transaction are of type 'notice'
*/
-function hasNoticeTypeViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+function hasNoticeTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some(
(violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && (showInReview === undefined || showInReview === (violation.showInReview ?? false)),
);
@@ -899,7 +904,7 @@ function hasNoticeTypeViolation(transactionID: string, transactionViolations: On
/**
* Checks if any violations for the provided transaction are of type 'warning'
*/
-function hasWarningTypeViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+function hasWarningTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID];
const warningTypeViolations =
violations?.filter(
@@ -1060,21 +1065,24 @@ function removeSettledAndApprovedTransactions(transactionIDs: string[]) {
*/
function compareDuplicateTransactionFields(
- reviewingTransactionID: string | undefined,
- reportID: string | undefined,
+ reviewingTransactionID?: string | undefined,
+ reportID?: string | undefined,
selectedTransactionID?: string,
): {keep: Partial; change: FieldsToChange} {
if (!reviewingTransactionID || !reportID) {
return {change: {}, keep: {}};
}
- const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`];
- const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [];
- const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keep: Record = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const change: Record = {};
+ if (!reviewingTransactionID || !reportID) {
+ return {keep, change};
+ }
+ const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`];
+ const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [];
+ const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item));
const fieldsToCompare: FieldsToCompare = {
merchant: ['modifiedMerchant', 'merchant'],
@@ -1255,11 +1263,12 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, policy: OnyxEntry) {
const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax);
- if (!taxRules || taxRules?.length === 0) {
+ if (!taxRules || taxRules?.length === 0 || isDistanceRequest(transaction)) {
return {categoryTaxCode: undefined, categoryTaxAmount: undefined};
}
- const categoryTaxCode = getCategoryDefaultTaxRate(taxRules, category, policy?.taxRates?.defaultExternalID);
+ const defaultTaxCode = getDefaultTaxCode(policy, transaction, getCurrency(transaction));
+ const categoryTaxCode = getCategoryDefaultTaxRate(taxRules, category, defaultTaxCode);
const categoryTaxPercentage = getTaxValue(policy, transaction, categoryTaxCode ?? '');
let categoryTaxAmount;
diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts
index 8e83b9192a71..d0e19398c257 100644
--- a/src/libs/actions/CompanyCards.ts
+++ b/src/libs/actions/CompanyCards.ts
@@ -124,7 +124,6 @@ function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: n
function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, policyID: string, bankName: CompanyCardFeed, liabilityType: string) {
const authToken = NetworkStore.getAuthToken();
- const isCustomFeed = CardUtils.isCustomFeed(bankName);
const feedUpdates = {
[bankName]: {liabilityType},
};
@@ -135,7 +134,7 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number,
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
- settings: isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates},
+ settings: {companyCards: feedUpdates},
},
},
],
@@ -151,10 +150,14 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number,
API.write(WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY, parameters, onyxData);
}
-function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed, feedToOpen?: CompanyCardFeed) {
+function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed, cardIDs: string[], feedToOpen?: CompanyCardFeed) {
const authToken = NetworkStore.getAuthToken();
const isCustomFeed = CardUtils.isCustomFeed(bankName);
- const feedUpdates = {[bankName]: null};
+ const optimisticFeedUpdates = {[bankName]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}};
+ const successFeedUpdates = {[bankName]: null};
+ const failureFeedUpdates = {[bankName]: {pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}};
+ const optimisticCardUpdates = Object.fromEntries(cardIDs.map((cardID) => [cardID, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}]));
+ const successAndFailureCardUpdates = Object.fromEntries(cardIDs.map((cardID) => [cardID, {pendingAction: null}]));
const optimisticData: OnyxUpdate[] = [
{
@@ -162,22 +165,74 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
settings: {
- ...(isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}),
+ companyCards: optimisticFeedUpdates,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ value: optimisticCardUpdates,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: optimisticCardUpdates,
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
+ value: {
+ settings: {
+ ...(isCustomFeed ? {companyCards: successFeedUpdates} : {oAuthAccountDetails: successFeedUpdates, companyCards: successFeedUpdates}),
companyCardNicknames: {
[bankName]: null,
},
},
},
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ value: successAndFailureCardUpdates,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: successAndFailureCardUpdates,
+ },
];
- if (feedToOpen) {
- optimisticData.push({
+ const failureData: OnyxUpdate[] = [
+ {
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`,
- value: feedToOpen,
- });
- }
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
+ value: {
+ settings: {
+ companyCards: failureFeedUpdates,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ value: successAndFailureCardUpdates,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: successAndFailureCardUpdates,
+ },
+ ];
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`,
+ value: feedToOpen ?? null,
+ });
const parameters = {
authToken,
@@ -185,7 +240,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu
bankName,
};
- API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, {optimisticData});
+ API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, {optimisticData, successData, failureData});
}
function assignWorkspaceCompanyCard(policyID: string, data?: Partial) {
@@ -194,7 +249,7 @@ function assignWorkspaceCompanyCard(policyID: string, data?: Partial,
@@ -2086,17 +2107,18 @@ function getSendInvoiceInformation(
const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {};
const trimmedComment = (comment?.comment ?? '').trim();
const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID ?? '-1';
- const receiverParticipant = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver;
+ const receiverParticipant: Participant | InvoiceReceiver | undefined = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver;
const receiverAccountID = receiverParticipant && 'accountID' in receiverParticipant && receiverParticipant.accountID ? receiverParticipant.accountID : -1;
let receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID);
let optimisticPersonalDetailListAction = {};
+ const receiverType = getReceiverType(receiverParticipant);
// STEP 1: Get existing chat report OR build a new optimistic one
let isNewChatReport = false;
let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null;
if (!chatReport) {
- chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID) ?? null;
+ chatReport = ReportUtils.getInvoiceChatByParticipants(receiverAccountID, receiverType, senderWorkspaceID) ?? null;
}
if (!chatReport) {
@@ -3441,15 +3463,8 @@ function updateMoneyRequestCategory(
policyTagList: OnyxEntry,
policyCategories: OnyxEntry,
) {
- const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
- const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, policy);
const transactionChanges: TransactionChanges = {
category,
- ...(categoryTaxCode &&
- categoryTaxAmount !== undefined && {
- taxCode: categoryTaxCode,
- taxAmount: categoryTaxAmount,
- }),
};
const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories);
@@ -5377,27 +5392,19 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
}
function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) {
- const newTransactionChanges = {...transactionChanges};
let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`];
if (!draftSplitTransaction) {
draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
}
- if (transactionChanges.category) {
- const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(transactionChanges.category, draftSplitTransaction, policy);
- if (categoryTaxCode && categoryTaxAmount !== undefined) {
- newTransactionChanges.taxCode = categoryTaxCode;
- newTransactionChanges.taxAmount = categoryTaxAmount;
- }
- }
-
const updatedTransaction = draftSplitTransaction
? TransactionUtils.getUpdatedTransaction({
transaction: draftSplitTransaction,
- transactionChanges: newTransactionChanges,
+ transactionChanges,
isFromExpenseReport: false,
shouldUpdateReceiptState: false,
+ policy,
})
: null;
@@ -6896,7 +6903,7 @@ function getPayMoneyRequestParams(
}
if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && activePolicyID) {
- const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID);
+ const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport.policyID);
if (existingB2BInvoiceRoom) {
chatReport = existingB2BInvoiceRoom;
}
@@ -7761,6 +7768,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
...expenseReport,
+ lastVisibleActionCreated: optimisticReportAction?.created,
lastMessageText: ReportActionsUtils.getReportActionText(optimisticReportAction),
lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticReportAction),
stateNum,
@@ -7864,6 +7872,8 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O
},
{optimisticData, successData, failureData},
);
+ Navigation.dismissModal();
+ Report.notifyNewAction(expenseReport.reportID, userAccountID);
}
/**
@@ -7923,6 +7933,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R
playSound(SOUNDS.SUCCESS);
API.write(apiCommand, params, {optimisticData, successData, failureData});
+ Report.notifyNewAction(iouReport?.reportID ?? '', userAccountID);
}
function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxEntry, payAsBusiness = false) {
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
index a1e4f0e4a22a..580f94e0f2f4 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
@@ -15,13 +15,15 @@ import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
function BaseSidebarScreen() {
const styles = useThemeStyles();
const activeWorkspaceID = useActiveWorkspaceFromNavigationState();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const [activeWorkspace] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? CONST.DEFAULT_NUMBER_ID}`);
+ const [activeWorkspace, activeWorkspaceResult] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? CONST.DEFAULT_NUMBER_ID}`);
+ const isLoading = isLoadingOnyxValue(activeWorkspaceResult);
useEffect(() => {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
@@ -29,13 +31,13 @@ function BaseSidebarScreen() {
}, []);
useEffect(() => {
- if (!!activeWorkspace || activeWorkspaceID === undefined) {
+ if (!!activeWorkspace || activeWorkspaceID === undefined || isLoading) {
return;
}
Navigation.navigateWithSwitchPolicyID({policyID: undefined});
updateLastAccessedWorkspace(undefined);
- }, [activeWorkspace, activeWorkspaceID]);
+ }, [activeWorkspace, activeWorkspaceID, isLoading]);
const shouldDisplaySearch = shouldUseNarrowLayout;
diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx
index ba406c3ddef6..533c113c2a86 100644
--- a/src/pages/iou/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/MoneyRequestAmountForm.tsx
@@ -19,6 +19,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
+import variables from '@styles/variables';
import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types';
import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
@@ -259,6 +260,7 @@ function MoneyRequestAmountForm(
>
(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID);
@@ -70,6 +74,8 @@ function IOURequestStepParticipants({
const receiptFilename = transaction?.filename;
const receiptPath = transaction?.receipt?.source;
const receiptType = transaction?.receipt?.type;
+ const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID;
+ const isMobileSafari = Browser.isMobileSafari();
// When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow.
// This is because until the expense is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then
@@ -86,7 +92,7 @@ function IOURequestStepParticipants({
(val: Participant[]) => {
HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS);
- const firstParticipantReportID = val.at(0)?.reportID ?? '';
+ const firstParticipantReportID = val.at(0)?.reportID;
const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID);
const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID);
numberOfParticipants.current = val.length;
@@ -102,11 +108,24 @@ function IOURequestStepParticipants({
}
// When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step.
- selectedReportID.current = firstParticipantReportID || reportID;
+ selectedReportID.current = firstParticipantReportID ?? reportID;
},
[iouType, reportID, transactionID],
);
+ const handleNavigation = useCallback(
+ (route: Route) => {
+ if (isAndroidNative || isMobileSafari) {
+ KeyboardUtils.dismiss().then(() => {
+ Navigation.navigate(route);
+ });
+ } else {
+ Navigation.navigate(route);
+ }
+ },
+ [isAndroidNative, isMobileSafari],
+ );
+
const goToNextStep = useCallback(() => {
const isCategorizing = action === CONST.IOU.ACTION.CATEGORIZE;
const isShareAction = action === CONST.IOU.ACTION.SHARE;
@@ -132,12 +151,13 @@ function IOURequestStepParticipants({
transactionID,
selectedReportID.current || reportID,
);
- if (isCategorizing) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute));
- } else {
- Navigation.navigate(iouConfirmationPageRoute);
- }
- }, [iouType, transactionID, transaction, reportID, action, participants]);
+
+ const route = isCategorizing
+ ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)
+ : iouConfirmationPageRoute;
+
+ handleNavigation(route);
+ }, [action, participants, iouType, transaction, transactionID, reportID, handleNavigation]);
const navigateBack = useCallback(() => {
IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action);
@@ -154,7 +174,8 @@ function IOURequestStepParticipants({
IOU.setCustomUnitRateID(transactionID, rateID);
IOU.setMoneyRequestParticipantsFromReport(transactionID, selfDMReport);
const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID);
- Navigation.navigate(iouConfirmationPageRoute);
+
+ handleNavigation(iouConfirmationPageRoute);
};
useEffect(() => {
diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
index 075bc4a3ff5c..bcb3fe646fff 100644
--- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
+++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
@@ -1,4 +1,3 @@
-import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
@@ -9,8 +8,6 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as LoginUtils from '@libs/LoginUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import * as PhoneNumberUtils from '@libs/PhoneNumber';
-import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -43,17 +40,12 @@ function GetPhysicalCardPhone({
const errors: OnValidateResult = {};
- if (!ValidationUtils.isRequiredFulfilled(phoneNumberToValidate)) {
+ if (!LoginUtils.validateNumber(phoneNumberToValidate)) {
+ errors.phoneNumber = translate('common.error.phoneNumber');
+ } else if (!phoneNumberToValidate) {
errors.phoneNumber = translate('common.error.fieldRequired');
}
- const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumberToValidate);
- const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
-
- if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
- errors.phoneNumber = translate('bankAccount.error.phoneNumber');
- }
-
return errors;
};
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index ddcb89064c7e..6c6e70b8cc28 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -74,7 +74,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
!!policy?.connections?.xero?.config?.importTaxRates ||
!!policy?.connections?.netsuite?.options?.config?.syncOptions?.syncTax;
const policyID = policy?.id;
- const workspaceAccountID = policy?.workspaceAccountID ?? -1;
+ const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}_${CONST.EXPENSIFY_CARD.BANK}`);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`);
const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false);
@@ -131,7 +131,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle',
isActive: policy?.areCompanyCardsEnabled ?? false,
pendingAction: policy?.pendingFields?.areCompanyCardsEnabled,
- disabled: !isEmptyObject(CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds)),
+ disabled: !isEmptyObject(CardUtils.getCompanyFeeds(cardFeeds)),
action: (isEnabled: boolean) => {
if (!policyID) {
return;
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index a8a37638f87e..737fbc2972c1 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -1,4 +1,4 @@
-import {useFocusEffect} from '@react-navigation/native';
+import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import lodashSortBy from 'lodash/sortBy';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
@@ -23,7 +23,6 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption';
-import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -71,6 +70,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const [selectedCategories, setSelectedCategories] = useState>({});
const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false);
const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false);
+ const isFocused = useIsFocused();
const {environmentURL} = useEnvironment();
const policyId = route.params.policyID ?? '-1';
const backTo = route.params?.backTo;
@@ -98,8 +98,12 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
}, [fetchCategories]),
);
- const cleanupSelectedOption = useCallback(() => setSelectedCategories({}), []);
- useCleanupSelectedOptions(cleanupSelectedOption);
+ useEffect(() => {
+ if (isFocused) {
+ return;
+ }
+ setSelectedCategories({});
+ }, [isFocused]);
const categoryList = useMemo(
() =>
@@ -147,10 +151,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
};
const navigateToCategorySettings = (category: PolicyOption) => {
- if (isSmallScreenWidth && selectionMode?.isEnabled) {
- toggleCategory(category);
- return;
- }
Navigation.navigate(
isQuickSettingsFlow
? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(policyId, category.keyForList, backTo)
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
index 015d018afb76..fb49ece0feb0 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
@@ -58,13 +58,15 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
const card = allBankCards?.[cardID];
const cardBank = card?.bank ?? '';
- const cardholder = personalDetails?.[card?.accountID ?? -1];
+ const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID];
const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(cardholder);
const exportMenuItem = getExportMenuItem(connectedIntegration, policyID, translate, policy, card);
const unassignCard = () => {
setIsUnassignModalVisible(false);
- CompanyCards.unassignWorkspaceCompanyCard(workspaceAccountID, bank, card);
+ if (card) {
+ CompanyCards.unassignWorkspaceCompanyCard(workspaceAccountID, bank, card);
+ }
Navigation.goBack();
};
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx
index 723242c55494..29d0dfb0c6af 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx
@@ -42,13 +42,14 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds);
const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds);
- const availableCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds);
- const feeds: CardFeedListItem[] = (Object.keys(availableCards) as CompanyCardFeed[]).map((feed) => ({
+ const feeds: CardFeedListItem[] = (Object.keys(companyFeeds) as CompanyCardFeed[]).map((feed) => ({
value: feed,
text: CardUtils.getCustomOrFormattedFeedName(feed, cardFeeds?.settings?.companyCardNicknames),
keyForList: feed,
isSelected: feed === selectedFeed,
+ isDisabled: companyFeeds[feed]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ pendingAction: companyFeeds[feed]?.pendingAction,
brickRoadIndicator: companyFeeds[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
canShowSeveralIndicators: !!companyFeeds[feed]?.errors,
leftElement: (
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
index 392138a2d8d1..ea3a0e0f7071 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -49,9 +49,9 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
const filteredCardList = CardUtils.getFilteredCardList(cardsList, selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined);
- const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds);
+ const companyCards = CardUtils.getCompanyFeeds(cardFeeds);
const selectedFeedData = selectedFeed && companyCards[selectedFeed];
- const isNoFeed = isEmptyObject(companyCards) && !selectedFeedData;
+ const isNoFeed = !selectedFeedData;
const isPending = !!selectedFeedData?.pending;
const isFeedAdded = !isPending && !isNoFeed;
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx
index 58c79d41d3c9..60774c448546 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx
@@ -35,13 +35,14 @@ function WorkspaceCompanyCardsSettingsPage({
const styles = useThemeStyles();
const {translate} = useLocalize();
const policy = usePolicy(policyID);
- const workspaceAccountID = policy?.workspaceAccountID ?? -1;
+ const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [deleteCompanyCardConfirmModalVisible, setDeleteCompanyCardConfirmModalVisible] = useState(false);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we want to run the hook only once to escape unexpected feed change
const selectedFeed = useMemo(() => CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds), []);
+ const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`);
const feedName = CardUtils.getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames);
const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds);
const liabilityType = selectedFeed && companyFeeds[selectedFeed]?.liabilityType;
@@ -53,8 +54,12 @@ function WorkspaceCompanyCardsSettingsPage({
const deleteCompanyCardFeed = () => {
if (selectedFeed) {
- const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[]).filter((feed) => feed !== selectedFeed).at(0);
- CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, feedToOpen);
+ const {cardList, ...cards} = cardsList ?? {};
+ const cardIDs = Object.keys(cards);
+ const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[])
+ .filter((feed) => feed !== selectedFeed && companyFeeds[feed]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
+ .at(0);
+ CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, cardIDs, feedToOpen);
}
setDeleteCompanyCardConfirmModalVisible(false);
Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
index 85a5d2372ee9..c53fe1fb2e7f 100644
--- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -81,10 +81,10 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID;
const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN;
const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login;
- const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails);
+ const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails);
const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? '';
const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0;
- const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0;
+ const paymentAccountID = cardSettings?.paymentBankAccountID ?? CONST.DEFAULT_NUMBER_ID;
useEffect(() => {
CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID);
diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
index d53d8a558276..afa76e63a815 100644
--- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
@@ -49,7 +49,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew
const accountID = Number(route.params.accountID);
const memberLogin = personalDetails?.[accountID]?.login ?? '';
const memberName = personalDetails?.[accountID]?.firstName ? personalDetails?.[accountID]?.firstName : personalDetails?.[accountID]?.login;
- const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds);
+ const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds);
const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`);
const filteredCardList = CardUtils.getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed as CompanyCardFeed]);
@@ -97,10 +97,12 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew
setShouldShowError(false);
};
- const companyCardFeeds: CardFeedListItem[] = (Object.keys(availableCompanyCards) as CompanyCardFeed[]).map((key) => ({
+ const companyCardFeeds: CardFeedListItem[] = (Object.keys(companyFeeds) as CompanyCardFeed[]).map((key) => ({
value: key,
text: CardUtils.getCustomOrFormattedFeedName(key, cardFeeds?.settings?.companyCardNicknames),
keyForList: key,
+ isDisabled: companyFeeds[key]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ pendingAction: companyFeeds[key]?.pendingAction,
isSelected: selectedFeed === key,
leftElement: (
setSelectedTags({}), []);
- useCleanupSelectedOptions(cleanupSelectedOption);
+ useEffect(() => {
+ if (isFocused) {
+ return;
+ }
+ setSelectedTags({});
+ }, [isFocused]);
const getPendingAction = (policyTagList: PolicyTagList): PendingAction | undefined => {
if (!policyTagList) {
@@ -172,10 +176,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
};
const navigateToTagSettings = (tag: TagListItem) => {
- if (isSmallScreenWidth && selectionMode?.isEnabled) {
- toggleTag(tag);
- return;
- }
if (tag.orderWeight !== undefined) {
Navigation.navigate(
isQuickSettingsFlow ? ROUTES.SETTINGS_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight, backTo) : ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight),
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
index e588a1ecb313..e064c04878a1 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
@@ -1,5 +1,5 @@
-import {useFocusEffect} from '@react-navigation/native';
-import React, {useCallback, useMemo, useState} from 'react';
+import {useFocusEffect, useIsFocused} from '@react-navigation/native';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -17,7 +17,6 @@ import SelectionListWithModal from '@components/SelectionListWithModal';
import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
-import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -52,8 +51,7 @@ function WorkspaceTaxesPage({
params: {policyID},
},
}: WorkspaceTaxesPageProps) {
- // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
- const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
@@ -63,6 +61,7 @@ function WorkspaceTaxesPage({
const {selectionMode} = useMobileSelectionMode();
const defaultExternalID = policy?.taxRates?.defaultExternalID;
const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
+ const isFocused = useIsFocused();
const hasAccountingConnections = PolicyUtils.hasAccountingConnections(policy);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`);
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
@@ -87,8 +86,12 @@ function WorkspaceTaxesPage({
}, [fetchTaxes]),
);
- const cleanupSelectedOption = useCallback(() => setSelectedTaxesIDs([]), []);
- useCleanupSelectedOptions(cleanupSelectedOption);
+ useEffect(() => {
+ if (isFocused) {
+ return;
+ }
+ setSelectedTaxesIDs([]);
+ }, [isFocused]);
const textForDefault = useCallback(
(taxID: string, taxRate: TaxRate): string => {
@@ -189,10 +192,6 @@ function WorkspaceTaxesPage({
if (!taxRate.keyForList) {
return;
}
- if (isSmallScreenWidth && selectionMode?.isEnabled) {
- toggleTax(taxRate);
- return;
- }
Navigation.navigate(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID, taxRate.keyForList));
};
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 0db9594b18fc..3bb80f71f1b5 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -1302,6 +1302,16 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
};
},
+ /*
+ * Returns styles for the text input container, with extraSpace allowing overflow without affecting the layout.
+ */
+ getAutoGrowWidthInputContainerStyles: (width: number, extraSpace: number): ViewStyle => {
+ if (!!width && !!extraSpace) {
+ return {marginRight: -extraSpace, width: width + extraSpace};
+ }
+ return {width};
+ },
+
/*
* Returns the actual maxHeight of the auto-growing markdown text input.
*/
diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts
index 3f5d82fb05cd..9ea014c5007c 100644
--- a/src/types/onyx/CardFeeds.ts
+++ b/src/types/onyx/CardFeeds.ts
@@ -13,7 +13,7 @@ type CardFeedProvider =
| typeof CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE;
/** Custom card feed data */
-type CustomCardFeedData = {
+type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether any actions are pending */
pending?: boolean;
@@ -37,10 +37,10 @@ type CustomCardFeedData = {
/** Broken connection errors */
errors?: OnyxCommon.Errors;
-};
+}>;
/** Direct card feed data */
-type DirectCardFeedData = {
+type DirectCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** List of accounts */
accountList: string[];
@@ -58,7 +58,7 @@ type DirectCardFeedData = {
/** Broken connection errors */
errors?: OnyxCommon.Errors;
-};
+}>;
/** Card feed data */
type CardFeedData = CustomCardFeedData | DirectCardFeedData;
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 5ea02862599e..4fdd95d7e3d9 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -1491,9 +1491,15 @@ type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Account balance */
stripeConnectAccountBalance?: number;
+ /** AccountID */
+ stripeConnectAccountID?: string;
+
/** bankAccountID of selected BBA for payouts */
transferBankAccountID?: number;
};
+
+ /** The markUp */
+ markUp?: number;
}>;
/** Names of policy features */
@@ -1630,6 +1636,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
harvesting?: {
/** Whether the scheduled submit is enabled */
enabled: boolean;
+
+ /** The ID of the Bedrock job that runs harvesting */
+ jobID?: number;
};
/** Whether the self approval or submitting is enabled */
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index 8e79ff1accf5..ca15806c0aca 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -30,6 +30,9 @@ type Participant = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether the participant is visible in the report */
notificationPreference: NotificationPreference;
+
+ /** Permissions granted to the participant */
+ permissions?: Array>;
}>;
/** Types of invoice receivers in a report */
@@ -49,6 +52,9 @@ type InvoiceReceiver =
policyID: string;
};
+/** Type of invoice receiver */
+type InvoiceReceiverType = InvoiceReceiver['type'];
+
/** Record of report participants, indexed by their accountID */
type Participants = Record;
@@ -217,6 +223,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the report is archived */
// eslint-disable-next-line @typescript-eslint/naming-convention
private_isArchived?: string;
+
+ /** The report's welcome message */
+ welcomeMessage?: string;
},
'addWorkspaceRoom' | 'avatar' | 'createChat' | 'partial' | 'reimbursed' | 'preview'
>;
@@ -226,4 +235,4 @@ type ReportCollectionDataSet = CollectionDataSet;
diff --git a/src/utils/keyboard.ts b/src/utils/keyboard/index.ts
similarity index 100%
rename from src/utils/keyboard.ts
rename to src/utils/keyboard/index.ts
diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts
new file mode 100644
index 000000000000..f2ea9c673fdf
--- /dev/null
+++ b/src/utils/keyboard/index.website.ts
@@ -0,0 +1,48 @@
+import {Keyboard} from 'react-native';
+
+let isVisible = false;
+const initialViewportHeight = window?.visualViewport?.height;
+
+const handleResize = () => {
+ const currentHeight = window?.visualViewport?.height;
+
+ if (!currentHeight || !initialViewportHeight) {
+ return;
+ }
+
+ if (currentHeight < initialViewportHeight) {
+ isVisible = true;
+ return;
+ }
+
+ if (currentHeight === initialViewportHeight) {
+ isVisible = false;
+ }
+};
+
+window.visualViewport?.addEventListener('resize', handleResize);
+
+const dismiss = (): Promise => {
+ return new Promise((resolve) => {
+ if (!isVisible) {
+ resolve();
+ return;
+ }
+
+ const handleDismissResize = () => {
+ if (window.visualViewport?.height !== initialViewportHeight) {
+ return;
+ }
+
+ window.visualViewport?.removeEventListener('resize', handleDismissResize);
+ return resolve();
+ };
+
+ window.visualViewport?.addEventListener('resize', handleDismissResize);
+ Keyboard.dismiss();
+ });
+};
+
+const utils = {dismiss};
+
+export default utils;
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 49e1150f1a57..415612afe414 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -10,6 +10,7 @@ import * as PolicyActions from '@src/libs/actions/Policy/Policy';
import * as Report from '@src/libs/actions/Report';
import * as ReportActions from '@src/libs/actions/ReportActions';
import * as User from '@src/libs/actions/User';
+import * as API from '@src/libs/API';
import DateUtils from '@src/libs/DateUtils';
import * as Localize from '@src/libs/Localize';
import * as NumberUtils from '@src/libs/NumberUtils';
@@ -24,6 +25,8 @@ import type {ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'
import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction';
import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import * as InvoiceData from '../data/Invoice';
+import type {InvoiceTestData} from '../data/Invoice';
import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
import createRandomReport from '../utils/collections/reports';
import createRandomTransaction from '../utils/collections/transaction';
@@ -3454,6 +3457,33 @@ describe('actions/IOU', () => {
});
describe('sendInvoice', () => {
+ it('creates a new invoice chat when one has been converted from individual to business', async () => {
+ // Mock API.write for this test
+ const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+
+ // Given a convertedInvoiceReport is stored in Onyx
+ const {policy, transaction, convertedInvoiceChat}: InvoiceTestData = InvoiceData;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat?.reportID}`, convertedInvoiceChat ?? {});
+
+ // And data for when a new invoice is sent to a user
+ const currentUserAccountID = 32;
+ const companyName = 'b1-53019';
+ const companyWebsite = 'https://www.53019.com';
+
+ // When the user sends a new invoice to an individual
+ IOU.sendInvoice(currentUserAccountID, transaction, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite);
+
+ // Then a new invoice chat is created instead of incorrectly using the invoice chat which has been converted from individual to business
+ expect(writeSpy).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID) as string,
+ }),
+ expect.anything(),
+ );
+ writeSpy.mockRestore();
+ });
+
it('should not clear transaction pending action when send invoice fails', async () => {
// Given a send invoice request
mockFetch?.pause?.();
@@ -3520,40 +3550,82 @@ describe('actions/IOU', () => {
});
});
- it('should not change the tax if there are no tax expense rules', async () => {
- // Given a policy without tax expense rules
- const transactionID = '1';
- const category = 'Advertising';
- const policyID = '2';
- const taxCode = 'id_TAX_EXEMPT';
- const taxAmount = 0;
- const fakePolicy: OnyxTypes.Policy = {
- ...createRandomPolicy(Number(policyID)),
- taxRates: CONST.DEFAULT_TAX,
- rules: {},
- };
- await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
- taxCode,
- taxAmount,
- amount: 100,
+ describe('should not change the tax', () => {
+ it('if the transaction type is distance', async () => {
+ // Given a policy with tax expense rules associated with category and a distance transaction
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const taxAmount = 0;
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting the money request category
+ IOU.setMoneyRequestCategory(transactionID, category, policyID);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(taxCode);
+ expect(transaction?.taxAmount).toBe(taxAmount);
+ resolve();
+ },
+ });
+ });
});
- await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
- // When setting the money request category
- IOU.setMoneyRequestCategory(transactionID, category, policyID);
+ it('if there are no tax expense rules', async () => {
+ // Given a policy without tax expense rules
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const taxAmount = 0;
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
- await waitForBatchedUpdates();
+ // When setting the money request category
+ IOU.setMoneyRequestCategory(transactionID, category, policyID);
- // Then the transaction tax rate and amount shouldn't be updated
- await new Promise((resolve) => {
- const connection = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
- callback: (transaction) => {
- Onyx.disconnect(connection);
- expect(transaction?.taxCode).toBe(taxCode);
- expect(transaction?.taxAmount).toBe(taxAmount);
- resolve();
- },
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(taxCode);
+ expect(transaction?.taxAmount).toBe(taxAmount);
+ resolve();
+ },
+ });
});
});
});
@@ -3593,6 +3665,7 @@ describe('actions/IOU', () => {
// Given a policy with tax expense rules associated with category
const transactionID = '1';
const policyID = '2';
+ const transactionThreadReportID = '3';
const category = 'Advertising';
const taxCode = 'id_TAX_EXEMPT';
const ruleTaxCode = 'id_TAX_RATE_1';
@@ -3607,9 +3680,10 @@ describe('actions/IOU', () => {
amount: 100,
});
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {reportID: transactionThreadReportID});
// When updating a money request category
- IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
+ IOU.updateMoneyRequestCategory(transactionID, transactionThreadReportID, category, fakePolicy, undefined, undefined);
await waitForBatchedUpdates();
@@ -3625,39 +3699,105 @@ describe('actions/IOU', () => {
},
});
});
- });
- it('should not update the tax when there are no tax expense rules', async () => {
- // Given a policy without tax expense rules
- const transactionID = '1';
- const policyID = '2';
- const category = 'Advertising';
- const fakePolicy: OnyxTypes.Policy = {
- ...createRandomPolicy(Number(policyID)),
- taxRates: CONST.DEFAULT_TAX,
- rules: {},
- };
- await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {amount: 100});
- await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
-
- // When updating the money request category
- IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
-
- await waitForBatchedUpdates();
-
- // Then the transaction tax rate and amount shouldn't be updated
+ // But the original message should only contains the old and new category data
await new Promise((resolve) => {
const connection = Onyx.connect({
- key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
- callback: (transaction) => {
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
+ callback: (reportActions) => {
Onyx.disconnect(connection);
- expect(transaction?.taxCode).toBeUndefined();
- expect(transaction?.taxAmount).toBeUndefined();
- resolve();
+ const reportAction = Object.values(reportActions ?? {}).at(0);
+ if (ReportActionsUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE)) {
+ const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
+ expect(originalMessage?.oldCategory).toBe('');
+ expect(originalMessage?.category).toBe(category);
+ expect(originalMessage?.oldTaxRate).toBeUndefined();
+ expect(originalMessage?.oldTaxAmount).toBeUndefined();
+ resolve();
+ }
},
});
});
});
+
+ describe('should not update the tax', () => {
+ it('if the transaction type is distance', async () => {
+ // Given a policy with tax expense rules associated with category and a distance transaction
+ const transactionID = '1';
+ const policyID = '2';
+ const category = 'Advertising';
+ const taxCode = 'id_TAX_EXEMPT';
+ const taxAmount = 0;
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ comment: {
+ type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT,
+ customUnit: {
+ name: CONST.CUSTOM_UNITS.NAME_DISTANCE,
+ },
+ },
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When updating a money request category
+ IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(taxCode);
+ expect(transaction?.taxAmount).toBe(taxAmount);
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('if there are no tax expense rules', async () => {
+ // Given a policy without tax expense rules
+ const transactionID = '1';
+ const policyID = '2';
+ const category = 'Advertising';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {amount: 100});
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When updating the money request category
+ IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBeUndefined();
+ expect(transaction?.taxAmount).toBeUndefined();
+ resolve();
+ },
+ });
+ });
+ });
+ });
});
describe('setDraftSplitTransaction', () => {
@@ -3700,7 +3840,7 @@ describe('actions/IOU', () => {
});
describe('should not change the tax', () => {
- it('if there is no tax expense rules', async () => {
+ it('if there are no tax expense rules', async () => {
// Given a policy without tax expense rules
const transactionID = '1';
const category = 'Advertising';
diff --git a/tests/data/Invoice.ts b/tests/data/Invoice.ts
new file mode 100644
index 000000000000..c94c7ce816be
--- /dev/null
+++ b/tests/data/Invoice.ts
@@ -0,0 +1,290 @@
+// Test data for Invoices. The values come from the Onyx store in the app while manually testing.
+import type {OnyxEntry} from 'react-native-onyx';
+import CONST from '@src/CONST';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {InvoiceReceiver} from '@src/types/onyx/Report';
+
+const policy: OnyxEntry = {
+ id: 'CC048FA711B35B1F',
+ type: 'team',
+ name: "53019's Workspace",
+ role: 'admin',
+ owner: 'a1@53019.com',
+ ownerAccountID: 32,
+ isPolicyExpenseChatEnabled: true,
+ outputCurrency: 'USD',
+ autoReporting: true,
+ autoReportingFrequency: 'instant',
+ approvalMode: 'OPTIONAL',
+ harvesting: {
+ enabled: true,
+ jobID: 7206965285807173000,
+ },
+ customUnits: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ '39C3FF491F559': {
+ customUnitID: '39C3FF491F559',
+ name: 'Distance',
+ attributes: {
+ unit: 'mi',
+ },
+ rates: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ '928A74633831E': {
+ customUnitRateID: '928A74633831E',
+ name: 'Default Rate',
+ rate: 67,
+ enabled: true,
+ currency: 'USD',
+ },
+ },
+ defaultCategory: 'Car',
+ enabled: true,
+ },
+ },
+ areCategoriesEnabled: true,
+ areTagsEnabled: false,
+ areDistanceRatesEnabled: false,
+ areWorkflowsEnabled: false,
+ areReportFieldsEnabled: false,
+ areConnectionsEnabled: false,
+ employeeList: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'a1@53019.com': {
+ role: 'admin',
+ errors: {},
+ email: 'a1@53019.com',
+ forwardsTo: '',
+ submitsTo: 'a1@53019.com',
+ },
+ },
+ pendingFields: {},
+ chatReportIDAnnounce: 0,
+ chatReportIDAdmins: 1811331783036078,
+ approver: 'a1@53019.com',
+ areCompanyCardsEnabled: false,
+ areExpensifyCardsEnabled: false,
+ areInvoicesEnabled: true,
+ arePerDiemRatesEnabled: false,
+ areRulesEnabled: false,
+ autoReimbursement: {
+ limit: 0,
+ },
+ autoReimbursementLimit: 0,
+ autoReportingOffset: 1,
+ avatarURL: '',
+ defaultBillable: false,
+ description: '',
+ disabledFields: {
+ defaultBillable: true,
+ reimbursable: false,
+ },
+ fieldList: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ text_title: {
+ defaultValue: '{report:type} {report:startdate}',
+ deletable: true,
+ disabledOptions: [],
+ externalIDs: [],
+ fieldID: 'text_title',
+ isTax: false,
+ keys: [],
+ name: 'title',
+ orderWeight: 0,
+ target: 'expense',
+ type: 'formula',
+ values: [],
+ },
+ },
+ hasMultipleTagLists: false,
+ invoice: {
+ markUp: 0,
+ companyName: 'b1-53019',
+ companyWebsite: 'https://www.53019.com',
+ pendingFields: {},
+ bankAccount: {
+ stripeConnectAccountBalance: 0,
+ stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY',
+ transferBankAccountID: 29,
+ },
+ },
+ preventSelfApproval: false,
+ reimbursementChoice: 'reimburseManual',
+ requiresCategory: false,
+ requiresTag: false,
+ tax: {
+ trackingEnabled: false,
+ },
+ mccGroup: {
+ airlines: {
+ category: 'Travel',
+ groupID: 'airlines',
+ },
+ commuter: {
+ category: 'Car',
+ groupID: 'commuter',
+ },
+ gas: {
+ category: 'Car',
+ groupID: 'gas',
+ },
+ goods: {
+ category: 'Materials',
+ groupID: 'goods',
+ },
+ groceries: {
+ category: 'Meals and Entertainment',
+ groupID: 'groceries',
+ },
+ hotel: {
+ category: 'Travel',
+ groupID: 'hotel',
+ },
+ mail: {
+ category: 'Office Supplies',
+ groupID: 'mail',
+ },
+ meals: {
+ category: 'Meals and Entertainment',
+ groupID: 'meals',
+ },
+ rental: {
+ category: 'Travel',
+ groupID: 'rental',
+ },
+ services: {
+ category: 'Professional Services',
+ groupID: 'services',
+ },
+ taxi: {
+ category: 'Travel',
+ groupID: 'taxi',
+ },
+ uncategorized: {
+ category: 'Other',
+ groupID: 'uncategorized',
+ },
+ utilities: {
+ category: 'Utilities',
+ groupID: 'utilities',
+ },
+ },
+};
+
+const transaction: OnyxEntry = {
+ amount: 100,
+ attendees: [
+ {
+ email: 'a1@53019.com',
+ login: 'a1@53019.com',
+ displayName: 'a1',
+ avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png',
+ accountID: 32,
+ text: 'a1@53019.com',
+ selected: true,
+ reportID: '3634215302663162',
+ },
+ ],
+ comment: {
+ customUnit: {
+ customUnitRateID: '_FAKE_P2P_ID_',
+ },
+ },
+ created: '2024-12-13',
+ currency: 'USD',
+ iouRequestType: 'manual',
+ reportID: '3634215302663162',
+ transactionID: '1',
+ isFromGlobalCreate: true,
+ merchant: '(none)',
+ splitPayerAccountIDs: [32],
+ shouldShowOriginalAmount: true,
+ participants: [
+ {
+ accountID: 33,
+ login: 'b1@53019.com',
+ isPolicyExpenseChat: false,
+ reportID: '',
+ selected: true,
+ iouType: 'invoice',
+ },
+ {
+ policyID: 'CC048FA711B35B1F',
+ isSender: true,
+ selected: false,
+ iouType: 'invoice',
+ },
+ ],
+ tag: '',
+ category: '',
+ billable: false,
+};
+
+const convertedInvoiceChat: OnyxTypes.Report = {
+ chatType: CONST.REPORT.CHAT_TYPE.INVOICE,
+ currency: 'USD',
+ description: '',
+ hasOutstandingChildRequest: false,
+ hasOutstandingChildTask: false,
+
+ // The invoice receiver shouldn't have an accountID when the type is business,
+ // but this is to test that it still works if the value is present, so cast it to unknown
+ invoiceReceiver: {
+ accountID: 33,
+ policyID: '5F2F82F98C848CAA',
+ type: 'policy',
+ } as unknown as InvoiceReceiver,
+ isCancelledIOU: false,
+ isOwnPolicyExpenseChat: false,
+ isPinned: false,
+ isWaitingOnBankAccount: false,
+ lastActionType: 'REPORTPREVIEW',
+ lastActorAccountID: 32,
+ lastMessageHtml: 'paid $1.00',
+ lastMessageText: 'paid $1.00',
+ lastReadSequenceNumber: 0,
+ lastReadTime: '2024-12-13 19:45:28.942',
+ lastVisibleActionCreated: '2024-12-13 19:19:01.794',
+ lastVisibleActionLastModified: '2024-12-13 19:19:01.794',
+ managerID: 0,
+ nonReimbursableTotal: 0,
+ oldPolicyName: '',
+ ownerAccountID: 0,
+ participants: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ '32': {
+ notificationPreference: 'always',
+ role: 'admin',
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ '33': {
+ notificationPreference: 'always',
+ permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE, CONST.REPORT.PERMISSIONS.SHARE, CONST.REPORT.PERMISSIONS.OWN],
+ },
+ },
+ policyAvatar: '',
+ policyID: 'CC048FA711B35B1F',
+ policyName: "53019's Workspace",
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ private_isArchived: '',
+ reportID: '7605647250932303',
+ reportName: 'Chat Report',
+ stateNum: 0,
+ statusNum: 0,
+ total: 0,
+ type: 'chat',
+ unheldNonReimbursableTotal: 0,
+ unheldTotal: 0,
+ visibility: 'private',
+ welcomeMessage: '',
+ writeCapability: 'all',
+};
+
+type InvoiceTestData = {
+ policy: OnyxEntry;
+ transaction: OnyxEntry;
+ convertedInvoiceChat: OnyxTypes.Report;
+};
+
+export type {InvoiceTestData};
+export {policy, transaction, convertedInvoiceChat};
diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx
index b7e14bb5bd82..162711b85499 100644
--- a/tests/ui/LHNItemsPresence.tsx
+++ b/tests/ui/LHNItemsPresence.tsx
@@ -6,10 +6,12 @@ import type {ValueOf} from 'type-fest';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as TransactionUtils from '@libs/TransactionUtils';
import FontUtils from '@styles/utils/FontUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetailsList, Report, ViolationName} from '@src/types/onyx';
+import type {PersonalDetailsList, Report, ReportAction, ViolationName} from '@src/types/onyx';
import type {ReportCollectionDataSet} from '@src/types/onyx/Report';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
@@ -124,11 +126,11 @@ describe('SidebarLinksData', () => {
describe('Report that should be included in the LHN', () => {
it('should display the current active report', async () => {
- // When the SidebarLinks are rendered without a specified report ID.
+ // Given the SidebarLinks are rendered without a specified report ID.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report = createReport();
- // And the Onyx state is initialized with a report.
+ // When the Onyx state is initialized with a report.
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
@@ -147,14 +149,14 @@ describe('SidebarLinksData', () => {
});
it('should display draft report', async () => {
- // When SidebarLinks are rendered initially.
+ // Given SidebarLinks are rendered initially.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const draftReport = {
...createReport(false, [1, 2], 0),
writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
};
- // And Onyx state is initialized with a draft report.
+ // When Onyx state is initialized with a draft report.
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport,
});
@@ -172,11 +174,11 @@ describe('SidebarLinksData', () => {
});
it('should display pinned report', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report = createReport(false);
- // And the report is initialized in Onyx.
+ // When the report is initialized in Onyx.
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
@@ -196,10 +198,10 @@ describe('SidebarLinksData', () => {
});
it('should display the report with violations', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
- // And the report is initialized in Onyx.
+ // When the report is initialized in Onyx.
const report: Report = {
...createReport(true, undefined, undefined, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, TEST_POLICY_ID),
ownerAccountID: TEST_USER_ACCOUNT_ID,
@@ -209,7 +211,7 @@ describe('SidebarLinksData', () => {
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
- // The report should appear in the sidebar because it’s pinned.
+ // Then the report should appear in the sidebar because it’s pinned.
expect(getOptionRows()).toHaveLength(1);
await waitForBatchedUpdatesWithAct();
@@ -226,19 +228,19 @@ describe('SidebarLinksData', () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, [transactionViolation]);
- // The RBR icon should be shown
+ // Then the RBR icon should be shown
expect(screen.getByTestId('RBR Icon')).toBeOnTheScreen();
});
it('should display the report awaiting user action', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report: Report = {
...createReport(false),
hasOutstandingChildRequest: true,
};
- // And the report is initialized in Onyx.
+ // When the report is initialized in Onyx.
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
@@ -251,7 +253,7 @@ describe('SidebarLinksData', () => {
});
it('should display the archived report in the default mode', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const archivedReport: Report = {
...createReport(false),
@@ -270,30 +272,30 @@ describe('SidebarLinksData', () => {
await waitForBatchedUpdatesWithAct();
- // And the user is in the default mode
+ // When the user is in the default mode
await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT);
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs);
- // The report should appear in the sidebar because it's archived
+ // Then the report should appear in the sidebar because it's archived
expect(getOptionRows()).toHaveLength(1);
});
it('should display the selfDM report by default', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report = createReport(true, undefined, undefined, undefined, CONST.REPORT.CHAT_TYPE.SELF_DM, undefined);
- // And the selfDM is initialized in Onyx
+ // When the selfDM is initialized in Onyx
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
- // The selfDM report should appear in the sidebar by default
+ // Then the selfDM report should appear in the sidebar by default
expect(getOptionRows()).toHaveLength(1);
});
it('should display the unread report in the focus mode with the bold text', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report: Report = {
...createReport(undefined, undefined, undefined, undefined, undefined, true),
@@ -307,10 +309,10 @@ describe('SidebarLinksData', () => {
await waitForBatchedUpdatesWithAct();
- // And the user is in focus mode
+ // When the user is in focus mode
await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD);
- // The report should appear in the sidebar because it's unread
+ // Then the report should appear in the sidebar because it's unread
expect(getOptionRows()).toHaveLength(1);
// And the text is bold
@@ -324,18 +326,18 @@ describe('SidebarLinksData', () => {
lastReadTime: report.lastVisibleActionCreated,
});
- // The report should not disappear in the sidebar because we are in the focus mode
+ // Then the report should not disappear in the sidebar because we are in the focus mode
expect(getOptionRows()).toHaveLength(0);
});
});
describe('Report that should NOT be included in the LHN', () => {
it('should not display report with no participants', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report = LHNTestUtils.getFakeReport([]);
- // And a report with no participants is initialized in Onyx.
+ // When a report with no participants is initialized in Onyx.
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
@@ -345,11 +347,11 @@ describe('SidebarLinksData', () => {
});
it('should not display empty chat', async () => {
- // When the SidebarLinks are rendered.
+ // Given the SidebarLinks are rendered.
LHNTestUtils.getDefaultRenderedSidebarLinks();
const report = LHNTestUtils.getFakeReport([1, 2], 0);
- // And a report with no messages is initialized in Onyx
+ // When a report with no messages is initialized in Onyx
await initializeState({
[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
});
@@ -357,5 +359,168 @@ describe('SidebarLinksData', () => {
// Then the empty report should not appear in the sidebar.
expect(getOptionRows()).toHaveLength(0);
});
+
+ it('should not display the report marked as hidden', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const report: Report = {
+ ...createReport(),
+ participants: {
+ [TEST_USER_ACCOUNT_ID]: {
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ },
+ },
+ };
+
+ // When a report with notification preference set as hidden is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
+ });
+
+ // Then hidden report should not appear in the sidebar.
+ expect(getOptionRows()).toHaveLength(0);
+ });
+
+ it('should not display the report has empty notification preference', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const report = createReport(false, [2]);
+
+ // When a report with empty notification preference is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
+ });
+
+ // Then the report should not appear in the sidebar.
+ expect(getOptionRows()).toHaveLength(0);
+ });
+
+ it('should not display the report the user cannot access due to policy restrictions', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const report: Report = {
+ ...createReport(),
+ chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL,
+ lastMessageText: 'fake last message',
+ };
+
+ // When a default room is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
+ });
+
+ // And the defaultRooms beta is removed
+ await Onyx.merge(ONYXKEYS.BETAS, []);
+
+ // Then the default room should not appear in the sidebar.
+ expect(getOptionRows()).toHaveLength(0);
+ });
+
+ it('should not display the single transaction thread', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD');
+ const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID);
+ const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction(
+ 'create',
+ 100,
+ 'USD',
+ '',
+ [],
+ expenseTransaction.transactionID,
+ undefined,
+ expenseReport.reportID,
+ undefined,
+ false,
+ false,
+ undefined,
+ undefined,
+ );
+ const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport);
+ expenseCreatedAction.childReportID = transactionThreadReport.reportID;
+
+ // When a single transaction thread is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`]: transactionThreadReport,
+ });
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, {
+ [expenseCreatedAction.reportActionID]: expenseCreatedAction,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${expenseTransaction.transactionID}`, expenseTransaction);
+
+ // Then such report should not appear in the sidebar because the highest level context is on the workspace chat with GBR that is visible in the LHN
+ expect(getOptionRows()).toHaveLength(0);
+ });
+
+ it('should not display the report with parent message is pending removal', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const parentReport = createReport();
+ const report = createReport();
+ const parentReportAction: ReportAction = {
+ ...LHNTestUtils.getFakeReportAction(),
+ message: [
+ {
+ type: 'COMMENT',
+ html: 'hey',
+ text: 'hey',
+ isEdited: false,
+ whisperedTo: [],
+ isDeletedParentAction: false,
+ moderationDecision: {
+ decision: CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE,
+ },
+ },
+ ],
+ childReportID: report.reportID,
+ };
+ report.parentReportID = parentReport.reportID;
+ report.parentReportActionID = parentReportAction.reportActionID;
+
+ // When a report with parent message is pending removal is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, parentReport);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, {
+ [parentReportAction.reportActionID]: parentReportAction,
+ });
+
+ // Then report should not appear in the sidebar until the moderation feature decides if the message should be removed
+ expect(getOptionRows()).toHaveLength(0);
+ });
+
+ it('should not display the read report in the focus mode', async () => {
+ // Given the SidebarLinks are rendered
+ LHNTestUtils.getDefaultRenderedSidebarLinks();
+ const report: Report = {
+ ...createReport(),
+ lastMessageText: 'fake last message',
+ lastActorAccountID: TEST_USER_ACCOUNT_ID,
+ };
+
+ // When a read report that isn't empty is initialized in Onyx
+ await initializeState({
+ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report,
+ });
+
+ await waitForBatchedUpdatesWithAct();
+
+ // And the user is in default mode
+ await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT);
+
+ // Then the report should appear in the sidebar
+ expect(getOptionRows()).toHaveLength(1);
+
+ await waitForBatchedUpdatesWithAct();
+
+ // When the user is in focus mode
+ await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD);
+
+ // Then the report should not disappear in the sidebar because it's read
+ expect(getOptionRows()).toHaveLength(0);
+ });
});
});
diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts
index d34a07a9b16e..7c7182059bec 100644
--- a/tests/unit/APITest.ts
+++ b/tests/unit/APITest.ts
@@ -581,7 +581,7 @@ describe('APITests', () => {
});
});
- test('Read request should not stuck when SequentialQueue is paused an resumed', async () => {
+ test('Read request should not stuck when SequentialQueue is paused and resumed', async () => {
// Given 2 WRITE requests and 1 READ request where the first write request pauses the SequentialQueue
const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValueOnce({previousUpdateID: 1});
API.write('MockWriteCommandOne' as WriteCommand, {});
diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts
index c294b068a62d..2685a77836b3 100644
--- a/tests/unit/CardUtilsTest.ts
+++ b/tests/unit/CardUtilsTest.ts
@@ -2,7 +2,6 @@ import type {OnyxCollection} from 'react-native-onyx';
import CONST from '@src/CONST';
import * as CardUtils from '@src/libs/CardUtils';
import type * as OnyxTypes from '@src/types/onyx';
-import type {CompanyFeeds} from '@src/types/onyx/CardFeeds';
const shortDate = '0924';
const shortDateSlashed = '09/24';
@@ -13,7 +12,7 @@ const longDateHyphen = '09-2024';
const expectedMonth = '09';
const expectedYear = '2024';
-const customFeeds = {
+const companyCardsCustomFeedSettings = {
[CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: {
pending: true,
},
@@ -24,7 +23,7 @@ const customFeeds = {
liabilityType: 'personal',
},
};
-const customFeedsWithoutExpensifyBank = {
+const companyCardsCustomFeedSettingsWithoutExpensifyBank = {
[CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: {
pending: true,
},
@@ -32,7 +31,25 @@ const customFeedsWithoutExpensifyBank = {
liabilityType: 'personal',
},
};
-const directFeeds = {
+const companyCardsDirectFeedSettings = {
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: {
+ liabilityType: 'personal',
+ },
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]: {
+ liabilityType: 'personal',
+ },
+};
+const companyCardsSettingsWithoutExpensifyBank = {
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: {
+ pending: true,
+ },
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: {
+ liabilityType: 'personal',
+ },
+ ...companyCardsDirectFeedSettings,
+};
+
+const oAuthAccountDetails = {
[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: {
accountList: ['CREDIT CARD...6607', 'CREDIT CARD...5501'],
credentials: 'xxxxx',
@@ -115,27 +132,30 @@ const customFeedCardsList = {
'480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER',
},
} as unknown as OnyxTypes.WorkspaceCardsList;
-const allFeeds: CompanyFeeds = {...customFeeds, ...directFeeds};
const customFeedName = 'Custom feed name';
const cardFeedsCollection: OnyxCollection = {
+ // Policy with both custom and direct feeds
FAKE_ID_1: {
settings: {
companyCardNicknames: {
[CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: customFeedName,
},
- companyCards: customFeeds,
- oAuthAccountDetails: directFeeds,
+ companyCards: {...companyCardsCustomFeedSettings, ...companyCardsDirectFeedSettings},
+ oAuthAccountDetails,
},
},
+ // Policy with direct feeds only
FAKE_ID_2: {
settings: {
- oAuthAccountDetails: directFeeds,
+ companyCards: companyCardsDirectFeedSettings,
+ oAuthAccountDetails,
},
},
+ // Policy with custom feeds only
FAKE_ID_3: {
settings: {
- companyCards: customFeeds,
+ companyCards: companyCardsCustomFeedSettings,
},
},
};
@@ -198,19 +218,19 @@ describe('CardUtils', () => {
});
describe('getCompanyFeeds', () => {
- it('Should return both custom and direct feeds if exists', () => {
+ it('Should return both custom and direct feeds with filtered out "Expensify Card" bank', () => {
const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_1);
- expect(companyFeeds).toStrictEqual(allFeeds);
+ expect(companyFeeds).toStrictEqual(companyCardsSettingsWithoutExpensifyBank);
});
it('Should return direct feeds only since custom feeds are not exist', () => {
const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_2);
- expect(companyFeeds).toStrictEqual(directFeeds);
+ expect(companyFeeds).toStrictEqual(companyCardsDirectFeedSettings);
});
- it('Should return custom feeds only since direct feeds are not exist', () => {
+ it('Should return custom feeds only with filtered out "Expensify Card" bank since direct feeds are not exist', () => {
const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_3);
- expect(companyFeeds).toStrictEqual(customFeeds);
+ expect(companyFeeds).toStrictEqual(companyCardsCustomFeedSettingsWithoutExpensifyBank);
});
it('Should return empty object if undefined is passed', () => {
@@ -219,23 +239,6 @@ describe('CardUtils', () => {
});
});
- describe('removeExpensifyCardFromCompanyCards', () => {
- it('Should return custom feeds without filtered out "Expensify Card" bank', () => {
- const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(cardFeedsCollection.FAKE_ID_3);
- expect(companyFeeds).toStrictEqual(customFeedsWithoutExpensifyBank);
- });
-
- it('Should return direct feeds without any updates, since there were no "Expensify Card" bank', () => {
- const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(cardFeedsCollection.FAKE_ID_2);
- expect(companyFeeds).toStrictEqual(directFeeds);
- });
-
- it('Should return empty object if undefined is passed', () => {
- const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(undefined);
- expect(companyFeeds).toStrictEqual({});
- });
- });
-
describe('getSelectedFeed', () => {
it('Should return last selected custom feed', () => {
const lastSelectedCustomFeed = CONST.COMPANY_CARD.FEED_BANK_NAME.VISA;
@@ -357,13 +360,13 @@ describe('CardUtils', () => {
});
it('Should return filtered direct feed cards list with a single card', () => {
- const cardsList = CardUtils.getFilteredCardList(directFeedCardsSingleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]);
+ const cardsList = CardUtils.getFilteredCardList(directFeedCardsSingleList, oAuthAccountDetails[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]);
// eslint-disable-next-line @typescript-eslint/naming-convention
expect(cardsList).toStrictEqual({'CREDIT CARD...6607': 'CREDIT CARD...6607'});
});
it('Should return filtered direct feed cards list with multiple cards', () => {
- const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]);
+ const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, oAuthAccountDetails[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]);
// eslint-disable-next-line @typescript-eslint/naming-convention
expect(cardsList).toStrictEqual({'CREDIT CARD...1233': 'CREDIT CARD...1233', 'CREDIT CARD...3333': 'CREDIT CARD...3333', 'CREDIT CARD...7788': 'CREDIT CARD...7788'});
});
diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts
index e1eda3171355..0f1f68c1cae3 100644
--- a/tests/unit/ReportUtilsTest.ts
+++ b/tests/unit/ReportUtilsTest.ts
@@ -1,15 +1,16 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {addDays, format as formatDate} from 'date-fns';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import DateUtils from '@libs/DateUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
-import * as TransactionUtils from '@src/libs/TransactionUtils';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx';
+import type {Beta, PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx';
import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
import * as NumberUtils from '../../src/libs/NumberUtils';
+import {convertedInvoiceChat} from '../data/Invoice';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import {fakePersonalDetails} from '../utils/LHNTestUtils';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
@@ -545,7 +546,10 @@ describe('ReportUtils', () => {
parentReportID: '101',
policyID: paidPolicy.id,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [
+ currentUserAccountID,
+ participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID,
+ ]);
expect(moneyRequestOptions.length).toBe(0);
});
});
@@ -558,7 +562,10 @@ describe('ReportUtils', () => {
...LHNTestUtils.getFakeReport(),
chatType,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [
+ currentUserAccountID,
+ participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID,
+ ]);
return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT);
});
expect(onlyHaveSplitOption).toBe(true);
@@ -605,7 +612,7 @@ describe('ReportUtils', () => {
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
managerID: currentUserAccountID,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]);
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
});
@@ -618,7 +625,7 @@ describe('ReportUtils', () => {
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
managerID: currentUserAccountID,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]);
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
});
@@ -667,7 +674,10 @@ describe('ReportUtils', () => {
outputCurrency: '',
isPolicyExpenseChatEnabled: false,
} as const;
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [
+ currentUserAccountID,
+ participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID,
+ ]);
expect(moneyRequestOptions.length).toBe(2);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK)).toBe(true);
@@ -682,7 +692,7 @@ describe('ReportUtils', () => {
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
managerID: currentUserAccountID,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]);
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
});
@@ -695,7 +705,7 @@ describe('ReportUtils', () => {
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
managerID: currentUserAccountID,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]);
expect(moneyRequestOptions.length).toBe(1);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
});
@@ -738,7 +748,10 @@ describe('ReportUtils', () => {
managerID: currentUserAccountID,
ownerAccountID: currentUserAccountID,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [
+ currentUserAccountID,
+ participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID,
+ ]);
expect(moneyRequestOptions.length).toBe(2);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK)).toBe(true);
@@ -752,7 +765,7 @@ describe('ReportUtils', () => {
...LHNTestUtils.getFakeReport(),
type: CONST.REPORT.TYPE.CHAT,
};
- const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]);
+ const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]);
expect(moneyRequestOptions.length).toBe(3);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true);
expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true);
@@ -902,7 +915,7 @@ describe('ReportUtils', () => {
const reportActionCollectionDataSet = toCollectionDataSet(
ONYXKEYS.COLLECTION.REPORT_ACTIONS,
reportActions.map((reportAction) => ({[reportAction.reportActionID]: reportAction})),
- (actions) => Object.values(actions).at(0)?.reportActionID ?? '',
+ (actions) => Object.values(actions).at(0)?.reportActionID,
);
Onyx.multiSet({
...reportCollectionDataSet,
@@ -1413,5 +1426,176 @@ describe('ReportUtils', () => {
}),
).toBeTruthy();
});
+
+ it('should return false when the report is marked as hidden', () => {
+ const report: Report = {
+ ...LHNTestUtils.getFakeReport(),
+ participants: {
+ '1': {
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ },
+ },
+ };
+ const currentReportId = '';
+ const isInFocusMode = true;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report does not have participants', () => {
+ const report = LHNTestUtils.getFakeReport([]);
+ const currentReportId = '';
+ const isInFocusMode = true;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report is the report that the user cannot access due to policy restrictions', () => {
+ const report: Report = {
+ ...LHNTestUtils.getFakeReport(),
+ chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL,
+ };
+ const currentReportId = '';
+ const isInFocusMode = false;
+ const betas: Beta[] = [];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report is the single transaction thread', async () => {
+ const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD');
+ const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID);
+ const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction(
+ 'create',
+ 100,
+ 'USD',
+ '',
+ [],
+ expenseTransaction.transactionID,
+ undefined,
+ expenseReport.reportID,
+ undefined,
+ false,
+ false,
+ undefined,
+ undefined,
+ );
+ const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport);
+ expenseCreatedAction.childReportID = transactionThreadReport.reportID;
+ const currentReportId = '1';
+ const isInFocusMode = false;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, {
+ [expenseCreatedAction.reportActionID]: expenseCreatedAction,
+ });
+ expect(
+ ReportUtils.shouldReportBeInOptionList({
+ report: transactionThreadReport,
+ currentReportId,
+ isInFocusMode,
+ betas,
+ policies: {},
+ doesReportHaveViolations: false,
+ excludeEmptyChats: false,
+ }),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report is empty chat and the excludeEmptyChats setting is true', () => {
+ const report = LHNTestUtils.getFakeReport();
+ const currentReportId = '';
+ const isInFocusMode = false;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: true}),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the user’s email is domain-based and the includeDomainEmail is false', () => {
+ const report = LHNTestUtils.getFakeReport();
+ const currentReportId = '';
+ const isInFocusMode = false;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({
+ report,
+ currentReportId,
+ isInFocusMode,
+ betas,
+ policies: {},
+ doesReportHaveViolations: false,
+ login: '+@domain.com',
+ excludeEmptyChats: false,
+ includeDomainEmail: false,
+ }),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report has the parent message is pending removal', async () => {
+ const parentReport = LHNTestUtils.getFakeReport();
+ const report = LHNTestUtils.getFakeReport();
+ const parentReportAction: ReportAction = {
+ ...LHNTestUtils.getFakeReportAction(),
+ message: [
+ {
+ type: 'COMMENT',
+ html: 'hey',
+ text: 'hey',
+ isEdited: false,
+ whisperedTo: [],
+ isDeletedParentAction: false,
+ moderationDecision: {
+ decision: CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE,
+ },
+ },
+ ],
+ childReportID: report.reportID,
+ };
+ report.parentReportID = parentReport.reportID;
+ report.parentReportActionID = parentReportAction.reportActionID;
+ const currentReportId = '';
+ const isInFocusMode = false;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, parentReport);
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, {
+ [parentReportAction.reportActionID]: parentReportAction,
+ });
+
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}),
+ ).toBeFalsy();
+ });
+
+ it('should return false when the report is read and we are in the focus mode', () => {
+ const report = LHNTestUtils.getFakeReport();
+ const currentReportId = '';
+ const isInFocusMode = true;
+ const betas = [CONST.BETAS.DEFAULT_ROOMS];
+ expect(
+ ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}),
+ ).toBeFalsy();
+ });
+ });
+
+ describe('getInvoiceChatByParticipants', () => {
+ it('only returns an invoice chat if the receiver type matches', () => {
+ // Given an invoice chat that has been converted from an individual to policy receiver type
+ const reports: OnyxCollection = {
+ [convertedInvoiceChat.reportID]: convertedInvoiceChat,
+ };
+
+ // When we send another invoice to the individual from global create and call getInvoiceChatByParticipants
+ const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(33, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, convertedInvoiceChat.policyID, reports);
+
+ // Then no invoice chat should be returned because the receiver type does not match
+ expect(invoiceChatReport).toBeUndefined();
+ });
});
});
diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts
index a6c847e5f7f4..558d09e6a0fa 100644
--- a/tests/unit/TransactionUtilsTest.ts
+++ b/tests/unit/TransactionUtilsTest.ts
@@ -104,9 +104,9 @@ describe('TransactionUtils', () => {
taxRates: CONST.DEFAULT_TAX,
rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')},
};
+ const transaction = generateTransaction();
// When retrieving the tax from the associated category
- const transaction = generateTransaction();
const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
// Then it should return the associated tax code and amount
@@ -123,9 +123,9 @@ describe('TransactionUtils', () => {
taxRates: CONST.DEFAULT_TAX,
rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')},
};
+ const transaction = generateTransaction();
// When retrieving the tax from a category that is not associated with the tax expense rules
- const transaction = generateTransaction();
const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy);
// Then it should return the default tax code and amount
@@ -133,22 +133,103 @@ describe('TransactionUtils', () => {
expect(categoryTaxAmount).toBe(0);
});
- it('should return and undefined tax when there are no policy tax expense rules', () => {
- // Given a policy without tax expense rules
+ it("should return the foreign default tax when the category doesn't match the tax expense rules and using a foreign currency", () => {
+ // Given a policy with tax expense rules associated with a category and a transaction with a foreign currency
+ const ruleCategory = 'Advertising';
+ const selectedCategory = 'Benefits';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: {
+ ...CONST.DEFAULT_TAX,
+ foreignTaxDefault: 'id_TAX_RATE_2',
+ taxes: {
+ ...CONST.DEFAULT_TAX.taxes,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ id_TAX_RATE_2: {
+ name: 'Tax rate 2',
+ value: '10%',
+ },
+ },
+ },
+ outputCurrency: 'IDR',
+ rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')},
+ };
+ const transaction = generateTransaction();
+
+ // When retrieving the tax from a category that is not associated with the tax expense rules
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy);
+
+ // Then it should return the default tax code and amount
+ expect(categoryTaxCode).toBe('id_TAX_RATE_2');
+ expect(categoryTaxAmount).toBe(9);
+ });
+
+ describe('should return undefined tax', () => {
+ it('if the transaction type is distance', () => {
+ // Given a policy with tax expense rules associated with a category
+ const category = 'Advertising';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')},
+ };
+ const transaction: Transaction = {
+ ...generateTransaction(),
+ iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
+ };
+
+ // When retrieving the tax from the associated category
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
+
+ // Then it should return undefined for both the tax code and the tax amount
+ expect(categoryTaxCode).toBe(undefined);
+ expect(categoryTaxAmount).toBe(undefined);
+ });
+
+ it('if there are no policy tax expense rules', () => {
+ // Given a policy without tax expense rules
+ const category = 'Advertising';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ const transaction = generateTransaction();
+
+ // When retrieving the tax from a category
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
+
+ // Then it should return undefined for both the tax code and the tax amount
+ expect(categoryTaxCode).toBe(undefined);
+ expect(categoryTaxAmount).toBe(undefined);
+ });
+ });
+ });
+
+ describe('getUpdatedTransaction', () => {
+ it('should return updated category and tax when updating category with a category tax rules', () => {
+ // Given a policy with tax expense rules associated with a category
const category = 'Advertising';
+ const taxCode = 'id_TAX_RATE_1';
const fakePolicy: Policy = {
...createRandomPolicy(0),
taxRates: CONST.DEFAULT_TAX,
- rules: {},
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, taxCode)},
};
-
- // When retrieving the tax from a category
const transaction = generateTransaction();
- const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
- // Then it should return undefined for both the tax code and the tax amount
- expect(categoryTaxCode).toBe(undefined);
- expect(categoryTaxAmount).toBe(undefined);
+ // When updating the transaction category to the category associated with the rule
+ const updatedTransaction = TransactionUtils.getUpdatedTransaction({
+ transaction,
+ isFromExpenseReport: true,
+ policy: fakePolicy,
+ transactionChanges: {category},
+ });
+
+ // Then the updated transaction should contain the tax from the matched rule
+ expect(updatedTransaction.category).toBe(category);
+ expect(updatedTransaction.taxCode).toBe(taxCode);
+ expect(updatedTransaction.taxAmount).toBe(5);
});
});
});