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 e3f2eb7966e3..86196f13d662 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -74,9 +74,9 @@ function MoneyRequestPreviewContent({
const route = useRoute>();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`);
const [session] = useOnyx(ONYXKEYS.SESSION);
- const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`);
const policy = PolicyUtils.getPolicy(iouReport?.policyID);
const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
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 605fb284bf24..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,
@@ -60,12 +61,15 @@ function BaseTextInput(
multiline = false,
autoCorrect = true,
prefixCharacter = '',
+ suffixCharacter = '',
inputID,
isMarkdownEnabled = false,
excludedMarkdownStyles = [],
shouldShowClearButton = false,
prefixContainerStyle = [],
prefixStyle = [],
+ suffixContainerStyle = [],
+ suffixStyle = [],
contentWidth,
loadingSpinnerStyle,
...props
@@ -86,7 +90,7 @@ function BaseTextInput(
// Disabling this line for safeness as nullish coalescing works only if the value is undefined or null
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const initialValue = value || defaultValue || '';
- const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter;
+ const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter || !!suffixCharacter;
const isMultiline = multiline || autoGrowHeight;
const [isFocused, setIsFocused] = useState(false);
@@ -140,13 +144,13 @@ function BaseTextInput(
const deactivateLabel = useCallback(() => {
const inputValue = value ?? '';
- if (!!forceActiveLabel || inputValue.length !== 0 || prefixCharacter) {
+ if (!!forceActiveLabel || inputValue.length !== 0 || prefixCharacter || suffixCharacter) {
return;
}
animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE);
isLabelActive.current = false;
- }, [animateLabel, forceActiveLabel, prefixCharacter, value]);
+ }, [animateLabel, forceActiveLabel, prefixCharacter, suffixCharacter, value]);
const onFocus = (event: NativeSyntheticEvent) => {
inputProps.onFocus?.(event);
@@ -249,11 +253,12 @@ function BaseTextInput(
// Disabling this line for safeness as nullish coalescing works only if the value is undefined or null, and errorText can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const inputHelpText = errorText || hint;
- const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined;
+ const placeholderValue = !!prefixCharacter || !!suffixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined;
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},
@@ -261,7 +266,7 @@ function BaseTextInput(
]);
const inputPaddingLeft = !!prefixCharacter && StyleUtils.getPaddingLeft(StyleUtils.getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft);
-
+ const inputPaddingRight = !!suffixCharacter && StyleUtils.getPaddingRight(StyleUtils.getCharacterPadding(suffixCharacter) + styles.pr1.paddingRight);
return (
<>
@@ -349,6 +354,7 @@ function BaseTextInput(
inputStyle,
(!hasLabel || isMultiline) && styles.pv0,
inputPaddingLeft,
+ inputPaddingRight,
inputProps.secureTextEntry && styles.secureInput,
!isMultiline && {height, lineHeight: undefined},
@@ -378,6 +384,17 @@ function BaseTextInput(
defaultValue={defaultValue}
markdownStyle={markdownStyle}
/>
+ {!!suffixCharacter && (
+
+
+ {suffixCharacter}
+
+
+ )}
{isFocused && !isReadOnly && shouldShowClearButton && !!value && setValue('')} />}
{!!inputProps.isLoading && (
;
+} & 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/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 e4480c511931..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));
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 21c346f613b5..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') {
@@ -1258,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(
>
{
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/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);
});
});
});