diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9343423c9f14..ba41291038f3 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,8 +107,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001048016
- versionName "1.4.80-16"
+ versionCode 1001048107
+ versionName "1.4.81-7"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index ad738e44ab44..5fd65532c021 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -71,7 +71,7 @@ platforms:
- href: integrations
title: Integrations
- icon: /assets/images/workflow.svg
+ icon: /assets/images/simple-illustration__monitor-remotesync.svg
description: Integrate with accounting or HR software to streamline expense approvals.
- href: spending-insights
@@ -131,7 +131,7 @@ platforms:
- href: connections
title: Connections
- icon: /assets/images/workflow.svg
+ icon: /assets/images/simple-illustration__monitor-remotesync.svg
description: Connect to accounting software to streamline expense approvals.
- href: settings
diff --git a/docs/assets/images/simple-illustration__monitor-remotesync.svg b/docs/assets/images/simple-illustration__monitor-remotesync.svg
new file mode 100644
index 000000000000..e4ed84a35851
--- /dev/null
+++ b/docs/assets/images/simple-illustration__monitor-remotesync.svg
@@ -0,0 +1,30 @@
+
+
\ No newline at end of file
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index ad1d743d778d..e782155beee7 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.80
+ 1.4.81
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.80.16
+ 1.4.81.7
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 23bdf74a3648..c18015ff0858 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.80
+ 1.4.81
CFBundleSignature
????
CFBundleVersion
- 1.4.80.16
+ 1.4.81.7
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 9f8cc7b1612e..2feda22f8a36 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName
$(PRODUCT_NAME)
CFBundleShortVersionString
- 1.4.80
+ 1.4.81
CFBundleVersion
- 1.4.80.16
+ 1.4.81.7
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index c138d1b27f61..aca46d6b18ed 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1852,7 +1852,7 @@ PODS:
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- - RNLiveMarkdown (0.1.82):
+ - RNLiveMarkdown (0.1.83):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -1870,9 +1870,9 @@ PODS:
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNLiveMarkdown/common (= 0.1.82)
+ - RNLiveMarkdown/common (= 0.1.83)
- Yoga
- - RNLiveMarkdown/common (0.1.82):
+ - RNLiveMarkdown/common (0.1.83):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2589,7 +2589,7 @@ SPEC CHECKSUMS:
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
- RNLiveMarkdown: d160a948e52282067439585c89a3962582c082ce
+ RNLiveMarkdown: 88030b7d9a31f5f6e67743df48ad952d64513b4a
RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81
rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c
RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216
diff --git a/package-lock.json b/package-lock.json
index aaef29c6fad9..6fb017197b28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,19 @@
{
"name": "new.expensify",
- "version": "1.4.80-16",
+ "version": "1.4.81-7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.80-16",
+ "version": "1.4.81-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.82",
+ "@expensify/react-native-live-markdown": "0.1.83",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
@@ -3558,9 +3558,9 @@
}
},
"node_modules/@expensify/react-native-live-markdown": {
- "version": "0.1.82",
- "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.82.tgz",
- "integrity": "sha512-w/K2+0d1sAYvyLVpPv1ufDOTaj4y96Z362N3JDN+SDUmPQN2MvVGwsTL0ltzdw78yd62azFcQl6th7P6l62THQ==",
+ "version": "0.1.83",
+ "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.83.tgz",
+ "integrity": "sha512-xGn1P9FbFVueEF8BNKJJ4dQb0wPtsAvrrxND9pwVQT35ZL5cu1KZ4o6nzCqtesISPRB8Dw9Zx0ftIZy2uCQyzA==",
"workspaces": [
"parser",
"example",
diff --git a/package.json b/package.json
index ee1ad32a0067..dcd8fd60863c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.80-16",
+ "version": "1.4.81-7",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -65,7 +65,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@dotlottie/react-player": "^1.6.3",
- "@expensify/react-native-live-markdown": "0.1.82",
+ "@expensify/react-native-live-markdown": "0.1.83",
"@expo/metro-runtime": "~3.1.1",
"@formatjs/intl-datetimeformat": "^6.10.0",
"@formatjs/intl-listformat": "^7.2.2",
diff --git a/src/App.tsx b/src/App.tsx
index 6316fa80fba1..9eda57816e9d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,7 +6,7 @@ import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
import ActiveElementRoleProvider from './components/ActiveElementRoleProvider';
-import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider';
+import ActiveWorkspaceContextProvider from './components/ActiveWorkspaceProvider';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
import ComposeProviders from './components/ComposeProviders';
import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground';
diff --git a/src/CONST.ts b/src/CONST.ts
index b0e3ab8c3af4..d3132b17a2ea 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1829,7 +1829,7 @@ const CONST = {
NAME_DISTANCE: 'Distance',
DISTANCE_UNIT_MILES: 'mi',
DISTANCE_UNIT_KILOMETERS: 'km',
- MILEAGE_IRS_RATE: 0.655,
+ MILEAGE_IRS_RATE: 0.67,
DEFAULT_RATE: 'Default Rate',
RATE_DECIMALS: 3,
FAKE_P2P_ID: '_FAKE_P2P_ID_',
@@ -3394,6 +3394,11 @@ const CONST = {
* @deprecated Please stop using the accessibilityRole prop and use the role prop instead.
*/
IMAGE: 'image',
+
+ /**
+ * @deprecated Please stop using the accessibilityRole prop and use the role prop instead.
+ */
+ TEXTBOX: 'textbox',
},
/**
* Acceptable values for the `role` attribute on react native components.
@@ -4753,6 +4758,7 @@ const CONST = {
SESSION_STORAGE_KEYS: {
INITIAL_URL: 'INITIAL_URL',
+ ACTIVE_WORKSPACE_ID: 'ACTIVE_WORKSPACE_ID',
},
RESERVATION_TYPE: {
diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx b/src/components/ActiveWorkspaceProvider/index.tsx
similarity index 80%
rename from src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx
rename to src/components/ActiveWorkspaceProvider/index.tsx
index 884b9a2a2d95..bc7260cdf10b 100644
--- a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx
+++ b/src/components/ActiveWorkspaceProvider/index.tsx
@@ -1,6 +1,6 @@
import React, {useMemo, useState} from 'react';
+import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
-import ActiveWorkspaceContext from './ActiveWorkspaceContext';
function ActiveWorkspaceContextProvider({children}: ChildrenProps) {
const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined);
@@ -10,7 +10,7 @@ function ActiveWorkspaceContextProvider({children}: ChildrenProps) {
activeWorkspaceID,
setActiveWorkspaceID,
}),
- [activeWorkspaceID],
+ [activeWorkspaceID, setActiveWorkspaceID],
);
return {children};
diff --git a/src/components/ActiveWorkspaceProvider/index.website.tsx b/src/components/ActiveWorkspaceProvider/index.website.tsx
new file mode 100644
index 000000000000..82e46d70f896
--- /dev/null
+++ b/src/components/ActiveWorkspaceProvider/index.website.tsx
@@ -0,0 +1,29 @@
+import React, {useCallback, useMemo, useState} from 'react';
+import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
+import CONST from '@src/CONST';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+
+function ActiveWorkspaceContextProvider({children}: ChildrenProps) {
+ const [activeWorkspaceID, updateActiveWorkspaceID] = useState(undefined);
+
+ const setActiveWorkspaceID = useCallback((workspaceID: string | undefined) => {
+ updateActiveWorkspaceID(workspaceID);
+ if (workspaceID && sessionStorage) {
+ sessionStorage?.setItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID, workspaceID);
+ } else {
+ sessionStorage?.removeItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID);
+ }
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ activeWorkspaceID,
+ setActiveWorkspaceID,
+ }),
+ [activeWorkspaceID, setActiveWorkspaceID],
+ );
+
+ return {children};
+}
+
+export default ActiveWorkspaceContextProvider;
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx
index e5eb09691eba..e641a0c2218a 100644
--- a/src/components/Breadcrumbs.tsx
+++ b/src/components/Breadcrumbs.tsx
@@ -68,7 +68,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) {
/
{secondaryBreadcrumb.text}
diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx
index 2fbd635ed42e..5e5f126da773 100644
--- a/src/components/Composer/index.native.tsx
+++ b/src/components/Composer/index.native.tsx
@@ -28,6 +28,7 @@ function Composer(
// On Android the selection prop is required on the TextInput but this prop has issues on IOS
selection,
value,
+ isGroupPolicyReport = false,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -36,7 +37,7 @@ function Composer(
const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
- const markdownStyle = useMarkdownStyle(value);
+ const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []);
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 14762b2d4bc1..eae27440d175 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -71,6 +71,7 @@ function Composer(
isReportActionCompose = false,
isComposerFullSize = false,
shouldContainScroll = false,
+ isGroupPolicyReport = false,
...props
}: ComposerProps,
ref: ForwardedRef,
@@ -78,7 +79,7 @@ function Composer(
const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
const styles = useThemeStyles();
- const markdownStyle = useMarkdownStyle(value);
+ const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []);
const StyleUtils = useStyleUtils();
const textRef = useRef(null);
const textInput = useRef(null);
diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts
index 531bcd03f8bf..0ff91111bd07 100644
--- a/src/components/Composer/types.ts
+++ b/src/components/Composer/types.ts
@@ -70,6 +70,9 @@ type ComposerProps = TextInputProps & {
/** Should make the input only scroll inside the element avoid scroll out to parent */
shouldContainScroll?: boolean;
+
+ /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */
+ isGroupPolicyReport?: boolean;
};
export type {TextSelection, ComposerProps};
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index 6405e3026b1a..7a2650ab5bd6 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -165,8 +165,8 @@ function LHNOptionsList({
);
const extraData = useMemo(
- () => [reportActions, reports, policy, personalDetails, data.length, draftComments],
- [reportActions, reports, policy, personalDetails, data.length, draftComments],
+ () => [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode],
+ [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode],
);
const previousOptionMode = usePrevious(optionMode);
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index ec52a6158ad7..5ea35c2d3c3f 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -27,6 +27,7 @@ import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
+import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import SettlementButton from './SettlementButton';
@@ -79,7 +80,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction;
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState();
- const [requestType, setRequestType] = useState<'pay' | 'approve'>();
+ const [requestType, setRequestType] = useState();
const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
const isPayer = ReportUtils.isPayer(session, moneyRequestReport);
@@ -124,7 +125,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
return;
}
setPaymentType(type);
- setRequestType('pay');
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) {
setIsHoldMenuVisible(true);
} else if (ReportUtils.isInvoiceReport(moneyRequestReport)) {
@@ -135,7 +136,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
};
const confirmApproval = () => {
- setRequestType('approve');
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) {
setIsHoldMenuVisible(true);
} else {
diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index 1cccfdc720b3..fb17297dc642 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -7,6 +7,7 @@ import * as Browser from '@libs/Browser';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import getOperatingSystem from '@libs/getOperatingSystem';
import * as MoneyRequestUtils from '@libs/MoneyRequestUtils';
+import shouldIgnoreSelectionWhenUpdatedManually from '@libs/shouldIgnoreSelectionWhenUpdatedManually';
import CONST from '@src/CONST';
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';
import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol';
@@ -140,6 +141,8 @@ function MoneyRequestAmountInput(
});
const forwardDeletePressedRef = useRef(false);
+ // The ref is used to ignore any onSelectionChange event that happens while we are updating the selection manually in setNewAmount
+ const willSelectionBeUpdatedManually = useRef(false);
/**
* Sets the selection and the amount accordingly to the value passed to the input
@@ -162,6 +165,7 @@ function MoneyRequestAmountInput(
// setCurrentAmount contains another setState(setSelection) making it error-prone since it is leading to setSelection being called twice for a single setCurrentAmount call. This solution introducing the hasSelectionBeenSet flag was chosen for its simplicity and lower risk of future errors https://github.com/Expensify/App/issues/23300#issuecomment-1766314724.
+ willSelectionBeUpdatedManually.current = true;
let hasSelectionBeenSet = false;
setCurrentAmount((prevAmount) => {
const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(finalAmount);
@@ -169,6 +173,7 @@ function MoneyRequestAmountInput(
if (!hasSelectionBeenSet) {
hasSelectionBeenSet = true;
setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length));
+ willSelectionBeUpdatedManually.current = false;
}
onAmountChange?.(strippedAmount);
return strippedAmount;
@@ -294,6 +299,10 @@ function MoneyRequestAmountInput(
selectedCurrencyCode={currency}
selection={selection}
onSelectionChange={(e: NativeSyntheticEvent) => {
+ if (shouldIgnoreSelectionWhenUpdatedManually && willSelectionBeUpdatedManually.current) {
+ willSelectionBeUpdatedManually.current = false;
+ return;
+ }
if (!shouldUpdateSelection) {
return;
}
diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx
index 6e81c9d57bc8..01896fb0a3cb 100644
--- a/src/components/ProcessMoneyReportHoldMenu.tsx
+++ b/src/components/ProcessMoneyReportHoldMenu.tsx
@@ -4,11 +4,15 @@ import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils';
import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
import DecisionModal from './DecisionModal';
+type ActionHandledType = DeepValueOf;
+
type ProcessMoneyReportHoldMenuProps = {
/** The chat report this report is linked to */
chatReport: OnyxEntry;
@@ -35,7 +39,7 @@ type ProcessMoneyReportHoldMenuProps = {
paymentType?: PaymentMethodType;
/** Type of action handled */
- requestType?: 'pay' | 'approve';
+ requestType?: ActionHandledType;
};
function ProcessMoneyReportHoldMenu({
@@ -50,7 +54,7 @@ function ProcessMoneyReportHoldMenu({
moneyRequestReport,
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
- const isApprove = requestType === 'approve';
+ const isApprove = requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE;
const onSubmit = (full: boolean) => {
if (isApprove) {
@@ -82,3 +86,4 @@ function ProcessMoneyReportHoldMenu({
ProcessMoneyReportHoldMenu.displayName = 'ProcessMoneyReportHoldMenu';
export default ProcessMoneyReportHoldMenu;
+export type {ActionHandledType};
diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx
index 65eb9b82ecdc..946856ecec37 100644
--- a/src/components/ReceiptImage.tsx
+++ b/src/components/ReceiptImage.tsx
@@ -76,9 +76,6 @@ type ReceiptImageProps = (
/** The colod of the fallback icon */
fallbackIconColor?: string;
-
- /** Whether the component is hovered */
- isHovered?: boolean;
};
function ReceiptImage({
@@ -96,7 +93,6 @@ function ReceiptImage({
fallbackIconSize,
shouldUseInitialObjectPosition = false,
fallbackIconColor,
- isHovered = false,
}: ReceiptImageProps) {
const styles = useThemeStyles();
@@ -134,7 +130,6 @@ function ReceiptImage({
fallbackIconSize={fallbackIconSize}
fallbackIconColor={fallbackIconColor}
objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP}
- isHovered={isHovered}
/>
);
}
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index 4f91b2084b45..fd82e723c6b9 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -10,7 +10,6 @@ import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
-import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -92,7 +91,6 @@ function MoneyRequestAction({
}
const childReportID = action?.childReportID ?? '0';
- Report.openReport(childReportID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
};
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index b6d6915279cb..e03ed59ffb79 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -163,11 +163,7 @@ function MoneyRequestPreviewContent({
const isTooLong = violationsCount > 1 || violationMessage.length > 15;
const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors;
- message += ` ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`;
- if (shouldShowHoldMessage) {
- message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.hold')}`;
- }
- return message;
+ return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`;
}
const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction);
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 12a325f6aa19..cc99a3f6e108 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -171,7 +171,8 @@ function MoneyRequestView({
const {getViolationsForField} = useViolations(transactionViolations ?? []);
const hasViolations = useCallback(
- (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0,
+ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean =>
+ !!canUseViolations && getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0,
[canUseViolations, getViolationsForField],
);
@@ -238,7 +239,7 @@ function MoneyRequestView({
const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction;
const getErrorForField = useCallback(
- (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => {
+ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
// Checks applied when creating a new expense
// NOTE: receipt field can return multiple violations, so we need to handle it separately
const fieldChecks: Partial> = {
@@ -264,14 +265,14 @@ function MoneyRequestView({
}
// Return violations if there are any
- if (canUseViolations && hasViolations(field, data)) {
- const violations = getViolationsForField(field, data);
+ if (hasViolations(field, data, policyHasDependentTags, tagValue)) {
+ const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue);
return ViolationsUtils.getViolationTranslation(violations[0], translate);
}
return '';
},
- [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, canUseViolations, hasViolations, translate, getViolationsForField],
+ [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, hasViolations, translate, getViolationsForField],
);
const distanceRequestFields = canUseP2PDistanceRequests ? (
@@ -333,6 +334,37 @@ function MoneyRequestView({
...parentReportAction?.errors,
};
+ const tagList = policyTagLists.map(({name, orderWeight}, index) => {
+ const tagError = getErrorForField(
+ 'tag',
+ {
+ tagListIndex: index,
+ tagListName: name,
+ },
+ PolicyUtils.hasDependentTags(policy, policyTagList),
+ TransactionUtils.getTagForDisplay(transaction, index),
+ );
+ return (
+
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report.reportID))
+ }
+ brickRoadIndicator={tagError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ errorText={tagError}
+ />
+
+ );
+ });
+
return (
{shouldShowAnimatedBackground && }
@@ -468,35 +500,7 @@ function MoneyRequestView({
/>
)}
- {shouldShowTag &&
- policyTagLists.map(({name, orderWeight}, index) => (
-
-
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report.reportID),
- )
- }
- brickRoadIndicator={
- getErrorForField('tag', {
- tagListIndex: index,
- tagListName: name,
- })
- ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
- : undefined
- }
- errorText={getErrorForField('tag', {tagListIndex: index, tagListName: name})}
- />
-
- ))}
+ {shouldShowTag && tagList}
{isCardTransaction && (
();
+ const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy);
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const [paymentType, setPaymentType] = useState();
+
const managerID = iouReport?.managerID ?? 0;
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport);
@@ -164,6 +173,32 @@ function ReportPreview({
[chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled],
);
+ const confirmPayment = (type: PaymentMethodType | undefined) => {
+ if (!type) {
+ return;
+ }
+ setPaymentType(type);
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY);
+ if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
+ setIsHoldMenuVisible(true);
+ } else if (chatReport && iouReport) {
+ if (ReportUtils.isInvoiceReport(iouReport)) {
+ IOU.payInvoice(type, chatReport, iouReport);
+ } else {
+ IOU.payMoneyRequest(type, chatReport, iouReport);
+ }
+ }
+ };
+
+ const confirmApproval = () => {
+ setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE);
+ if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
+ setIsHoldMenuVisible(true);
+ } else {
+ IOU.approveMoneyRequest(iouReport ?? {}, true);
+ }
+ };
+
const getDisplayAmount = (): string => {
if (totalDisplaySpend) {
return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency);
@@ -282,17 +317,6 @@ function ReportPreview({
};
}, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]);
- const confirmPayment = (paymentMethodType?: PaymentMethodType) => {
- if (!paymentMethodType || !chatReport || !iouReport) {
- return;
- }
- if (ReportUtils.isInvoiceReport(iouReport)) {
- IOU.payInvoice(paymentMethodType, chatReport, iouReport);
- } else {
- IOU.payMoneyRequest(paymentMethodType, chatReport, iouReport);
- }
- };
-
return (
+ {isHoldMenuVisible && iouReport && requestType !== undefined && (
+ setIsHoldMenuVisible(false)}
+ isVisible={isHoldMenuVisible}
+ paymentType={paymentType}
+ chatReport={chatReport}
+ moneyRequestReport={iouReport}
+ />
+ )}
);
}
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index bc5a84c33b90..9adff46395e6 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -146,64 +146,61 @@ function ReportListItem({
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
- {(hovered?: boolean) => (
-
- {!isLargeScreenWidth && (
-
- )}
-
-
-
-
- {reportItem?.reportName}
- {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
-
+
+ {!isLargeScreenWidth && (
+
+ )}
+
+
+
+
+ {reportItem?.reportName}
+ {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
-
-
+
+
+
+
+ {isLargeScreenWidth && (
+ <>
+ {/** We add an empty view with type style to align the total with the table header */}
+
+
+
-
- {isLargeScreenWidth && (
- <>
- {/** We add an empty view with type style to align the total with the table header */}
-
-
-
-
- >
- )}
-
-
- {reportItem.transactions.map((transaction) => (
- {
- openReportInRHP(transaction);
- }}
- showItemHeaderOnNarrowLayout={false}
- containerStyle={styles.mt3}
- isHovered={hovered}
- isChildListItem
- />
- ))}
+ >
+ )}
- )}
+
+ {reportItem.transactions.map((transaction) => (
+ {
+ openReportInRHP(transaction);
+ }}
+ showItemHeaderOnNarrowLayout={false}
+ containerStyle={styles.mt3}
+ isChildListItem
+ />
+ ))}
+
);
}
diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx
index b222631b7273..ecf9264301c2 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -50,16 +50,13 @@ function TransactionListItem({
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
>
- {(hovered?: boolean) => (
- {
- onSelectRow(item);
- }}
- isHovered={hovered}
- />
- )}
+ {
+ onSelectRow(item);
+ }}
+ />
);
}
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 50a34be86f61..d17d923a54e1 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -33,10 +33,6 @@ type TransactionCellProps = {
transactionItem: TransactionListItemType;
} & CellProps;
-type ReceiptCellProps = {
- isHovered?: boolean;
-} & TransactionCellProps;
-
type ActionCellProps = {
onButtonPress: () => void;
} & CellProps;
@@ -51,7 +47,6 @@ type TransactionListItemRowProps = {
onButtonPress: () => void;
showItemHeaderOnNarrowLayout?: boolean;
containerStyle?: StyleProp;
- isHovered?: boolean;
isChildListItem?: boolean;
};
@@ -68,7 +63,7 @@ const getTypeIcon = (type?: SearchTransactionType) => {
}
};
-function ReceiptCell({transactionItem, isHovered = false}: ReceiptCellProps) {
+function ReceiptCell({transactionItem}: TransactionCellProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -85,7 +80,6 @@ function ReceiptCell({transactionItem, isHovered = false}: ReceiptCellProps) {
fallbackIconSize={20}
fallbackIconColor={theme.icon}
iconSize="x-small"
- isHovered={isHovered}
/>
);
@@ -181,14 +175,14 @@ function TagCell({isLargeScreenWidth, showTooltip, transactionItem}: Transaction
return isLargeScreenWidth ? (
) : (
);
}
@@ -209,15 +203,7 @@ function TaxCell({transactionItem, showTooltip}: TransactionCellProps) {
);
}
-function TransactionListItemRow({
- item,
- showTooltip,
- onButtonPress,
- showItemHeaderOnNarrowLayout = true,
- containerStyle,
- isHovered = false,
- isChildListItem = false,
-}: TransactionListItemRowProps) {
+function TransactionListItemRow({item, showTooltip, onButtonPress, showItemHeaderOnNarrowLayout = true, containerStyle, isChildListItem = false}: TransactionListItemRowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isLargeScreenWidth} = useWindowDimensions();
@@ -242,7 +228,6 @@ function TransactionListItemRow({
transactionItem={item}
isLargeScreenWidth={false}
showTooltip={false}
- isHovered={isHovered}
/>
diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx
index b6e2a753c829..85954e68c5a9 100644
--- a/src/components/SettlementButton.tsx
+++ b/src/components/SettlementButton.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useMemo} from 'react';
+import React, {useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
@@ -8,7 +8,6 @@ import * as ReportUtils from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import * as BankAccounts from '@userActions/BankAccounts';
import * as IOU from '@userActions/IOU';
-import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
@@ -144,10 +143,6 @@ function SettlementButton({
const {translate} = useLocalize();
const {isOffline} = useNetwork();
- useEffect(() => {
- PaymentMethods.openWalletPage();
- }, []);
-
const session = useSession();
const chatReport = ReportUtils.getReport(chatReportID);
const isInvoiceReport = (!isEmptyObject(iouReport) && ReportUtils.isInvoiceReport(iouReport)) || false;
diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx
index d4a45f3d93b3..3b89b7c3a7ad 100644
--- a/src/components/ThumbnailImage.tsx
+++ b/src/components/ThumbnailImage.tsx
@@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react';
import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useNetwork from '@hooks/useNetwork';
+import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useThumbnailDimensions from '@hooks/useThumbnailDimensions';
@@ -48,9 +49,6 @@ type ThumbnailImageProps = {
/** The object position of image */
objectPosition?: ImageObjectPosition;
-
- /** Whether the component is hovered */
- isHovered?: boolean;
};
type UpdateImageSizeParams = {
@@ -69,7 +67,6 @@ function ThumbnailImage({
fallbackIconSize = variables.iconSizeSuperLarge,
fallbackIconColor,
objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL,
- isHovered = false,
}: ThumbnailImageProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -78,6 +75,7 @@ function ThumbnailImage({
const cachedDimensions = shouldDynamicallyResize && typeof previewSourceURL === 'string' ? thumbnailDimensionsCache.get(previewSourceURL) : null;
const [imageDimensions, setImageDimensions] = useState({width: cachedDimensions?.width ?? imageWidth, height: cachedDimensions?.height ?? imageHeight});
const {thumbnailDimensionsStyles} = useThumbnailDimensions(imageDimensions.width, imageDimensions.height);
+ const StyleUtils = useStyleUtils();
useEffect(() => {
setFailedToLoad(false);
@@ -110,7 +108,7 @@ function ThumbnailImage({
if (failedToLoad || previewSourceURL === '') {
return (
-
+
(null);
- const {source, name, type, id} = useMemo(() => {
+ const mainAvatar = useMemo(() => {
if (!policy) {
return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
}
@@ -56,7 +56,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) {
>
{({hovered}) => (
{
inputRef.current = ref;
+ if (isInputInitialized) {
+ return;
+ }
setIsInputInitialized(true);
};
diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts
index d1af33aa9da5..77ad5ce816f5 100644
--- a/src/hooks/useMarkdownStyle.ts
+++ b/src/hooks/useMarkdownStyle.ts
@@ -5,12 +5,25 @@ import FontUtils from '@styles/utils/FontUtils';
import variables from '@styles/variables';
import useTheme from './useTheme';
-function useMarkdownStyle(message: string | null = null): MarkdownStyle {
+function useMarkdownStyle(message: string | null = null, excludeStyles: Array = []): MarkdownStyle {
const theme = useTheme();
const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal;
- const markdownStyle = useMemo(
+ // this map is used to reset the styles that are not needed - passing undefined value can break the native side
+ const nonStylingDefaultValues: Record = useMemo(
() => ({
+ color: theme.text,
+ backgroundColor: 'transparent',
+ marginLeft: 0,
+ paddingLeft: 0,
+ borderColor: 'transparent',
+ borderWidth: 0,
+ }),
+ [theme],
+ );
+
+ const markdownStyle = useMemo(() => {
+ const styling = {
syntax: {
color: theme.syntax,
},
@@ -59,9 +72,21 @@ function useMarkdownStyle(message: string | null = null): MarkdownStyle {
color: theme.mentionText,
backgroundColor: theme.mentionBG,
},
- }),
- [theme, emojiFontSize],
- );
+ };
+
+ if (excludeStyles.length) {
+ excludeStyles.forEach((key) => {
+ const style: Record = styling[key];
+ if (style) {
+ Object.keys(style).forEach((styleKey) => {
+ style[styleKey] = nonStylingDefaultValues[styleKey] ?? style[styleKey];
+ });
+ }
+ });
+ }
+
+ return styling;
+ }, [theme, emojiFontSize, excludeStyles, nonStylingDefaultValues]);
return markdownStyle;
}
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index d1cc7ec70181..83c725a48db0 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -62,12 +62,12 @@ function useViolations(violations: TransactionViolation[]) {
}, [violations]);
const getViolationsForField = useCallback(
- (field: ViolationField, data?: TransactionViolation['data']) => {
+ (field: ViolationField, data?: TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
const currentViolations = violationsByField.get(field) ?? [];
// someTagLevelsRequired has special logic becase data.errorIndexes is a bit unique in how it denotes the tag list that has the violation
// tagListIndex can be 0 so we compare with undefined
- if (currentViolations[0]?.name === 'someTagLevelsRequired' && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) {
+ if (currentViolations[0]?.name === CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) {
return currentViolations
.filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex ?? -1))
.map((violation) => ({
@@ -79,8 +79,28 @@ function useViolations(violations: TransactionViolation[]) {
}));
}
+ // missingTag has special logic for policies with dependent tags, because only one violation is returned for all tags
+ // when no tags are present, so the tag name isn't set in the violation data. That's why we add it here
+ if (policyHasDependentTags && currentViolations[0]?.name === CONST.VIOLATIONS.MISSING_TAG && data?.tagListName) {
+ return [
+ {
+ ...currentViolations[0],
+ data: {
+ ...currentViolations[0].data,
+ tagName: data?.tagListName,
+ },
+ },
+ ];
+ }
+
// tagOutOfPolicy has special logic because we have to account for multi-level tags and use tagName to find the right tag to put the violation on
- if (currentViolations[0]?.name === 'tagOutOfPolicy' && data?.tagListName !== undefined && currentViolations[0]?.data?.tagName) {
+ if (currentViolations[0]?.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY && data?.tagListName !== undefined && currentViolations[0]?.data?.tagName) {
+ return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName);
+ }
+
+ // allTagLevelsRequired has special logic because it is returned when one but not all the tags are set,
+ // so we need to return the violation for the tag fields without a tag set
+ if (currentViolations[0]?.name === CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED && tagValue) {
return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName);
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 76e4d5d5a143..40c21a7fbfd0 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -689,7 +689,6 @@ export default {
finished: 'Finished',
sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`,
submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`,
- trackAmount: ({amount}: RequestAmountParams) => `track ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index d03e13d1a9ff..a6d317c623e0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -683,7 +683,6 @@ export default {
finished: 'Finalizado',
sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`,
submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
- trackAmount: ({amount}: RequestAmountParams) => `seguimiento de ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`,
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 50cb9a20dff6..032a8261bec9 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -380,7 +380,8 @@ function getDBTime(timestamp: string | number = ''): string {
*/
function getDBTimeWithSkew(timestamp: string | number = ''): string {
if (networkTimeSkew > 0) {
- return getDBTime(new Date(timestamp).valueOf() + networkTimeSkew);
+ const datetime = timestamp ? new Date(timestamp) : new Date();
+ return getDBTime(datetime.valueOf() + networkTimeSkew);
}
return getDBTime(timestamp);
}
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx
similarity index 91%
rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
rename to src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx
index 25bec235ed45..8be79e538ced 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx
@@ -1,5 +1,5 @@
import {useNavigation, useNavigationState} from '@react-navigation/native';
-import React, {useEffect} from 'react';
+import React, {useCallback, useEffect} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
@@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Session from '@libs/actions/Session';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import Navigation from '@libs/Navigation/Navigation';
@@ -24,6 +25,7 @@ import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
@@ -36,9 +38,8 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {activeWorkspaceID} = useActiveWorkspace();
-
const navigation = useNavigation();
+ const {activeWorkspaceID} = useActiveWorkspace();
useEffect(() => {
const navigationState = navigation.getState() as State | undefined;
@@ -68,13 +69,16 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID);
+ const navigateToChats = useCallback(() => {
+ const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME;
+ Navigation.navigate(route);
+ }, [activeWorkspaceID]);
+
return (
{
- Navigation.navigate(ROUTES.HOME);
- }}
+ onPress={navigateToChats}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('common.inbox')}
wrapperStyle={styles.flex1}
@@ -96,7 +100,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
{
- Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL));
+ interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)));
}}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('common.search')}
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
new file mode 100644
index 000000000000..4fecfdcb0e3e
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx
@@ -0,0 +1,135 @@
+import {useNavigation, useNavigationState} from '@react-navigation/native';
+import React, {useCallback, useEffect} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithFeedback} from '@components/Pressable';
+import Tooltip from '@components/Tooltip';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as Session from '@libs/actions/Session';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
+import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
+import Navigation from '@libs/Navigation/Navigation';
+import type {RootStackParamList, State} from '@libs/Navigation/types';
+import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
+import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
+import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
+import variables from '@styles/variables';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+
+type PurposeForUsingExpensifyModalOnyxProps = {
+ isLoadingApp: OnyxEntry;
+};
+type PurposeForUsingExpensifyModalProps = PurposeForUsingExpensifyModalOnyxProps;
+
+function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const navigation = useNavigation();
+ const {activeWorkspaceID: contextActiveWorkspaceID} = useActiveWorkspace();
+ const activeWorkspaceID = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID) ?? contextActiveWorkspaceID;
+
+ useEffect(() => {
+ const navigationState = navigation.getState() as State | undefined;
+ const routes = navigationState?.routes;
+ const currentRoute = routes?.[navigationState?.index ?? 0];
+ // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method.
+ // To prevent this, the value of the bottomTabRoute?.name is checked here
+ if (!!(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || Session.isAnonymousUser()) {
+ return;
+ }
+
+ Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)});
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isLoadingApp]);
+
+ // Parent navigator of the bottom tab bar is the root navigator.
+ const currentTabName = useNavigationState((state) => {
+ const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state);
+
+ if (topmostCentralPaneRoute && topmostCentralPaneRoute.name === SCREENS.SEARCH.CENTRAL_PANE) {
+ return SCREENS.SEARCH.CENTRAL_PANE;
+ }
+
+ const topmostBottomTabRoute = getTopmostBottomTabRoute(state);
+ return topmostBottomTabRoute?.name ?? SCREENS.HOME;
+ });
+
+ const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID);
+
+ const navigateToChats = useCallback(() => {
+ const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/home` as Route) : ROUTES.HOME;
+ Navigation.navigate(route);
+ }, [activeWorkspaceID]);
+
+ return (
+
+
+
+
+
+ {chatTabBrickRoad && (
+
+ )}
+
+
+
+
+ {
+ interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)));
+ }}
+ role={CONST.ROLE.BUTTON}
+ accessibilityLabel={translate('common.search')}
+ wrapperStyle={styles.flex1}
+ style={styles.bottomTabBarItem}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+BottomTabBar.displayName = 'BottomTabBar';
+
+export default withOnyx({
+ isLoadingApp: {
+ key: ONYXKEYS.IS_LOADING_APP,
+ },
+})(BottomTabBar);
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index 9e1eb348451b..06a3dce8d59a 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -135,11 +135,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N
// We want to clean saved scroll offsets for screens that aren't anymore in the state.
cleanStaleScrollOffsets(state);
-
- // clear all window selection on navigation
- // this is to prevent the selection from persisting when navigating to a new page in web
- // using "?" to avoid crash in native
- window?.getSelection?.()?.removeAllRanges?.();
};
return (
diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts
index bf68a694523a..10df3c703c92 100644
--- a/src/libs/Network/SequentialQueue.ts
+++ b/src/libs/Network/SequentialQueue.ts
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
import * as ActiveClientManager from '@libs/ActiveClientManager';
+import Log from '@libs/Log';
import * as Request from '@libs/Request';
import * as RequestThrottle from '@libs/RequestThrottle';
import * as PersistedRequests from '@userActions/PersistedRequests';
@@ -26,10 +27,11 @@ let isQueuePaused = false;
*/
function pause() {
if (isQueuePaused) {
+ Log.info('[SequentialQueue] Queue already paused');
return;
}
- console.debug('[SequentialQueue] Pausing the queue');
+ Log.info('[SequentialQueue] Pausing the queue');
isQueuePaused = true;
}
@@ -40,6 +42,7 @@ function flushOnyxUpdatesQueue() {
// The only situation where the queue is paused is if we found a gap between the app current data state and our server's. If that happens,
// we'll trigger async calls to make the client updated again. While we do that, we don't want to insert anything in Onyx.
if (isQueuePaused) {
+ Log.info('[SequentialQueue] Queue already paused');
return;
}
QueuedOnyxUpdates.flushQueue();
@@ -56,11 +59,18 @@ function flushOnyxUpdatesQueue() {
function process(): Promise {
// When the queue is paused, return early. This prevents any new requests from happening. The queue will be flushed again when the queue is unpaused.
if (isQueuePaused) {
+ Log.info('[SequentialQueue] Unable to process. Queue is paused.');
+ return Promise.resolve();
+ }
+
+ if (NetworkStore.isOffline()) {
+ Log.info('[SequentialQueue] Unable to process. We are offline.');
return Promise.resolve();
}
const persistedRequests = PersistedRequests.getAll();
- if (persistedRequests.length === 0 || NetworkStore.isOffline()) {
+ if (persistedRequests.length === 0) {
+ Log.info('[SequentialQueue] Unable to process. No requests to process.');
return Promise.resolve();
}
@@ -72,6 +82,7 @@ function process(): Promise {
// A response might indicate that the queue should be paused. This happens when a gap in onyx updates is detected between the client and the server and
// that gap needs resolved before the queue can continue.
if (response?.shouldPauseQueue) {
+ Log.info("[SequentialQueue] Handled 'shouldPauseQueue' in response. Pausing the queue.");
pause();
}
PersistedRequests.remove(requestToProcess);
@@ -102,16 +113,24 @@ function process(): Promise {
function flush() {
// 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) {
+ Log.info('[SequentialQueue] Unable to flush. Queue is paused.');
+ return;
+ }
+
+ if (isSequentialQueueRunning) {
+ Log.info('[SequentialQueue] Unable to flush. Queue is already running.');
return;
}
- if (isSequentialQueueRunning || PersistedRequests.getAll().length === 0) {
+ if (PersistedRequests.getAll().length === 0) {
+ Log.info('[SequentialQueue] Unable to flush. No requests to process.');
return;
}
// ONYXKEYS.PERSISTED_REQUESTS is shared across clients, thus every client/tab will have a copy
// It is very important to only process the queue from leader client otherwise requests will be duplicated.
if (!ActiveClientManager.isClientTheLeader()) {
+ Log.info('[SequentialQueue] Unable to flush. Client is not the leader.');
return;
}
@@ -128,6 +147,7 @@ function flush() {
callback: () => {
Onyx.disconnect(connectionID);
process().finally(() => {
+ Log.info('[SequentialQueue] Finished processing queue.');
isSequentialQueueRunning = false;
if (NetworkStore.isOffline() || PersistedRequests.getAll().length === 0) {
resolveIsReadyPromise?.();
@@ -144,6 +164,7 @@ function flush() {
*/
function unpause() {
if (!isQueuePaused) {
+ Log.info('[SequentialQueue] Unable to unpause queue. We are already processing.');
return;
}
diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts
index 2d020ec778ae..de064441047a 100644
--- a/src/libs/NetworkConnection.ts
+++ b/src/libs/NetworkConnection.ts
@@ -37,13 +37,13 @@ const triggerReconnectionCallbacks = throttle(
* Called when the offline status of the app changes and if the network is "reconnecting" (going from offline to online)
* then all of the reconnection callbacks are triggered
*/
-function setOfflineStatus(isCurrentlyOffline: boolean): void {
- NetworkActions.setIsOffline(isCurrentlyOffline);
+function setOfflineStatus(isCurrentlyOffline: boolean, reason = ''): void {
+ NetworkActions.setIsOffline(isCurrentlyOffline, reason);
// When reconnecting, ie, going from offline to online, all the reconnection callbacks
// are triggered (this is usually Actions that need to re-download data from the server)
if (isOffline && !isCurrentlyOffline) {
- NetworkActions.setIsBackendReachable(true);
+ NetworkActions.setIsBackendReachable(true, 'moved from offline to online');
triggerReconnectionCallbacks('offline status changed');
}
@@ -74,13 +74,13 @@ Onyx.connect({
}
shouldForceOffline = currentShouldForceOffline;
if (shouldForceOffline) {
- setOfflineStatus(true);
+ setOfflineStatus(true, 'shouldForceOffline was detected in the Onyx data');
Log.info(`[NetworkStatus] Setting "offlineStatus" to "true" because user is under force offline`);
} else {
// If we are no longer forcing offline fetch the NetInfo to set isOffline appropriately
NetInfo.fetch().then((state) => {
const isInternetReachable = !!state.isInternetReachable;
- setOfflineStatus(isInternetReachable);
+ setOfflineStatus(isInternetReachable, 'NetInfo checked if the internet is reachable');
Log.info(
`[NetworkStatus] The force-offline mode was turned off. Getting the device network status from NetInfo. Network state: ${JSON.stringify(
state,
@@ -122,20 +122,20 @@ function subscribeToBackendAndInternetReachability(): () => void {
})
.then((isBackendReachable: boolean) => {
if (isBackendReachable) {
- NetworkActions.setIsBackendReachable(true);
+ NetworkActions.setIsBackendReachable(true, 'successfully completed API request');
return;
}
+ NetworkActions.setIsBackendReachable(false, 'request succeeded, but internet reachability test failed');
checkInternetReachability().then((isInternetReachable: boolean) => {
- setOfflineStatus(!isInternetReachable);
+ setOfflineStatus(!isInternetReachable, 'checkInternetReachability was called after api/ping returned a non-200 jsonCode');
setNetWorkStatus(isInternetReachable);
- NetworkActions.setIsBackendReachable(false);
});
})
.catch(() => {
+ NetworkActions.setIsBackendReachable(false, 'request failed and internet reachability test failed');
checkInternetReachability().then((isInternetReachable: boolean) => {
- setOfflineStatus(!isInternetReachable);
+ setOfflineStatus(!isInternetReachable, 'checkInternetReachability was called after api/ping request failed');
setNetWorkStatus(isInternetReachable);
- NetworkActions.setIsBackendReachable(false);
});
});
}, CONST.NETWORK.BACKEND_CHECK_INTERVAL_MS);
@@ -163,7 +163,7 @@ function subscribeToNetworkStatus(): () => void {
Log.info('[NetworkConnection] Not setting offline status because shouldForceOffline = true');
return;
}
- setOfflineStatus(state.isInternetReachable === false);
+ setOfflineStatus(state.isInternetReachable === false, 'NetInfo received a state change event');
Log.info(`[NetworkStatus] NetInfo.addEventListener event coming, setting "offlineStatus" to ${!!state.isInternetReachable} with network state: ${JSON.stringify(state)}`);
setNetWorkStatus(state.isInternetReachable);
});
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 7678de592a6f..b39c8465bd78 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -424,6 +424,13 @@ function canSendInvoice(policies: OnyxCollection): boolean {
return getActiveAdminWorkspaces(policies).length > 0;
}
+function hasDependentTags(policy: OnyxEntry, policyTagList: OnyxEntry) {
+ if (!policy?.hasMultipleTagLists) {
+ return false;
+ }
+ return Object.values(policyTagList ?? {}).some((tagList) => Object.values(tagList.tags).some((tag) => !!tag.rules?.parentTagsFilter || !!tag.parentTagsFilter));
+}
+
/** Get the Xero organizations connected to the policy */
function getXeroTenants(policy: Policy | undefined): Tenant[] {
// Due to the way optional chain is being handled in this useMemo we are forced to use this approach to properly handle undefined values
@@ -516,6 +523,7 @@ export {
shouldShowPolicy,
getActiveAdminWorkspaces,
canSendInvoice,
+ hasDependentTags,
getXeroTenants,
findCurrentXeroOrganization,
getCurrentXeroOrganizationName,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 269f4c7bc01b..4de976aaf160 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -6111,7 +6111,7 @@ function shouldDisableRename(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry): boolean {
- return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report);
+ return PolicyUtils.isPolicyAdmin(policy) && !isAdminRoom(report) && !isArchivedRoom(report) && !isThread(report) && !isInvoiceRoom(report);
}
/**
@@ -6164,18 +6164,27 @@ function getTaskAssigneeChatOnyxData(
},
);
- successData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${assigneeChatReportID}`,
- value: {
- pendingFields: {
- createChat: null,
+ // BE will send different report's participants and assigneeAccountID. We clear the optimistic ones to avoid duplicated entries
+ successData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${assigneeChatReportID}`,
+ value: {
+ pendingFields: {
+ createChat: null,
+ },
+ isOptimisticReport: false,
+ participants: {[assigneeAccountID]: null},
},
- isOptimisticReport: false,
- // BE will send a different participant. We clear the optimistic one to avoid duplicated entries
- participants: {[assigneeAccountID]: null},
},
- });
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [assigneeAccountID]: null,
+ },
+ },
+ );
failureData.push(
{
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index 2ceecb42dba5..686db5e6a6c5 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -7,7 +7,7 @@ import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx';
+import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx';
/**
* Calculates tag out of policy and missing tag violations for the given transaction
@@ -49,17 +49,41 @@ function getTagViolationsForSingleLevelTags(
}
/**
- * Calculates some tag levels required and missing tag violations for the given transaction
+ * Calculates missing tag violations for policies with dependent tags
*/
-function getTagViolationsForMultiLevelTags(
- updatedTransaction: Transaction,
- transactionViolations: TransactionViolation[],
- policyRequiresTags: boolean,
- policyTagList: PolicyTagList,
-): TransactionViolation[] {
+function getTagViolationsForDependentTags(policyTagList: PolicyTagList, transactionViolations: TransactionViolation[], tagName: string) {
+ const tagViolations = [...transactionViolations];
+
+ if (!tagName) {
+ Object.values(policyTagList).forEach((tagList) =>
+ tagViolations.push({
+ name: CONST.VIOLATIONS.MISSING_TAG,
+ type: CONST.VIOLATION_TYPES.VIOLATION,
+ data: {tagName: tagList.name},
+ }),
+ );
+ } else {
+ const tags = TransactionUtils.getTagArrayFromName(tagName);
+ if (Object.keys(policyTagList).length !== tags.length || tags.includes('')) {
+ tagViolations.push({
+ name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED,
+ type: CONST.VIOLATION_TYPES.VIOLATION,
+ data: {},
+ });
+ }
+ }
+
+ return tagViolations;
+}
+
+/**
+ * Calculates missing tag violations for policies with independent tags
+ */
+function getTagViolationForIndependentTags(policyTagList: PolicyTagList, transactionViolations: TransactionViolation[], transaction: Transaction) {
const policyTagKeys = getSortedTagKeys(policyTagList);
- const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? [];
+ const selectedTags = transaction.tag?.split(CONST.COLON) ?? [];
let newTransactionViolations = [...transactionViolations];
+
newTransactionViolations = newTransactionViolations.filter(
(violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
);
@@ -109,6 +133,30 @@ function getTagViolationsForMultiLevelTags(
return newTransactionViolations;
}
+/**
+ * Calculates tag violations for a transaction on a policy with multi level tags
+ */
+function getTagViolationsForMultiLevelTags(
+ updatedTransaction: Transaction,
+ transactionViolations: TransactionViolation[],
+ policyTagList: PolicyTagList,
+ hasDependentTags: boolean,
+): TransactionViolation[] {
+ const tagViolations = [
+ CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED,
+ CONST.VIOLATIONS.TAG_OUT_OF_POLICY,
+ CONST.VIOLATIONS.MISSING_TAG,
+ CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED,
+ ] as ViolationName[];
+ const filteredTransactionViolations = transactionViolations.filter((violation) => !tagViolations.includes(violation.name));
+
+ if (hasDependentTags) {
+ return getTagViolationsForDependentTags(policyTagList, filteredTransactionViolations, updatedTransaction.tag ?? '');
+ }
+
+ return getTagViolationForIndependentTags(policyTagList, filteredTransactionViolations, updatedTransaction);
+}
+
const ViolationsUtils = {
/**
* Checks a transaction for policy violations and returns an object with Onyx method, key and updated transaction
@@ -121,6 +169,7 @@ const ViolationsUtils = {
policyTagList: PolicyTagList,
policyRequiresCategories: boolean,
policyCategories: PolicyCategories,
+ hasDependentTags: boolean,
): OnyxUpdate {
const isPartialTransaction = TransactionUtils.isPartialMerchant(TransactionUtils.getMerchant(updatedTransaction)) && TransactionUtils.isAmountMissing(updatedTransaction);
if (isPartialTransaction) {
@@ -166,7 +215,7 @@ const ViolationsUtils = {
newTransactionViolations =
Object.keys(policyTagList).length === 1
? getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList)
- : getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList);
+ : getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyTagList, hasDependentTags);
}
return {
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 1bd4de43acfb..e21d86644c3e 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -506,6 +506,7 @@ function buildOnyxDataForMoneyRequest(
...iouReport,
lastMessageText: iouAction.message?.[0]?.text,
lastMessageHtml: iouAction.message?.[0]?.html,
+ lastVisibleActionCreated: iouAction.created,
pendingFields: {
...(shouldCreateNewMoneyRequestReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
},
@@ -822,7 +823,15 @@ function buildOnyxDataForMoneyRequest(
return [optimisticData, successData, failureData];
}
- const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {});
+ const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(
+ transaction,
+ [],
+ !!policy.requiresTag,
+ policyTagList ?? {},
+ !!policy.requiresCategory,
+ policyCategories ?? {},
+ PolicyUtils.hasDependentTags(policy, policyTagList ?? {}),
+ );
if (violationsOnyxData) {
optimisticData.push(violationsOnyxData);
@@ -1136,7 +1145,15 @@ function buildOnyxDataForInvoice(
return [optimisticData, successData, failureData];
}
- const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {});
+ const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(
+ transaction,
+ [],
+ !!policy.requiresTag,
+ policyTagList ?? {},
+ !!policy.requiresCategory,
+ policyCategories ?? {},
+ PolicyUtils.hasDependentTags(policy, policyTagList ?? {}),
+ );
if (violationsOnyxData) {
optimisticData.push(violationsOnyxData);
@@ -1505,7 +1522,15 @@ function buildOnyxDataForTrackExpense(
return [optimisticData, successData, failureData];
}
- const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {});
+ const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(
+ transaction,
+ [],
+ !!policy.requiresTag,
+ policyTagList ?? {},
+ !!policy.requiresCategory,
+ policyCategories ?? {},
+ PolicyUtils.hasDependentTags(policy, policyTagList ?? {}),
+ );
if (violationsOnyxData) {
optimisticData.push(violationsOnyxData);
@@ -1983,6 +2008,7 @@ function getMoneyRequestInformation(
reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction as ReportPreviewAction, false, comment, optimisticTransaction);
} else {
reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction);
+ chatReport.lastVisibleActionCreated = reportPreviewAction.created;
// Generated ReportPreview action is a parent report action of the iou report.
// We are setting the iou report's parentReportActionID to display subtitle correctly in IOU page when offline.
@@ -2675,6 +2701,7 @@ function getUpdateMoneyRequestParams(
policyTagList ?? {},
!!policy.requiresCategory,
policyCategories ?? {},
+ PolicyUtils.hasDependentTags(policy, policyTagList ?? {}),
),
);
failureData.push({
@@ -3835,6 +3862,7 @@ function createSplitsAndOnyxData(
splitChatReport.lastMessageText = splitIOUReportAction.message?.[0]?.text;
splitChatReport.lastMessageHtml = splitIOUReportAction.message?.[0]?.html;
splitChatReport.lastActorAccountID = currentUserAccountID;
+ splitChatReport.lastVisibleActionCreated = splitIOUReportAction.created;
let splitChatReportNotificationPreference = splitChatReport.notificationPreference;
if (splitChatReportNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
@@ -4639,7 +4667,7 @@ function startSplitBill({
API.write(WRITE_COMMANDS.START_SPLIT_BILL, parameters, {optimisticData, successData, failureData});
Navigation.dismissModalWithReport(splitChatReport);
- Report.notifyNewAction(splitChatReport.chatReportID ?? '', currentUserAccountID);
+ Report.notifyNewAction(splitChatReport.reportID ?? '', currentUserAccountID);
}
/** Used for editing a split expense while it's still scanning or when SmartScan fails, it completes a split expense started by startSplitBill above.
@@ -5130,6 +5158,7 @@ function editRegularMoneyRequest(
policyTags,
!!policy.requiresCategory,
policyCategories,
+ PolicyUtils.hasDependentTags(policy, policyTags),
);
optimisticData.push(updatedViolationsOnyxData);
failureData.push({
diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts
index 9c88403b0e98..883e336d6c90 100644
--- a/src/libs/actions/Network.ts
+++ b/src/libs/actions/Network.ts
@@ -1,12 +1,24 @@
import Onyx from 'react-native-onyx';
+import Log from '@libs/Log';
import type {NetworkStatus} from '@libs/NetworkConnection';
import ONYXKEYS from '@src/ONYXKEYS';
-function setIsBackendReachable(isBackendReachable: boolean) {
+function setIsBackendReachable(isBackendReachable: boolean, reason: string) {
+ if (isBackendReachable) {
+ Log.info(`[Network] Backend is reachable because: ${reason}`);
+ } else {
+ Log.info(`[Network] Backend is not reachable because: ${reason}`);
+ }
Onyx.merge(ONYXKEYS.NETWORK, {isBackendReachable});
}
-function setIsOffline(isOffline: boolean) {
+function setIsOffline(isOffline: boolean, reason = '') {
+ if (reason) {
+ let textToLog = '[Network] Client is';
+ textToLog += isOffline ? ' entering offline mode' : ' back online';
+ textToLog += ` because: ${reason}`;
+ Log.info(textToLog);
+ }
Onyx.merge(ONYXKEYS.NETWORK, {isOffline});
}
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 7326b70edb61..ab8ca2a1a4b6 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -469,7 +469,7 @@ function addActions(reportID: string, text = '', file?: FileObject) {
const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.text ?? '');
const optimisticReport: Partial = {
- lastVisibleActionCreated: currentTime,
+ lastVisibleActionCreated: lastAction?.created,
lastMessageTranslationKey: lastComment?.translationKey ?? '',
lastMessageText: lastCommentText,
lastMessageHtml: lastCommentText,
@@ -1019,7 +1019,6 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[])
*/
function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0') {
if (childReportID !== '0') {
- openReport(childReportID);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
} else {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])];
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 55d898d3d4f3..57b551510d58 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -131,10 +131,10 @@ function createTaskAndNavigate(
const optimisticAddCommentReport = ReportUtils.buildOptimisticTaskCommentReportAction(taskReportID, title, assigneeAccountID, `task for ${title}`, parentReportID);
optimisticTaskReport.parentReportActionID = optimisticAddCommentReport.reportAction.reportActionID;
- const currentTime = DateUtils.getDBTime();
+ const currentTime = DateUtils.getDBTimeWithSkew();
const lastCommentText = ReportUtils.formatReportLastMessageText(optimisticAddCommentReport?.reportAction?.message?.[0]?.text ?? '');
const optimisticParentReport = {
- lastVisibleActionCreated: currentTime,
+ lastVisibleActionCreated: optimisticAddCommentReport.reportAction.created,
lastMessageText: lastCommentText,
lastActorAccountID: currentUserAccountID,
lastReadTime: currentTime,
diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx
new file mode 100644
index 000000000000..289c56ad69be
--- /dev/null
+++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.android.tsx
@@ -0,0 +1,5 @@
+import type ShouldIgnoreSelectionWhenUpdatedManually from './types';
+
+const shouldIgnoreSelectionWhenUpdatedManually: ShouldIgnoreSelectionWhenUpdatedManually = true;
+
+export default shouldIgnoreSelectionWhenUpdatedManually;
diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx
new file mode 100644
index 000000000000..744a94aa1f32
--- /dev/null
+++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/index.tsx
@@ -0,0 +1,5 @@
+import type ShouldIgnoreSelectionWhenUpdatedManually from './types';
+
+const shouldIgnoreSelectionWhenUpdatedManually: ShouldIgnoreSelectionWhenUpdatedManually = false;
+
+export default shouldIgnoreSelectionWhenUpdatedManually;
diff --git a/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts
new file mode 100644
index 000000000000..56394183ef7d
--- /dev/null
+++ b/src/libs/shouldIgnoreSelectionWhenUpdatedManually/types.ts
@@ -0,0 +1,3 @@
+type ShouldIgnoreSelectionWhenUpdatedManually = boolean;
+
+export default ShouldIgnoreSelectionWhenUpdatedManually;
diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx
index 46a10483fd08..e95fa84c09c9 100644
--- a/src/pages/RoomDescriptionPage.tsx
+++ b/src/pages/RoomDescriptionPage.tsx
@@ -97,6 +97,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
value={description}
onChangeText={handleReportDescriptionChange}
autoCapitalize="none"
+ isMarkdownEnabled
/>
diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx
index c201047033e6..663a01ba02b8 100644
--- a/src/pages/Search/SearchPageBottomTab.tsx
+++ b/src/pages/Search/SearchPageBottomTab.tsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ScreenWrapper from '@components/ScreenWrapper';
import Search from '@components/Search';
@@ -7,20 +8,42 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
+import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import type {SearchQuery} from '@src/types/onyx/SearchResults';
import SearchFilters from './SearchFilters';
+type SearchPageProps = StackScreenProps;
+
+const defaultSearchProps = {
+ query: '' as SearchQuery,
+ policyIDs: undefined,
+ sortBy: CONST.SEARCH_TABLE_COLUMNS.DATE,
+ sortOrder: CONST.SORT_ORDER.DESC,
+};
function SearchPageBottomTab() {
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
const activeRoute = useActiveRoute();
const styles = useThemeStyles();
- const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : '';
- const policyIDs = activeRoute?.params && 'policyIDs' in activeRoute.params ? (activeRoute?.params?.policyIDs as string) : undefined;
- const query = currentQuery as SearchQuery;
+
+ const {
+ query: rawQuery,
+ policyIDs,
+ sortBy,
+ sortOrder,
+ } = useMemo(() => {
+ if (activeRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE || !activeRoute.params) {
+ return defaultSearchProps;
+ }
+ return {...defaultSearchProps, ...activeRoute.params} as SearchPageProps['route']['params'];
+ }, [activeRoute]);
+
+ const query = rawQuery as SearchQuery;
+
const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query);
const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL));
@@ -45,6 +68,8 @@ function SearchPageBottomTab() {
)}
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index 0515ca011517..d7f4ee7f6c5c 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -775,6 +775,7 @@ function ComposerWithSuggestions(
onLayout={onLayout}
onScroll={hideSuggestionMenu}
shouldContainScroll={Browser.isMobileSafari()}
+ isGroupPolicyReport={isGroupPolicyReport}
/>
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index c700fea4fb85..940feb99bc27 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -210,12 +210,9 @@ function ReportActionsList({
[sortedReportActions, isOffline],
);
- // whisper action doesn't affect lastVisibleActionCreated, so we should not take it into account while checking if there is the newest report action
- const newestVisibleReportAction = useMemo(() => sortedVisibleReportActions.find((item) => !ReportActionsUtils.isWhisperAction(item)) ?? null, [sortedVisibleReportActions]);
-
const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID;
const reportActionSize = useRef(sortedVisibleReportActions.length);
- const hasNewestReportAction = newestVisibleReportAction?.created === report.lastVisibleActionCreated;
+ const hasNewestReportAction = sortedVisibleReportActions[0]?.created === report.lastVisibleActionCreated;
const hasNewestReportActionRef = useRef(hasNewestReportAction);
hasNewestReportActionRef.current = hasNewestReportAction;
const previousLastIndex = useRef(lastActionIndex);
diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx
index 609e4044002e..5d4af7ea4961 100644
--- a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx
+++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx
@@ -19,12 +19,14 @@ function AvatarWithOptionalStatus({emojiStatus = '', isSelected = false}: Avatar
-
- {emojiStatus}
-
+
+
+ {emojiStatus}
+
+
);
diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx
index 5bbc9d22a97f..ce1cddc91958 100644
--- a/src/pages/iou/MoneyRequestAmountForm.tsx
+++ b/src/pages/iou/MoneyRequestAmountForm.tsx
@@ -228,19 +228,7 @@ function MoneyRequestAmountForm(
);
const buttonText: string = useMemo(() => {
- const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? '';
if (skipConfirmation) {
- if (currentAmount !== '') {
- const currencyAmount = CurrencyUtils.convertToDisplayString(CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)), currency) ?? '';
- let text = translate('iou.submitAmount', {amount: currencyAmount});
- if (iouType === CONST.IOU.TYPE.SPLIT) {
- text = translate('iou.splitAmount', {amount: currencyAmount});
- } else if (iouType === CONST.IOU.TYPE.TRACK) {
- text = translate('iou.trackAmount', {amount: currencyAmount});
- }
- return text[0].toUpperCase() + text.slice(1);
- }
-
if (iouType === CONST.IOU.TYPE.SPLIT) {
return translate('iou.splitExpense');
}
@@ -250,7 +238,7 @@ function MoneyRequestAmountForm(
return translate('iou.submitExpense');
}
return isEditing ? translate('common.save') : translate('common.next');
- }, [skipConfirmation, iouType, currency, isEditing, translate]);
+ }, [skipConfirmation, iouType, isEditing, translate]);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx
index 393afcd25eb8..5b265ff400e8 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.tsx
+++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx
@@ -295,6 +295,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli
maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH}
autoCapitalize="none"
shouldInterceptSwipe
+ isMarkdownEnabled
/>
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 58288f213818..738a0663b94b 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -106,7 +106,7 @@ function WorkspacePageWithSections({
reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA,
route,
shouldUseScrollView = false,
- shouldSkipVBBACall = false,
+ shouldSkipVBBACall = true,
shouldShowBackButton = false,
user,
shouldShowLoading = true,
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 36d5bfbbb75b..aabe99019c4f 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -125,6 +125,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
shouldShowOfflineIndicatorInWideScreen
shouldShowNonAdmin
icon={Illustrations.House}
+ shouldSkipVBBACall={false}
>
{(hasVBA?: boolean) => (
diff --git a/src/pages/workspace/bills/WorkspaceBillsPage.tsx b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
index 9e1810d74793..cdd733d59141 100644
--- a/src/pages/workspace/bills/WorkspaceBillsPage.tsx
+++ b/src/pages/workspace/bills/WorkspaceBillsPage.tsx
@@ -23,6 +23,7 @@ function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) {
shouldUseScrollView
headerText={translate('workspace.common.bills')}
route={route}
+ shouldSkipVBBACall={false}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BILLS}
shouldShowOfflineIndicatorInWideScreen
>
diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx
index eeec0edf106b..c79a335376d6 100644
--- a/src/pages/workspace/card/WorkspaceCardPage.tsx
+++ b/src/pages/workspace/card/WorkspaceCardPage.tsx
@@ -24,6 +24,7 @@ function WorkspaceCardPage({route}: WorkspaceCardPageProps) {
shouldUseScrollView
headerText={translate('workspace.common.card')}
route={route}
+ shouldSkipVBBACall={false}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_CARD}
shouldShowOfflineIndicatorInWideScreen
>
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index ef0a73788dfa..f1a973d5c47d 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -23,6 +23,7 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
headerText={translate('workspace.common.invoices')}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_INVOICES}
shouldShowOfflineIndicatorInWideScreen
+ shouldSkipVBBACall={false}
route={route}
>
{(hasVBA?: boolean, policyID?: string) => (
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
index 0a384728f00d..08da904dec20 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
@@ -101,7 +101,6 @@ function WorkspaceRateAndUnitPage(props: WorkspaceRateAndUnitPageProps) {
headerText={translate('workspace.reimburse.trackDistance')}
route={props.route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE}
- shouldSkipVBBACall
backButtonRoute=""
shouldShowLoading={false}
shouldShowBackButton
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
index 1acd3f501314..ec37d1d110b1 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
@@ -64,7 +64,6 @@ function WorkspaceRatePage(props: WorkspaceRatePageProps) {
headerText={translate('workspace.reimburse.trackDistanceRate')}
route={props.route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE}
- shouldSkipVBBACall
backButtonRoute={ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')}
shouldShowLoading={false}
shouldShowBackButton
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
index 257c29572d71..e8430cab60bb 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
@@ -52,7 +52,6 @@ function WorkspaceUnitPage(props: WorkspaceUnitPageProps) {
headerText={translate('workspace.reimburse.trackDistanceUnit')}
route={props.route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE}
- shouldSkipVBBACall
backButtonRoute={ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? '')}
shouldShowLoading={false}
shouldShowBackButton
diff --git a/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx
index 6bb15a8a17ce..d929ec10748a 100644
--- a/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceReimbursePage.tsx
@@ -20,7 +20,6 @@ function WorkspaceReimbursePage({route, policy}: WorkspaceReimbursePageProps) {
headerText={translate('workspace.common.reimburse')}
route={route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE}
- shouldSkipVBBACall
shouldShowLoading={false}
shouldShowOfflineIndicatorInWideScreen
>
diff --git a/src/pages/workspace/travel/WorkspaceTravelPage.tsx b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
index 1acae9e6d359..eb61397d10b2 100644
--- a/src/pages/workspace/travel/WorkspaceTravelPage.tsx
+++ b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
@@ -25,6 +25,7 @@ function WorkspaceTravelPage({route}: WorkspaceTravelPageProps) {
route={route}
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_TRAVEL}
shouldShowOfflineIndicatorInWideScreen
+ shouldSkipVBBACall={false}
>
{(hasVBA, policyID) => (
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 25df6538bb01..ae55b00653ae 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -289,7 +289,6 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_WORKFLOWS}
shouldShowOfflineIndicatorInWideScreen
shouldShowNotFoundPage={!isPaidGroupPolicy || !isPolicyAdmin}
- shouldSkipVBBACall
isLoading={isLoading}
shouldShowLoading={isLoading}
shouldUseScrollView
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 7b49b90505ba..d0cc09a9ccf0 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4396,6 +4396,15 @@ const styles = (theme: ThemeColors) =>
emojiStatusLHN: {
fontSize: 9,
+ ...(Browser.getBrowser() && !Browser.isMobile() && {transform: 'scale(.5)', fontSize: 22, overflow: 'visible'}),
+ ...(Browser.getBrowser() &&
+ Browser.isSafari() &&
+ !Browser.isMobile() && {
+ transform: 'scale(0.7)',
+ fontSize: 13,
+ lineHeight: 15,
+ overflow: 'visible',
+ }),
},
onboardingVideoPlayer: {
@@ -4423,6 +4432,7 @@ const styles = (theme: ThemeColors) =>
bottom: -4,
borderColor: theme.highlightBG,
borderWidth: 2,
+ overflow: 'hidden',
},
moneyRequestViewImage: {
...spacing.mh5,
diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts
index 6b2a2bf5ffd6..467ba3271981 100644
--- a/src/types/onyx/PolicyTag.ts
+++ b/src/types/onyx/PolicyTag.ts
@@ -17,6 +17,21 @@ type PolicyTag = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** A list of errors keyed by microtime */
errors?: OnyxCommon.Errors | null;
+
+ /** The rules applied to the policy tag */
+ rules?: {
+ /**
+ * String representation of regex to match against parent tag. Eg, if San Francisco is a child tag of California
+ * its parentTagsFilter will be ^California$
+ */
+ parentTagsFilter?: string;
+ };
+
+ /**
+ * String representation of regex to match against parent tag. Eg, if San Francisco is a child tag of California
+ * its parentTagsFilter will be ^California$
+ */
+ parentTagsFilter?: string;
}>;
/** Record of policy tags, indexed by their name */
diff --git a/src/types/onyx/UserMetadata.ts b/src/types/onyx/UserMetadata.ts
index 14181b33c4b4..a8b34cb29401 100644
--- a/src/types/onyx/UserMetadata.ts
+++ b/src/types/onyx/UserMetadata.ts
@@ -11,6 +11,8 @@ type UserMetadata = {
/** User's account ID */
accountID?: number;
+
+ /** Type of environment the user is using (staging or production) */
environment?: string;
};
diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts
index b967617918c1..5e8f93a56dbc 100644
--- a/tests/unit/ViolationUtilsTest.ts
+++ b/tests/unit/ViolationUtilsTest.ts
@@ -43,7 +43,7 @@ describe('getViolationsOnyxData', () => {
});
it('should return an object with correct shape and with empty transactionViolations array', () => {
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result).toEqual({
onyxMethod: Onyx.METHOD.SET,
@@ -57,7 +57,7 @@ describe('getViolationsOnyxData', () => {
{name: 'duplicatedTransaction', type: CONST.VIOLATION_TYPES.VIOLATION},
{name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining(transactionViolations));
});
@@ -70,24 +70,32 @@ describe('getViolationsOnyxData', () => {
it('should add missingCategory violation if no category is included', () => {
transaction.category = undefined;
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([missingCategoryViolation, ...transactionViolations]));
});
it('should add categoryOutOfPolicy violation when category is not in policy', () => {
transaction.category = 'Bananas';
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([categoryOutOfPolicyViolation, ...transactionViolations]));
});
it('should not include a categoryOutOfPolicy violation when category is in policy', () => {
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).not.toContainEqual(categoryOutOfPolicyViolation);
});
it('should not add a category violation when the transaction is partial', () => {
const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: undefined};
- const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(
+ partialTransaction,
+ transactionViolations,
+ policyRequiresTags,
+ policyTags,
+ policyRequiresCategories,
+ policyCategories,
+ false,
+ );
expect(result.value).not.toContainEqual(missingCategoryViolation);
});
@@ -98,7 +106,7 @@ describe('getViolationsOnyxData', () => {
{name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([categoryOutOfPolicyViolation, ...transactionViolations]));
});
@@ -110,7 +118,7 @@ describe('getViolationsOnyxData', () => {
{name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([missingCategoryViolation, ...transactionViolations]));
});
@@ -122,7 +130,7 @@ describe('getViolationsOnyxData', () => {
});
it('should not add any violations when categories are not required', () => {
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).not.toContainEqual([categoryOutOfPolicyViolation]);
expect(result.value).not.toContainEqual([missingCategoryViolation]);
@@ -147,7 +155,7 @@ describe('getViolationsOnyxData', () => {
});
it("shouldn't update the transactionViolations if the policy requires tags and the transaction has a tag from the policy", () => {
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(transactionViolations);
});
@@ -155,7 +163,7 @@ describe('getViolationsOnyxData', () => {
it('should add a missingTag violation if none is provided and policy requires tags', () => {
transaction.tag = undefined;
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([{...missingTagViolation}]));
});
@@ -163,14 +171,22 @@ describe('getViolationsOnyxData', () => {
it('should add a tagOutOfPolicy violation when policy requires tags and tag is not in the policy', () => {
policyTags = {};
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual([]);
});
it('should not add a tag violation when the transaction is partial', () => {
const partialTransaction = {...transaction, amount: 0, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, tag: undefined};
- const result = ViolationsUtils.getViolationsOnyxData(partialTransaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(
+ partialTransaction,
+ transactionViolations,
+ policyRequiresTags,
+ policyTags,
+ policyRequiresCategories,
+ policyCategories,
+ false,
+ );
expect(result.value).not.toContainEqual(missingTagViolation);
});
@@ -181,7 +197,7 @@ describe('getViolationsOnyxData', () => {
{name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([{...tagOutOfPolicyViolation}, ...transactionViolations]));
});
@@ -193,7 +209,7 @@ describe('getViolationsOnyxData', () => {
{name: 'receiptRequired', type: CONST.VIOLATION_TYPES.VIOLATION},
];
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual(expect.arrayContaining([{...missingTagViolation}, ...transactionViolations]));
});
@@ -205,7 +221,7 @@ describe('getViolationsOnyxData', () => {
});
it('should not add any violations when tags are not required', () => {
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).not.toContainEqual([tagOutOfPolicyViolation]);
expect(result.value).not.toContainEqual([missingTagViolation]);
@@ -260,32 +276,40 @@ describe('getViolationsOnyxData', () => {
};
// Test case where transaction has no tags
- let result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ let result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual([someTagLevelsRequiredViolation]);
// Test case where transaction has 1 tag
transaction.tag = 'Africa';
someTagLevelsRequiredViolation.data = {errorIndexes: [1, 2]};
- result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual([someTagLevelsRequiredViolation]);
// Test case where transaction has 2 tags
transaction.tag = 'Africa::Project1';
someTagLevelsRequiredViolation.data = {errorIndexes: [1]};
- result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual([someTagLevelsRequiredViolation]);
// Test case where transaction has all tags
transaction.tag = 'Africa:Accounting:Project1';
- result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
expect(result.value).toEqual([]);
});
it('should return tagOutOfPolicy when a tag is not enabled in the policy but is set in the transaction', () => {
policyTags.Department.tags.Accounting.enabled = false;
transaction.tag = 'Africa:Accounting:Project1';
- const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories);
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, false);
const violation = {...tagOutOfPolicyViolation, data: {tagName: 'Department'}};
expect(result.value).toEqual([violation]);
});
+ it('should return missingTag when all dependent tags are enabled in the policy but are not set in the transaction', () => {
+ const missingDepartmentTag = {...missingTagViolation, data: {tagName: 'Department'}};
+ const missingRegionTag = {...missingTagViolation, data: {tagName: 'Region'}};
+ const missingProjectTag = {...missingTagViolation, data: {tagName: 'Project'}};
+ transaction.tag = undefined;
+ const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories, true);
+ expect(result.value).toEqual(expect.arrayContaining([missingDepartmentTag, missingRegionTag, missingProjectTag]));
+ });
});
});