diff --git a/.eslintrc.js b/.eslintrc.js
index 661a6aa286b9..77c7fafb7a02 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -108,7 +108,7 @@ module.exports = {
'plugin:you-dont-need-lodash-underscore/all',
'plugin:prettier/recommended',
],
- plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler'],
+ plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash'],
ignorePatterns: ['lib/**'],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -231,6 +231,7 @@ module.exports = {
'you-dont-need-lodash-underscore/throttle': 'off',
// The suggested alternative (structuredClone) is not supported in Hermes:https://github.com/facebook/hermes/issues/684
'you-dont-need-lodash-underscore/clone-deep': 'off',
+ 'lodash/import-scope': ['error', 'method'],
'prefer-regex-literals': 'off',
'valid-jsdoc': 'off',
'jsdoc/no-types': 'error',
diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts
index 8bedceb894fd..bdd5713be43a 100644
--- a/.github/libs/promiseWhile.ts
+++ b/.github/libs/promiseWhile.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc} from 'lodash';
/**
diff --git a/assets/animations/BankVault.lottie b/assets/animations/BankVault.lottie
new file mode 100644
index 000000000000..82361e8e2209
Binary files /dev/null and b/assets/animations/BankVault.lottie differ
diff --git a/assets/animations/GenericEmptyState.lottie b/assets/animations/GenericEmptyState.lottie
new file mode 100644
index 000000000000..d62ff9d980bb
Binary files /dev/null and b/assets/animations/GenericEmptyState.lottie differ
diff --git a/assets/animations/TripsEmptyState.lottie b/assets/animations/TripsEmptyState.lottie
new file mode 100644
index 000000000000..8a07a6ad10d5
Binary files /dev/null and b/assets/animations/TripsEmptyState.lottie differ
diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md
index 6af3a82c2ff6..304811332916 100644
--- a/contributingGuides/STYLE.md
+++ b/contributingGuides/STYLE.md
@@ -50,6 +50,9 @@
- [Stateless components vs Pure Components vs Class based components vs Render Props](#stateless-components-vs-pure-components-vs-class-based-components-vs-render-props---when-to-use-what)
- [Use Refs Appropriately](#use-refs-appropriately)
- [Are we allowed to use [insert brand new React feature]?](#are-we-allowed-to-use-insert-brand-new-react-feature-why-or-why-not)
+- [Handling Scroll Issues with Nested Lists in React Native](#handling-scroll-issues-with-nested-lists-in-react-native)
+ - [Wrong Approach (Using SelectionList)](#wrong-approach-using-selectionlist)
+ - [Correct Approach (Using SelectionList)](#correct-approach-using-selectionlist)
- [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions)
- [Onyx Best Practices](#onyx-best-practices)
- [Collection Keys](#collection-keys)
@@ -1105,6 +1108,48 @@ There are several ways to use and declare refs and we prefer the [callback metho
We love React and learning about all the new features that are regularly being added to the API. However, we try to keep our organization's usage of React limited to the most stable set of features that React offers. We do this mainly for **consistency** and so our engineers don't have to spend extra time trying to figure out how everything is working. That said, if you aren't sure if we have adopted something, please ask us first.
+
+## Handling Scroll Issues with Nested Lists in React Native
+
+### Problem
+
+When using `SelectionList` alongside other components (e.g., `Text`, `Button`), wrapping them inside a `ScrollView` can lead to alignment and performance issues. Additionally, using `ScrollView` with nested `FlatList` or `SectionList` causes the error:
+
+> "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation."
+
+### Solution
+
+The correct approach is avoid using `ScrollView`. You can add props like `listHeaderComponent` and `listFooterComponent` to add other components before or after the list while keeping the layout scrollable.
+
+### Wrong Approach (Using `SelectionList`)
+
+```jsx
+
+ Header Content
+
+
+
+```
+
+### Correct Approach (Using `SelectionList`)
+
+```jsx
+Header Content}
+ listFooterComponent={}
+/>
+```
+
+This ensures optimal performance and avoids layout issues.
+
+
## React Hooks: Frequently Asked Questions
### Are Hooks a Replacement for HOCs or Render Props?
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index 85ffbb30c360..2d876700580d 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -138,3 +138,9 @@ platforms:
title: Settings
icon: /assets/images/gears.svg
description: Manage profile settings and notifications.
+
+ - href: billing-and-subscriptions
+ title: Expensify Billing & Subscriptions
+ icon: /assets/images/subscription-annual.svg
+ description: Review Expensify's subscription options, plan types, and payment methods.
+
diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md
new file mode 100644
index 000000000000..2ae2fcd2426d
--- /dev/null
+++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md
@@ -0,0 +1,6 @@
+---
+title: Billing and Subscriptions
+description: Coming soon
+---
+
+# Coming Soon
diff --git a/docs/new-expensify/hubs/billing-and-subscriptions/index.html b/docs/new-expensify/hubs/billing-and-subscriptions/index.html
new file mode 100644
index 000000000000..bef3c05f826b
--- /dev/null
+++ b/docs/new-expensify/hubs/billing-and-subscriptions/index.html
@@ -0,0 +1,6 @@
+---
+layout: default
+title: Billing & Subscriptions
+---
+
+{% include hub.html %}
diff --git a/package-lock.json b/package-lock.json
index f2063482c6ce..1470e5d2662e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -221,6 +221,7 @@
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsdoc": "^46.2.6",
+ "eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-storybook": "^0.8.0",
@@ -23446,6 +23447,21 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/eslint-plugin-lodash": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-7.4.0.tgz",
+ "integrity": "sha512-Tl83UwVXqe1OVeBRKUeWcfg6/pCW1GTRObbdnbEJgYwjxp5Q92MEWQaH9+dmzbRt6kvYU1Mp893E79nJiCSM8A==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": ">=2"
+ }
+ },
"node_modules/eslint-plugin-prettier": {
"version": "4.2.1",
"dev": true,
@@ -23655,8 +23671,9 @@
},
"node_modules/eslint-plugin-you-dont-need-lodash-underscore": {
"version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.14.0.tgz",
+ "integrity": "sha512-3zkkU/O1agczP7szJGHmisZJS/AknfVl6mb0Zqoc95dvFsdmfK+cbhrn+Ffy0UWB1pgDJwQr7kIO3rPstWs3Dw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"kebab-case": "^1.0.0"
},
@@ -30001,8 +30018,9 @@
},
"node_modules/kebab-case": {
"version": "1.0.2",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
+ "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==",
+ "dev": true
},
"node_modules/keyv": {
"version": "4.5.2",
diff --git a/package.json b/package.json
index 1ef7deac8d46..00d671decf17 100644
--- a/package.json
+++ b/package.json
@@ -288,6 +288,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0",
+ "eslint-plugin-lodash": "^7.4.0",
"html-webpack-plugin": "^5.5.0",
"http-server": "^14.1.1",
"jest": "29.4.1",
diff --git a/src/CONST.ts b/src/CONST.ts
index a55b21856f4a..a27a30f61582 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -189,8 +189,9 @@ const CONST = {
},
// Multiplier for gyroscope animation in order to make it a bit more subtle
ANIMATION_GYROSCOPE_VALUE: 0.4,
- ANIMATION_PAY_BUTTON_DURATION: 200,
- ANIMATION_PAY_BUTTON_HIDE_DELAY: 1000,
+ ANIMATION_PAID_DURATION: 200,
+ ANIMATION_PAID_CHECKMARK_DELAY: 300,
+ ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
SCREEN_TRANSITION_END_TIMEOUT: 1000,
ARROW_HIDE_DELAY: 3000,
@@ -5763,6 +5764,7 @@ const CONST = {
ICON_HEIGHT: 160,
CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories',
+ MEMBERS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles#import-a-group-of-members',
TAGS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags#import-a-spreadsheet-1',
},
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 95dc53f979a0..146d35611a72 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -728,6 +728,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/members',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
},
+ WORKSPACE_MEMBERS_IMPORT: {
+ route: 'settings/workspaces/:policyID/members/import',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/import` as const,
+ },
+ WORKSPACE_MEMBERS_IMPORTED: {
+ route: 'settings/workspaces/:policyID/members/imported',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/imported` as const,
+ },
POLICY_ACCOUNTING: {
route: 'settings/workspaces/:policyID/accounting',
getRoute: (policyID: string, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => {
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 23c9794fa914..496978677870 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -408,6 +408,8 @@ const SCREENS = {
INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website',
TRAVEL: 'Workspace_Travel',
MEMBERS: 'Workspace_Members',
+ MEMBERS_IMPORT: 'Members_Import',
+ MEMBERS_IMPORTED: 'Members_Imported',
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
index e5f36075b13f..8164a930ba53 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
@@ -3,7 +3,6 @@ import lodashDebounce from 'lodash/debounce';
import React, {useCallback} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
-import {runOnUI, scrollTo} from 'react-native-reanimated';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
@@ -67,11 +66,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r
const scrollToHeader = (headerIndex: number) => {
const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
- runOnUI(() => {
- 'worklet';
-
- scrollTo(emojiListRef, 0, calculatedOffset, true);
- })();
+ emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true});
};
/**
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
index 4c7483e55a6b..1a1c07558459 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.tsx
@@ -3,7 +3,6 @@ import throttle from 'lodash/throttle';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
-import {scrollTo} from 'react-native-reanimated';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
@@ -114,9 +113,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r
const filterEmojis = throttle((searchTerm: string) => {
const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm);
- if (emojiListRef.current) {
- scrollTo(emojiListRef, 0, 0, false);
- }
+ emojiListRef.current?.scrollToOffset({offset: 0, animated: false});
if (normalizedSearchTerm === '') {
// There are no headers when searching, so we need to re-make them sticky when there is no search term
setFilteredEmojis(allEmojis);
@@ -241,7 +238,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r
}
const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
- scrollTo(emojiListRef, 0, calculatedOffset, true);
+ emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true});
},
[emojiListRef],
);
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
index 9b60d15b3833..0d8acd5eef38 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
@@ -1,6 +1,5 @@
import type {FlashList} from '@shopify/flash-list';
-import {useCallback, useEffect, useMemo, useState} from 'react';
-import {useAnimatedRef} from 'react-native-reanimated';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import emojis from '@assets/emojis';
import {useFrequentlyUsedEmojis} from '@components/OnyxProvider';
import useLocalize from '@hooks/useLocalize';
@@ -10,7 +9,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as EmojiUtils from '@libs/EmojiUtils';
const useEmojiPickerMenu = () => {
- const emojiListRef = useAnimatedRef>();
+ const emojiListRef = useRef>(null);
const frequentlyUsedEmojis = useFrequentlyUsedEmojis();
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 578ea13fece9..876f1a745403 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -7,6 +7,7 @@ import Lottie from '@components/Lottie';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import VideoPlayer from '@components/VideoPlayer';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type {EmptyStateComponentProps, VideoLoadedEventType} from './types';
@@ -24,10 +25,12 @@ function EmptyStateComponent({
subtitle,
headerStyles,
headerContentStyles,
+ lottieWebViewStyles,
minModalHeight = 400,
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
+ const {isSmallScreenWidth} = useResponsiveLayout();
const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => {
if (!event) {
@@ -63,6 +66,7 @@ function EmptyStateComponent({
autoPlay
loop
style={headerContentStyles}
+ webStyle={lottieWebViewStyles}
/>
);
case CONST.EMPTY_STATE_MEDIA.ILLUSTRATION:
@@ -75,7 +79,7 @@ function EmptyStateComponent({
default:
return null;
}
- }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]);
+ }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo, lottieWebViewStyles]);
return (
@@ -88,7 +92,7 @@ function EmptyStateComponent({
{HeaderComponent}
-
+
{title}
{subtitle}
{!!buttonText && !!buttonAction && (
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts
index a30a9222c01f..4926d3b002b8 100644
--- a/src/components/EmptyStateComponent/types.ts
+++ b/src/components/EmptyStateComponent/types.ts
@@ -20,6 +20,7 @@ type SharedProps = {
headerStyles?: StyleProp;
headerMediaType: T;
headerContentStyles?: StyleProp;
+ lottieWebViewStyles?: React.CSSProperties | undefined;
minModalHeight?: number;
};
diff --git a/src/components/ImportSpreadsheetColumns.tsx b/src/components/ImportSpreadsheetColumns.tsx
index ef2c237eb5c6..7df36dd80c9d 100644
--- a/src/components/ImportSpreadsheetColumns.tsx
+++ b/src/components/ImportSpreadsheetColumns.tsx
@@ -40,7 +40,7 @@ type ImportSpreadsheetColumnsProps = {
learnMoreLink?: string;
};
-function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) {
+function ImportSpreadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -101,6 +101,6 @@ function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles
);
}
-ImportSpreeadsheetColumns.displayName = 'ImportSpreeadsheetColumns';
+ImportSpreadsheetColumns.displayName = 'ImportSpreadsheetColumns';
-export default ImportSpreeadsheetColumns;
+export default ImportSpreadsheetColumns;
diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx
index afbc9cd56e28..b9e1410809cf 100644
--- a/src/components/LottieAnimations/index.tsx
+++ b/src/components/LottieAnimations/index.tsx
@@ -79,6 +79,21 @@ const DotLottieAnimations = {
w: 180,
h: 200,
},
+ GenericEmptyState: {
+ file: require('@assets/animations/GenericEmptyState.lottie'),
+ w: 375,
+ h: 240,
+ },
+ TripsEmptyState: {
+ file: require('@assets/animations/TripsEmptyState.lottie'),
+ w: 375,
+ h: 240,
+ },
+ BankVault: {
+ file: require('@assets/animations/BankVault.lottie'),
+ w: 375,
+ h: 240,
+ },
} satisfies Record;
export default DotLottieAnimations;
diff --git a/src/components/Onfido/index.native.tsx b/src/components/Onfido/index.native.tsx
index c6eb9c8868ee..a30d70ea7464 100644
--- a/src/components/Onfido/index.native.tsx
+++ b/src/components/Onfido/index.native.tsx
@@ -27,6 +27,7 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) {
countryCode: OnfidoCountryCode.USA,
},
},
+ disableNFC: true,
})
.then(onSuccess)
.catch((error: OnfidoError) => {
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index efd5b1f99a79..f4815cd4e228 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -12,6 +12,7 @@ import ReceiptEmptyState from '@components/ReceiptEmptyState';
import Switch from '@components/Switch';
import Text from '@components/Text';
import ViolationMessages from '@components/ViolationMessages';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
@@ -175,6 +176,10 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const isAdmin = policy?.role === 'admin';
const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;
+
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const isRequestor = currentUserPersonalDetails.accountID === parentReportAction?.actorAccountID;
+
// A flag for verifying that the current report is a sub-report of a workspace chat
// if the policy of the report is either Collect or Control, then this report must be tied to workspace chat
const isPolicyExpenseChat = ReportUtils.isReportInGroupPolicy(report);
@@ -361,7 +366,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
const isReceiptAllowed = !isPaidReport && !isInvoice;
const shouldShowReceiptEmptyState =
- isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover) && (canEditReceipt || ReportUtils.isPaidGroupPolicy(report));
+ isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover || isRequestor) && (canEditReceipt || ReportUtils.isPaidGroupPolicy(report));
const [receiptImageViolations, receiptViolations] = useMemo(() => {
const imageViolations = [];
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index dcc815f2adc1..6202bdf972ea 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -1,9 +1,9 @@
import truncate from 'lodash/truncate';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import {useOnyx, withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated';
import Button from '@components/Button';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import Icon from '@components/Icon';
@@ -18,6 +18,7 @@ import Text from '@components/Text';
import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
+import usePolicy from '@hooks/usePolicy';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
@@ -39,33 +40,13 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx';
+import type {ReportAction} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import ExportWithDropdownMenu from './ExportWithDropdownMenu';
import type {PendingMessageProps} from './MoneyRequestPreview/types';
import ReportActionItemImages from './ReportActionItemImages';
-type ReportPreviewOnyxProps = {
- /** The policy tied to the expense report */
- policy: OnyxEntry;
-
- /** ChatReport associated with iouReport */
- chatReport: OnyxEntry;
-
- /** Active IOU Report for current report */
- iouReport: OnyxEntry;
-
- /** All the transactions, used to update ReportPreview label and status */
- transactions: OnyxCollection;
-
- /** All of the transaction violations */
- transactionViolations: OnyxCollection;
-
- /** The user's wallet account */
- userWallet: OnyxEntry;
-};
-
-type ReportPreviewProps = ReportPreviewOnyxProps & {
+type ReportPreviewProps = {
/** All the data of the action */
action: ReportAction;
@@ -101,24 +82,24 @@ type ReportPreviewProps = ReportPreviewOnyxProps & {
};
function ReportPreview({
- iouReport,
- policy,
iouReportID,
policyID,
chatReportID,
- chatReport,
action,
containerStyles,
contextMenuAnchor,
- transactions,
- transactionViolations,
isHovered = false,
isWhisper = false,
checkIfContextMenuActive = () => {},
onPaymentOptionsShow,
onPaymentOptionsHide,
- userWallet,
}: ReportPreviewProps) {
+ const policy = usePolicy(policyID);
+ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
+ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
+ const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+ const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -151,6 +132,18 @@ function ReportPreview({
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport);
const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
+ const previewMessageOpacity = useSharedValue(1);
+ const previewMessageStyle = useAnimatedStyle(() => ({
+ ...styles.flex1,
+ ...styles.flexRow,
+ ...styles.alignItemsCenter,
+ opacity: previewMessageOpacity.value,
+ }));
+ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
+ const checkMarkStyle = useAnimatedStyle(() => ({
+ ...styles.defaultCheckmarkWrapper,
+ transform: [{scale: checkMarkScale.value}],
+ }));
const moneyRequestComment = action?.childLastMoneyRequestComment ?? '';
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport);
@@ -285,14 +278,14 @@ function ReportPreview({
return !Number.isNaN(amount) && amount === 0;
}
- const getPreviewMessage = () => {
+ const previewMessage = useMemo(() => {
if (isScanning) {
return translate('common.receipt');
}
let payerOrApproverName;
if (isPolicyExpenseChat) {
- payerOrApproverName = ReportUtils.getPolicyName(chatReport);
+ payerOrApproverName = ReportUtils.getPolicyName(chatReport, undefined, policy);
} else if (isInvoiceRoom) {
payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport, invoiceReceiverPolicy);
} else {
@@ -310,7 +303,20 @@ function ReportPreview({
payerOrApproverName = ReportUtils.getDisplayNameForParticipant(chatReport?.ownerAccountID, true);
}
return translate(paymentVerb, {payer: payerOrApproverName});
- };
+ }, [
+ isScanning,
+ isPolicyExpenseChat,
+ policy,
+ chatReport,
+ isInvoiceRoom,
+ invoiceReceiverPolicy,
+ managerID,
+ isApproved,
+ iouSettled,
+ iouReport?.isWaitingOnBankAccount,
+ hasNonReimbursableTransactions,
+ translate,
+ ]);
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
@@ -400,6 +406,33 @@ function ReportPreview({
const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport);
+ useEffect(() => {
+ if (!isPaidAnimationRunning) {
+ return;
+ }
+
+ // eslint-disable-next-line react-compiler/react-compiler
+ previewMessageOpacity.value = withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
+ // eslint-disable-next-line react-compiler/react-compiler
+ previewMessageOpacity.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2});
+ });
+ // We only want to animate the text when the text changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [previewMessage, previewMessageOpacity]);
+
+ useEffect(() => {
+ if (!iouSettled) {
+ return;
+ }
+
+ if (isPaidAnimationRunning) {
+ // eslint-disable-next-line react-compiler/react-compiler
+ checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION}));
+ } else {
+ checkMarkScale.value = 1;
+ }
+ }, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
+
return (
)}
-
+
-
- {getPreviewMessage()}
-
+
+ {previewMessage}
+
{shouldShowRBR && (
{getDisplayAmount()}
{iouSettled && (
-
+
-
+
)}
@@ -562,23 +595,4 @@ function ReportPreview({
ReportPreview.displayName = 'ReportPreview';
-export default withOnyx({
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- chatReport: {
- key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
- },
- iouReport: {
- key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`,
- },
- transactions: {
- key: ONYXKEYS.COLLECTION.TRANSACTION,
- },
- transactionViolations: {
- key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
- },
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
-})(ReportPreview);
+export default ReportPreview;
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index c2dc4937503d..648c1dad36c3 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -19,6 +19,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
const paymentCompleteTextScale = useSharedValue(0);
const paymentCompleteTextOpacity = useSharedValue(1);
const height = useSharedValue(variables.componentSizeNormal);
+ const buttonMarginTop = useSharedValue(styles.expenseAndReportPreviewTextButtonContainer.gap);
const buttonStyles = useAnimatedStyle(() => ({
transform: [{scale: buttonScale.value}],
opacity: buttonOpacity.value,
@@ -33,6 +34,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
height: height.value,
justifyContent: 'center',
overflow: 'hidden',
+ marginTop: buttonMarginTop.value,
}));
const buttonDisabledStyle = isPaidAnimationRunning
? {
@@ -48,7 +50,8 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
paymentCompleteTextScale.value = 0;
paymentCompleteTextOpacity.value = 1;
height.value = variables.componentSizeNormal;
- }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height]);
+ buttonMarginTop.value = styles.expenseAndReportPreviewTextButtonContainer.gap;
+ }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
useEffect(() => {
if (!isPaidAnimationRunning) {
@@ -56,18 +59,19 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
return;
}
// eslint-disable-next-line react-compiler/react-compiler
- buttonScale.value = withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION});
- buttonOpacity.value = withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION});
- paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION});
+ buttonScale.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION});
+ buttonOpacity.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION});
+ paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION});
// Wait for the above animation + 1s delay before hiding the component
- const totalDelay = CONST.ANIMATION_PAY_BUTTON_DURATION + CONST.ANIMATION_PAY_BUTTON_HIDE_DELAY;
+ const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
height.value = withDelay(
totalDelay,
- withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}, () => runOnJS(onAnimationFinish)()),
+ withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
);
- paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}));
- }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]);
+ buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
+ paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
+ }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]);
return (
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
index c3d6c22abf86..b5e3f333c44a 100644
--- a/src/hooks/useDebounce.ts
+++ b/src/hooks/useDebounce.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc, DebounceSettings} from 'lodash';
import lodashDebounce from 'lodash/debounce';
import {useCallback, useEffect, useRef} from 'react';
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 78763695bc45..6e5bf7ef703c 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -687,6 +687,7 @@ export default {
singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
importSuccessfullTitle: 'Import successful',
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
+ importMembersSuccessfullDescription: (members: number) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'),
importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'),
importFailedTitle: 'Import failed',
importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.',
@@ -3269,6 +3270,7 @@ export default {
addedWithPrimary: 'Some members were added with their primary logins.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`,
membersListTitle: 'Directory of all workspace members.',
+ importMembers: 'Import members',
},
card: {
header: 'Unlock free Expensify Cards',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 26a7c92f557f..b12b5a05e98c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -681,6 +681,7 @@ export default {
importFailedTitle: 'Fallo en la importación',
importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.',
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'),
+ importMembersSuccessfullDescription: (members: number) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'),
importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'),
importSuccessfullTitle: 'Importar categorías',
importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.',
@@ -3318,6 +3319,7 @@ export default {
addedWithPrimary: 'Se agregaron algunos miembros con sus nombres de usuario principales.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.',
+ importMembers: 'Importar miembros',
},
accounting: {
settings: 'configuración',
diff --git a/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts
new file mode 100644
index 000000000000..04a114cddaf5
--- /dev/null
+++ b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts
@@ -0,0 +1,6 @@
+type ExportMembersSpreadsheetParams = {
+ /** ID of the policy */
+ policyID: string;
+};
+
+export default ExportMembersSpreadsheetParams;
diff --git a/src/libs/API/parameters/ImportMembersSpreadsheet.ts b/src/libs/API/parameters/ImportMembersSpreadsheet.ts
new file mode 100644
index 000000000000..166e43c0510f
--- /dev/null
+++ b/src/libs/API/parameters/ImportMembersSpreadsheet.ts
@@ -0,0 +1,10 @@
+type ImportMembersSpreadsheetParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{email: string, role: string}>
+ */
+ employees: string;
+};
+
+export default ImportMembersSpreadsheetParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 3e4251628394..871a2a1da2ba 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -312,6 +312,8 @@ export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPoli
export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams';
export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './UpdateQuickbooksOnlineAutoCreateVendorParams';
export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet';
+export type {default as ImportMembersSpreadsheetParams} from './ImportMembersSpreadsheet';
+export type {default as ExportMembersSpreadsheetParams} from './ExportCategoriesSpreadsheet';
export type {default as ImportTagsSpreadsheetParams} from './ImportTagsSpreadsheet';
export type {default as ExportCategoriesSpreadsheetParams} from './ExportCategoriesSpreadsheet';
export type {default as ExportTagsSpreadsheetParams} from './ExportTagsSpreadsheet';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index fb847e4059b8..37bdf6b81d6e 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -142,9 +142,11 @@ const WRITE_COMMANDS = {
SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled',
SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled',
CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories',
- IMPORT_TAGS_SREADSHEET: 'ImportTagsSpreadsheet',
IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet',
+ IMPORT_MEMBERS_SPREADSHEET: 'ImportMembersSpreadsheet',
+ IMPORT_TAGS_SPREADSHEET: 'ImportTagsSpreadsheet',
EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV',
+ EXPORT_MEMBERS_CSV: 'ExportMembersCSV',
EXPORT_TAGS_CSV: 'ExportTagsCSV',
RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory',
CREATE_POLICY_TAG: 'CreatePolicyTag',
@@ -530,9 +532,11 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams;
[WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams;
[WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams;
- [WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET]: Parameters.ImportTagsSpreadsheetParams;
[WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams;
+ [WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET]: Parameters.ImportMembersSpreadsheetParams;
+ [WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET]: Parameters.ImportTagsSpreadsheetParams;
[WRITE_COMMANDS.EXPORT_CATEGORIES_CSV]: Parameters.ExportCategoriesSpreadsheetParams;
+ [WRITE_COMMANDS.EXPORT_MEMBERS_CSV]: Parameters.ExportMembersSpreadsheetParams;
[WRITE_COMMANDS.EXPORT_TAGS_CSV]: Parameters.ExportTagsSpreadsheetParams;
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index e0c9a36fe963..8632234d0f7a 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -225,6 +225,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage').default,
[SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage').default,
[SCREENS.WORKSPACE.INVITE]: () => require('../../../../pages/workspace/WorkspaceInvitePage').default,
+ [SCREENS.WORKSPACE.MEMBERS_IMPORT]: () => require('../../../../pages/workspace/members/ImportMembersPage').default,
+ [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: () => require('../../../../pages/workspace/members/ImportedMembersPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage').default,
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EXPENSES_FROM]: () =>
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index ef8685878aaf..3d97b7be2db7 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -12,6 +12,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS,
SCREENS.WORKSPACE.OWNER_CHANGE_ERROR,
SCREENS.WORKSPACE.OWNER_CHANGE_ERROR,
+ SCREENS.WORKSPACE.MEMBERS_IMPORT,
+ SCREENS.WORKSPACE.MEMBERS_IMPORTED,
],
[SCREENS.WORKSPACE.WORKFLOWS]: [
SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 473d5928f3c0..09ed50a57395 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -569,6 +569,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.INVITE]: {
path: ROUTES.WORKSPACE_INVITE.route,
},
+ [SCREENS.WORKSPACE.MEMBERS_IMPORT]: {
+ path: ROUTES.WORKSPACE_MEMBERS_IMPORT.route,
+ },
+ [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: {
+ path: ROUTES.WORKSPACE_MEMBERS_IMPORTED.route,
+ },
[SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: {
path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 7882d90c44a8..a1fa710428df 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -194,6 +194,12 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.INVITE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.MEMBERS_IMPORT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.INVITE_MESSAGE]: {
policyID: string;
};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index bfac0d6be3d6..d529ab250687 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1011,7 +1011,7 @@ function getDomainNameForPolicy(policyID?: string): string {
return '';
}
- return `${CONST.EXPENSIFY_POLICY_DOMAIN}${policyID}${CONST.EXPENSIFY_POLICY_DOMAIN_EXTENSION}`;
+ return `${CONST.EXPENSIFY_POLICY_DOMAIN}${policyID.toLowerCase()}${CONST.EXPENSIFY_POLICY_DOMAIN_EXTENSION}`;
}
function getWorkflowApprovalsUnavailable(policy: OnyxEntry) {
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index aa6412a2bdfd..1b1ac92dc260 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -630,14 +630,6 @@ Onyx.connect({
},
});
-let isFirstTimeNewExpensifyUser = false;
-Onyx.connect({
- key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
- callback: (value) => {
- isFirstTimeNewExpensifyUser = value ?? false;
- },
-});
-
let onboarding: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.NVP_ONBOARDING,
@@ -1341,26 +1333,13 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa
});
}
- if (isFirstTimeNewExpensifyUser) {
- // Filter out the systemChat report from the reports list, as we don't want to drop the user into that report over Concierge when they first log in
- reportsValues = reportsValues.filter((report) => !isSystemChat(report)) ?? [];
- if (reportsValues.length === 1) {
- return reportsValues.at(0);
- }
+ // Filter out the system chat (Expensify chat) because the composer is disabled in it,
+ // and it prompts the user to use the Concierge chat instead.
+ reportsValues = reportsValues.filter((report) => !isSystemChat(report)) ?? [];
- return adminReport ?? reportsValues.find((report) => !isConciergeChatReport(report));
- }
-
- // If we only have two reports and one of them is the system chat, filter it out so we don't
- // overwrite showing the concierge chat
- const hasSystemChat = reportsValues.find((report) => isSystemChat(report)) ?? false;
- if (reportsValues.length === 2 && hasSystemChat) {
- reportsValues = reportsValues.filter((report) => !isSystemChat(report)) ?? [];
- }
-
- // We are getting the last read report from the metadata of the report.
+ // At least two reports remain: self DM and Concierge chat.
+ // Return the most recently visited report. Get the last read report from the report metadata.
const lastRead = getMostRecentlyVisitedReport(reportsValues, allReportMetadata);
-
return adminReport ?? lastRead;
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 79c469841fad..40a5373170ae 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -6453,7 +6453,7 @@ function getReportFromHoldRequestsOnyxData(
chatReport.reportID,
chatReport.policyID ?? iouReport.policyID ?? '',
recipient.accountID ?? 1,
- holdTransactions.reduce((acc, transaction) => acc + transaction.amount, 0) * -1,
+ holdTransactions.reduce((acc, transaction) => acc + transaction.amount, 0) * (ReportUtils.isIOUReport(iouReport) ? 1 : -1),
getCurrency(firstHoldTransaction),
false,
newParentReportActionID,
diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index 057729c7622c..32df4160b8c0 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -10,8 +10,12 @@ import type {
UpdateWorkspaceMembersRoleParams,
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
+import * as ApiUtils from '@libs/ApiUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
+import fileDownload from '@libs/fileDownload';
+import {translateLocal} from '@libs/Localize';
import Log from '@libs/Log';
+import enhanceParameters from '@libs/Network/enhanceParameters';
import Parser from '@libs/Parser';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
@@ -23,6 +27,7 @@ import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmplo
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
import type {Attributes, Rate} from '@src/types/onyx/Policy';
+import type {OnyxData} from '@src/types/onyx/Request';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {createPolicyExpenseChats} from './Policy';
@@ -167,6 +172,36 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[]
});
return announceRoomMembers;
}
+/**
+ * Updates the import spreadsheet data according to the result of the import
+ */
+function updateImportSpreadsheetData(membersLength: number): OnyxData {
+ const onyxData: OnyxData = {
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IMPORTED_SPREADSHEET,
+ value: {
+ shouldFinalModalBeOpened: true,
+ importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', membersLength)},
+ },
+ },
+ ],
+
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IMPORTED_SPREADSHEET,
+ value: {
+ shouldFinalModalBeOpened: true,
+ importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')},
+ },
+ },
+ ],
+ };
+
+ return onyxData;
+}
/**
* Build optimistic data for removing users from the announcement room
@@ -640,6 +675,22 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData});
}
+type PolicyMember = {
+ email: string;
+ role: string;
+};
+
+function importPolicyMembers(policyID: string, members: PolicyMember[]) {
+ const onyxData = updateImportSpreadsheetData(members.length);
+
+ const parameters = {
+ policyID,
+ employees: JSON.stringify(members.map((member) => ({email: member.email, role: member.role}))),
+ };
+
+ API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET, parameters, onyxData);
+}
+
/**
* Invite member to the specified policyID
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
@@ -836,6 +887,21 @@ function declineJoinRequest(reportID: string, reportAction: OnyxEntry {
+ formData.append(key, String(value));
+ });
+
+ fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MEMBERS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST);
+}
+
export {
removeMembers,
updateWorkspaceMembersRole,
@@ -850,6 +916,8 @@ export {
acceptJoinRequest,
declineJoinRequest,
isApprover,
+ importPolicyMembers,
+ downloadMembersCSV,
};
export type {NewCustomUnit};
diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts
index d6336aa58447..68f3406afafd 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -231,7 +231,7 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) {
tags: JSON.stringify(tags.map((tag) => ({name: tag.name, enabled: tag.enabled, 'GL Code': tag['GL Code']}))),
};
- API.write(WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET, parameters, onyxData);
+ API.write(WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET, parameters, onyxData);
}
function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record, tagListIndex: number) {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 3de8171a5482..91bc41f54250 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -57,7 +57,6 @@ import {prepareDraftComment} from '@libs/DraftCommentUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as Environment from '@libs/Environment/Environment';
import * as ErrorUtils from '@libs/ErrorUtils';
-import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector';
import HttpUtils from '@libs/HttpUtils';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import * as Localize from '@libs/Localize';
@@ -2697,32 +2696,29 @@ function openReportFromDeepLink(url: string) {
return;
}
- const state = navigationRef.getRootState();
- const currentFocusedRoute = findFocusedRoute(state);
- const hasCompletedGuidedSetupFlow = hasCompletedGuidedSetupFlowSelector(onboarding);
-
// We need skip deeplinking if the user hasn't completed the guided setup flow.
- if (!hasCompletedGuidedSetupFlow) {
- Welcome.isOnboardingFlowCompleted({
- onNotCompleted: () => OnboardingFlow.startOnboardingFlow(),
- });
- return;
- }
-
- if (isOnboardingFlowName(currentFocusedRoute?.name)) {
- Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
- return;
- }
-
- if (shouldSkipDeepLinkNavigation(route)) {
- return;
- }
-
- if (isAuthenticated) {
- return;
- }
-
- Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ Welcome.isOnboardingFlowCompleted({
+ onNotCompleted: () => OnboardingFlow.startOnboardingFlow(),
+ onCompleted: () => {
+ const state = navigationRef.getRootState();
+ const currentFocusedRoute = findFocusedRoute(state);
+
+ if (isOnboardingFlowName(currentFocusedRoute?.name)) {
+ Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
+ return;
+ }
+
+ if (shouldSkipDeepLinkNavigation(route)) {
+ return;
+ }
+
+ if (isAuthenticated) {
+ return;
+ }
+
+ Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH);
+ },
+ });
});
},
});
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index 1f259c96d625..edcf6ab23219 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -1,12 +1,12 @@
import React, {useMemo} from 'react';
import EmptyStateComponent from '@components/EmptyStateComponent';
-import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
@@ -19,14 +19,14 @@ function EmptySearchView({type}: EmptySearchViewProps) {
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const content = useMemo(() => {
switch (type) {
case CONST.SEARCH.DATA_TYPES.TRIP:
return {
- headerMedia: Illustrations.EmptyStateTravel,
- headerStyles: StyleUtils.getBackgroundColorStyle(theme.travelBG),
- headerContentStyles: StyleUtils.getWidthAndHeightStyle(variables.w191, variables.h172),
+ headerMedia: LottieAnimations.TripsEmptyState,
+ headerStyles: [StyleUtils.getBackgroundColorStyle(theme.travelBG), styles.w100],
title: translate('search.searchResults.emptyTripResults.title'),
subtitle: translate('search.searchResults.emptyTripResults.subtitle'),
buttonText: translate('search.searchResults.emptyTripResults.buttonText'),
@@ -37,28 +37,29 @@ function EmptySearchView({type}: EmptySearchViewProps) {
case CONST.SEARCH.DATA_TYPES.INVOICE:
default:
return {
- headerMedia: Illustrations.EmptyState,
- headerStyles: StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG),
- headerContentStyles: StyleUtils.getWidthAndHeightStyle(variables.w184, variables.h112),
+ headerMedia: LottieAnimations.GenericEmptyState,
+ headerStyles: [StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG)],
title: translate('search.searchResults.emptyResults.title'),
subtitle: translate('search.searchResults.emptyResults.subtitle'),
buttonText: undefined,
buttonAction: undefined,
+ headerContentStyles: styles.emptyStateFolderWebStyles,
};
}
- }, [type, StyleUtils, translate, theme]);
+ }, [type, StyleUtils, translate, theme, styles.w100, styles.emptyStateFolderWebStyles]);
return (
);
}
diff --git a/src/pages/Search/SavedSearchRenamePage.tsx b/src/pages/Search/SavedSearchRenamePage.tsx
index 0460ecae5317..50810ae6f175 100644
--- a/src/pages/Search/SavedSearchRenamePage.tsx
+++ b/src/pages/Search/SavedSearchRenamePage.tsx
@@ -5,6 +5,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import type {SearchQueryJSON} from '@components/Search/types';
import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
@@ -20,6 +21,7 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin
const styles = useThemeStyles();
const {q, name} = route.params;
const [newName, setNewName] = useState(name);
+ const {inputCallbackRef} = useAutoFocusInput();
const applyFiltersAndNavigate = () => {
SearchActions.clearAdvancedFilters();
@@ -62,6 +64,8 @@ function SavedSearchRenamePage({route}: {route: {params: {q: string; name: strin
accessibilityLabel={translate('search.searchName')}
role={CONST.ROLE.PRESENTATION}
onChangeText={(renamedName) => setNewName(renamedName)}
+ ref={inputCallbackRef}
+ defaultValue={name}
/>
diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx
index c5d381846631..0591d8cf2fcf 100644
--- a/src/pages/Travel/ManageTrips.tsx
+++ b/src/pages/Travel/ManageTrips.tsx
@@ -6,6 +6,7 @@ import type {FeatureListItem} from '@components/FeatureList';
import FeatureList from '@components/FeatureList';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
@@ -82,8 +83,8 @@ function ManageTrips() {
});
}}
ctaErrorMessage={ctaErrorMessage}
- illustration={Illustrations.EmptyStateTravel}
- illustrationStyle={[styles.mv4, styles.tripIllustrationSize]}
+ illustration={LottieAnimations.TripsEmptyState}
+ illustrationStyle={[styles.mv4]}
secondaryButtonText={translate('travel.bookDemo')}
secondaryButtonAccessibilityLabel={translate('travel.bookDemo')}
onSecondaryButtonPress={navigateToBookTravelDemo}
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 340624247dee..1ffdde361b21 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -1,6 +1,7 @@
import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList';
import {useIsFocused, useRoute} from '@react-navigation/native';
import type {RouteProp} from '@react-navigation/native';
+// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc} from 'lodash';
import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {DeviceEventEmitter, InteractionManager, View} from 'react-native';
@@ -632,7 +633,7 @@ function ReportActionsList({
}, [isLoadingNewerReportActions, canShowHeader, hasLoadingNewerReportActionsError, retryLoadNewerChatsError]);
const onStartReached = useCallback(() => {
- loadNewerChats(false);
+ InteractionManager.runAfterInteractions(() => requestAnimationFrame(() => loadNewerChats(false)));
}, [loadNewerChats]);
const onEndReached = useCallback(() => {
diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx
index 21ce3a7907cc..b156a6c7b2f1 100644
--- a/src/pages/settings/AboutPage/ConsolePage.tsx
+++ b/src/pages/settings/AboutPage/ConsolePage.tsx
@@ -4,7 +4,7 @@ import {format} from 'date-fns';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {ListRenderItem, ListRenderItemInfo} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
@@ -33,23 +33,15 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {CapturedLogs} from '@src/types/onyx';
-type ConsolePageOnyxProps = {
- /** Logs captured on the current device */
- capturedLogs: OnyxEntry;
-
- /** Whether or not logs should be stored */
- shouldStoreLogs: OnyxEntry;
-};
-
-type ConsolePageProps = ConsolePageOnyxProps;
-
const filterBy = {
all: '',
network: '[Network]',
} as const;
type FilterBy = (typeof filterBy)[keyof typeof filterBy];
-function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
+function ConsolePage() {
+ const [capturedLogs] = useOnyx(ONYXKEYS.LOGS);
+ const [shouldStoreLogs] = useOnyx(ONYXKEYS.SHOULD_STORE_LOGS);
const [input, setInput] = useState('');
const [isGeneratingLogsFile, setIsGeneratingLogsFile] = useState(false);
const [isLimitModalVisible, setIsLimitModalVisible] = useState(false);
@@ -159,7 +151,10 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
);
return (
-
+
Navigation.goBack(route.params?.backTo)}
@@ -228,11 +223,4 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
ConsolePage.displayName = 'ConsolePage';
-export default withOnyx({
- capturedLogs: {
- key: ONYXKEYS.LOGS,
- },
- shouldStoreLogs: {
- key: ONYXKEYS.SHOULD_STORE_LOGS,
- },
-})(ConsolePage);
+export default ConsolePage;
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
index c14c20ffc992..df89a1719ffe 100644
--- a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
+++ b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -34,13 +34,10 @@ import INPUT_IDS from '@src/types/form/ExitSurveyResponseForm';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import ExitSurveyOffline from './ExitSurveyOffline';
-type ExitSurveyResponsePageOnyxProps = {
- draftResponse: string;
-};
+type ExitSurveyResponsePageProps = StackScreenProps;
-type ExitSurveyResponsePageProps = ExitSurveyResponsePageOnyxProps & StackScreenProps;
-
-function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyResponsePageProps) {
+function ExitSurveyResponsePage({route, navigation}: ExitSurveyResponsePageProps) {
+ const [draftResponse = ''] = useOnyx(ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM_DRAFT, {selector: (value) => value?.[INPUT_IDS.RESPONSE]});
const {translate} = useLocalize();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -119,7 +116,10 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
);
return (
-
+
Navigation.goBack()}
@@ -179,9 +179,4 @@ function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyRe
ExitSurveyResponsePage.displayName = 'ExitSurveyResponsePage';
-export default withOnyx({
- draftResponse: {
- key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM_DRAFT,
- selector: (value) => value?.[INPUT_IDS.RESPONSE] ?? '',
- },
-})(ExitSurveyResponsePage);
+export default ExitSurveyResponsePage;
diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx
index ce4f2aec88d0..38dec5fe0647 100755
--- a/src/pages/settings/Profile/ProfilePage.tsx
+++ b/src/pages/settings/Profile/ProfilePage.tsx
@@ -3,19 +3,16 @@ import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import AvatarSkeleton from '@components/AvatarSkeleton';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
+import Button from '@components/Button';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemGroup from '@components/MenuItemGroup';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import {PressableWithFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
-import Text from '@components/Text';
-import Tooltip from '@components/Tooltip';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -26,7 +23,6 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as UserUtils from '@libs/UserUtils';
-import variables from '@styles/variables';
import * as PersonalDetails from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
@@ -169,25 +165,13 @@ function ProfilePage() {
brickRoadIndicator={detail.brickRoadIndicator}
/>
))}
-
-
- Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE)}
- style={[styles.button, styles.flexRow, styles.gap1, styles.ph4]}
- >
-
- {translate('common.share')}
-
-
-
+
);
}
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index 316c0b66b95d..cf13c29ffb20 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -13,6 +13,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import KYCWall from '@components/KYCWall';
import type {PaymentMethodType, Source} from '@components/KYCWall/types';
+import LottieAnimations from '@components/LottieAnimations';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -381,7 +382,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) {
title={translate('walletPage.bankAccounts')}
isCentralPane
titleStyles={styles.accountSettingsSectionTitle}
- illustration={Illustrations.BigVault}
+ illustration={LottieAnimations.BankVault}
illustrationStyle={styles.walletIllustration}
illustrationContainerStyle={{height: 220}}
illustrationBackgroundColor="#411103"
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 9de547509762..5cee6f58d1de 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -28,6 +28,7 @@ import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
@@ -37,6 +38,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {getDisplayNameForParticipant} from '@libs/ReportUtils';
+import * as Modal from '@userActions/Modal';
import * as Member from '@userActions/Policy/Member';
import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
@@ -70,10 +72,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const [errors, setErrors] = useState({});
const {isOffline} = useNetwork();
+ const {windowWidth} = useWindowDimensions();
const prevIsOffline = usePrevious(isOffline);
const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]);
const prevAccountIDs = usePrevious(accountIDs);
const textInputRef = useRef(null);
+ const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline;
const prevPersonalDetails = usePrevious(personalDetails);
const {translate, formatPhoneNumber, preferredLocale} = useLocalize();
@@ -531,7 +535,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
return null;
}
return (
-
+
{(shouldUseNarrowLayout ? canSelectMultiple : selectedEmployees.length > 0) ? (
shouldAlwaysShowDropdownMenu
@@ -558,6 +562,35 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
);
};
+ const threeDotsMenuItems = useMemo(() => {
+ const menuItems = [
+ {
+ icon: Expensicons.Table,
+ text: translate('spreadsheet.importSpreadsheet'),
+ onSelected: () => {
+ if (isOffline) {
+ Modal.close(() => setIsOfflineModalVisible(true));
+ return;
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID));
+ },
+ },
+ {
+ icon: Expensicons.Download,
+ text: translate('spreadsheet.downloadCSV'),
+ onSelected: () => {
+ if (isOffline) {
+ Modal.close(() => setIsOfflineModalVisible(true));
+ return;
+ }
+ Member.downloadMembersCSV(policyID);
+ },
+ },
+ ];
+
+ return menuItems;
+ }, [policyID, translate, isOffline]);
+
const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout;
return (
@@ -570,6 +603,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
testID={WorkspaceMembersPage.displayName}
shouldShowLoading={false}
shouldShowOfflineIndicatorInWideScreen
+ shouldShowThreeDotsButton
+ threeDotsMenuItems={threeDotsMenuItems}
+ threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
shouldShowNonAdmin
onBackButtonPress={() => {
if (selectionMode?.isEnabled) {
@@ -583,6 +619,15 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
{() => (
<>
{shouldUseNarrowLayout && {getHeaderButtons()}}
+ setIsOfflineModalVisible(false)}
+ title={translate('common.youAppearToBeOffline')}
+ prompt={translate('common.thisFeatureRequiresInternet')}
+ confirmText={translate('common.buttonConfirm')}
+ shouldShowCancelButton={false}
+ />
+
;
-
- /** User Data from Onyx */
- user: OnyxEntry;
-};
-
type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
- WorkspacePageWithSectionsOnyxProps & {
+ Pick & {
shouldSkipVBBACall?: boolean;
/** The text to display in the header */
@@ -60,9 +52,6 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
/** Option to show the loading page while the API is calling */
shouldShowLoading?: boolean;
- /** Should show the back button. It is used when in RHP. */
- shouldShowBackButton?: boolean;
-
/** Whether the offline indicator should be shown in wide screen devices */
shouldShowOfflineIndicatorInWideScreen?: boolean;
@@ -96,9 +85,6 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
/** Whether the page is loading, example any other API call in progres */
isLoading?: boolean;
-
- /** Callback to be called when the back button is pressed */
- onBackButtonPress?: () => void;
};
function fetchData(policyID: string, skipVBBACal?: boolean) {
@@ -118,13 +104,11 @@ function WorkspacePageWithSections({
headerText,
policy,
policyDraft,
- reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA,
route,
shouldUseScrollView = false,
showLoadingAsFirstRender = true,
shouldSkipVBBACall = true,
shouldShowBackButton = false,
- user,
shouldShowLoading = true,
shouldShowOfflineIndicatorInWideScreen = false,
includeSafeAreaPaddingBottom = false,
@@ -134,11 +118,17 @@ function WorkspacePageWithSections({
shouldShowNotFoundPage = false,
isLoading: isPageLoading = false,
onBackButtonPress,
+ shouldShowThreeDotsButton,
+ threeDotsMenuItems,
+ threeDotsAnchorPosition,
}: WorkspacePageWithSectionsProps) {
const styles = useThemeStyles();
const policyID = route.params?.policyID ?? '-1';
useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)});
+ const [user] = useOnyx(ONYXKEYS.USER);
+ const [reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true;
const achState = reimbursementAccount?.achData?.state ?? '-1';
@@ -149,7 +139,6 @@ function WorkspacePageWithSections({
const firstRender = useRef(showLoadingAsFirstRender);
const isFocused = useIsFocused();
const prevPolicy = usePrevious(policy);
-
useEffect(() => {
// Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true
firstRender.current = false;
@@ -197,6 +186,9 @@ function WorkspacePageWithSections({
shouldShowBackButton={shouldUseNarrowLayout || shouldShowBackButton}
icon={icon ?? undefined}
style={styles.headerBarDesktopHeight}
+ shouldShowThreeDotsButton={shouldShowThreeDotsButton}
+ threeDotsMenuItems={threeDotsMenuItems}
+ threeDotsAnchorPosition={threeDotsAnchorPosition}
>
{headerContent}
@@ -224,14 +216,4 @@ function WorkspacePageWithSections({
WorkspacePageWithSections.displayName = 'WorkspacePageWithSections';
-export default withPolicyAndFullscreenLoading(
- withOnyx({
- user: {
- key: ONYXKEYS.USER,
- },
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- })(WorkspacePageWithSections),
-);
+export default withPolicyAndFullscreenLoading(WorkspacePageWithSections);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 340bd991c609..f82591a4fc04 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -13,6 +13,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import ScreenWrapper from '@components/ScreenWrapper';
import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel';
import TableListItem from '@components/SelectionList/TableListItem';
@@ -391,12 +392,13 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
{!hasVisibleCategories && !isLoading && (
)}
{hasVisibleCategories && !isLoading && (
diff --git a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
index 52ff521dfb4e..369973485499 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceSettlementAccountPage.tsx
@@ -6,7 +6,6 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import getBankIcon from '@components/Icon/BankIcons';
import ScreenWrapper from '@components/ScreenWrapper';
-import ScrollView from '@components/ScrollView';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import Text from '@components/Text';
@@ -98,26 +97,28 @@ function WorkspaceSettlementAccountPage({route}: WorkspaceSettlementAccountPageP
title={translate('workspace.expensifyCard.settlementAccount')}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.getRoute(policyID))}
/>
-
- {translate('workspace.expensifyCard.settlementAccountDescription')}
- {isUsedContinuousReconciliation && (
-
- {translate('workspace.expensifyCard.settlementAccountInfoPt1')}{' '}
- Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.getRoute(policyID, connectionParam))}>
- {translate('workspace.expensifyCard.reconciliationAccount')}
- {' '}
- {`(${CONST.MASKED_PAN_PREFIX}${getLastFourDigits(paymentBankAccountNumber)}) `}
- {translate('workspace.expensifyCard.settlementAccountInfoPt2')}
-
- )}
- updateSettlementAccount(value ?? 0)}
- shouldSingleExecuteRowSelect
- initiallyFocusedOptionKey={paymentBankAccountID.toString()}
- />
-
+ updateSettlementAccount(value ?? 0)}
+ shouldSingleExecuteRowSelect
+ initiallyFocusedOptionKey={paymentBankAccountID.toString()}
+ listHeaderContent={
+ <>
+ {translate('workspace.expensifyCard.settlementAccountDescription')}
+ {isUsedContinuousReconciliation && (
+
+ {translate('workspace.expensifyCard.settlementAccountInfoPt1')}{' '}
+ Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.getRoute(policyID, connectionParam))}>
+ {translate('workspace.expensifyCard.reconciliationAccount')}
+ {' '}
+ {`(${CONST.MASKED_PAN_PREFIX}${getLastFourDigits(paymentBankAccountNumber)}) `}
+ {translate('workspace.expensifyCard.settlementAccountInfoPt2')}
+
+ )}
+ >
+ }
+ />
);
diff --git a/src/pages/workspace/members/ImportMembersPage.tsx b/src/pages/workspace/members/ImportMembersPage.tsx
new file mode 100644
index 000000000000..6fff3085b472
--- /dev/null
+++ b/src/pages/workspace/members/ImportMembersPage.tsx
@@ -0,0 +1,21 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import ImportSpreedsheet from '@components/ImportSpreadsheet';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type ImportMembersPageProps = StackScreenProps;
+
+function ImportMembersPage({route}: ImportMembersPageProps) {
+ const policyID = route.params.policyID;
+
+ return (
+
+ );
+}
+
+export default ImportMembersPage;
diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx
new file mode 100644
index 000000000000..5f8f38de0b83
--- /dev/null
+++ b/src/pages/workspace/members/ImportedMembersPage.tsx
@@ -0,0 +1,140 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {ColumnRole} from '@components/ImportColumn';
+import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import {closeImportPage} from '@libs/actions/ImportSpreadsheet';
+import {importPolicyMembers} from '@libs/actions/Policy/Member';
+import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type ImportedMembersPageProps = StackScreenProps;
+
+function ImportedMembersPage({route}: ImportedMembersPageProps) {
+ const {translate} = useLocalize();
+ const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET);
+ const [isImporting, setIsImporting] = useState(false);
+ const [isValidationEnabled, setIsValidationEnabled] = useState(false);
+ const policyID = route.params.policyID;
+ const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0);
+ const {containsHeader = true} = spreadsheet ?? {};
+
+ const columnRoles: ColumnRole[] = [
+ {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE},
+ {text: translate('common.email'), value: CONST.CSV_IMPORT_COLUMNS.EMAIL, isRequired: true},
+ {text: translate('common.role'), value: CONST.CSV_IMPORT_COLUMNS.ROLE},
+ ];
+
+ const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role);
+
+ // checks if all required columns are mapped and no column is mapped more than once
+ // returns found errors or empty object if both conditions are met
+ const validate = useCallback(() => {
+ const columns = Object.values(spreadsheet?.columns ?? {});
+ let errors: Record = {};
+
+ if (!requiredColumns.every((requiredColumn) => columns.includes(requiredColumn.value))) {
+ // eslint-disable-next-line rulesdir/prefer-early-return
+ requiredColumns.forEach((requiredColumn) => {
+ if (!columns.includes(requiredColumn.value)) {
+ errors.required = translate('spreadsheet.fieldNotMapped', requiredColumn.text);
+ }
+ });
+ } else {
+ const duplicate = findDuplicate(columns);
+ if (duplicate) {
+ errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicate);
+ } else {
+ errors = {};
+ }
+ }
+
+ return errors;
+ }, [requiredColumns, spreadsheet?.columns, translate]);
+
+ const importMembers = useCallback(() => {
+ setIsValidationEnabled(true);
+
+ const errors = validate();
+ if (Object.keys(errors).length > 0) {
+ return;
+ }
+
+ const columns = Object.values(spreadsheet?.columns ?? {});
+ const membersEmailsColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.EMAIL);
+ const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE);
+ const membersEmails = spreadsheet?.data[membersEmailsColumn].map((email) => email);
+ const membersRoles = membersRolesColumn !== -1 ? spreadsheet?.data[membersRolesColumn].map((role) => role) : [];
+ const members = membersEmails?.slice(containsHeader ? 1 : 0).map((email, index) => {
+ let role: string = CONST.POLICY.ROLE.USER;
+ if (membersRolesColumn !== -1 && membersRoles?.[containsHeader ? index + 1 : index]) {
+ role = membersRoles?.[containsHeader ? index + 1 : index];
+ }
+
+ return {
+ email,
+ role,
+ };
+ });
+
+ if (members) {
+ setIsImporting(true);
+ importPolicyMembers(policyID, members);
+ }
+ }, [validate, spreadsheet, containsHeader, policyID]);
+
+ const spreadsheetColumns = spreadsheet?.data;
+ if (!spreadsheetColumns) {
+ return;
+ }
+
+ const closeImportPageAndModal = () => {
+ setIsImporting(false);
+ closeImportPage();
+ Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID));
+ };
+
+ return (
+
+ Navigation.goBack(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID))}
+ />
+
+
+
+
+ );
+}
+
+ImportedMembersPage.displayName = 'ImportedMembersPage';
+
+export default ImportedMembersPage;
diff --git a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx
index 24abfb5df811..37b027439363 100644
--- a/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx
+++ b/src/pages/workspace/reportFields/ReportFieldsListValuesPage.tsx
@@ -315,9 +315,9 @@ function ReportFieldsListValuesPage({
subtitle={translate('workspace.reportFields.emptyReportFieldsValues.subtitle')}
SkeletonComponent={TableListItemSkeleton}
headerMediaType={CONST.EMPTY_STATE_MEDIA.ILLUSTRATION}
- headerMedia={Illustrations.EmptyStateExpenses}
- headerStyles={styles.emptyFolderBG}
- headerContentStyles={styles.emptyStateFolderIconSize}
+ headerMedia={Illustrations.FolderWithPapers}
+ headerStyles={styles.emptyFolderDarkBG}
+ headerContentStyles={styles.emptyStateFolderWithPaperIconSize}
/>
)}
{!shouldShowEmptyState && (
diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx
index 8fe57a13ef95..c0fffe6e39dd 100644
--- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx
+++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx
@@ -11,6 +11,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import ScreenWrapper from '@components/ScreenWrapper';
import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel';
import TableListItem from '@components/SelectionList/TableListItem';
@@ -289,10 +290,11 @@ function WorkspaceReportFieldsPage({
title={translate('workspace.reportFields.emptyReportFields.title')}
subtitle={translate('workspace.reportFields.emptyReportFields.subtitle')}
SkeletonComponent={TableListItemSkeleton}
- headerMediaType={CONST.EMPTY_STATE_MEDIA.ILLUSTRATION}
- headerMedia={Illustrations.EmptyStateExpenses}
- headerStyles={styles.emptyFolderBG}
- headerContentStyles={styles.emptyStateFolderIconSize}
+ headerMediaType={CONST.EMPTY_STATE_MEDIA.ANIMATION}
+ headerMedia={LottieAnimations.GenericEmptyState}
+ headerStyles={[styles.emptyStateCardIllustrationContainer, styles.emptyFolderBG]}
+ lottieWebViewStyles={styles.emptyStateFolderWebStyles}
+ headerContentStyles={styles.emptyStateFolderWebStyles}
/>
)}
{!shouldShowEmptyState && !isLoading && (
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 1a45210617be..940ec986b5b7 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -12,6 +12,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import ScreenWrapper from '@components/ScreenWrapper';
import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel';
@@ -397,12 +398,13 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
{!hasVisibleTags && !isLoading && (
)}
{hasVisibleTags && !isLoading && (
diff --git a/src/styles/index.ts b/src/styles/index.ts
index ad6c0552aa29..05bedf7cf3af 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4727,8 +4727,7 @@ const styles = (theme: ThemeColors) =>
},
walletIllustration: {
- width: 262,
- height: 152,
+ height: 180,
},
walletCardLimit: {
@@ -5076,6 +5075,8 @@ const styles = (theme: ThemeColors) =>
emptyStateCardIllustrationContainer: {
height: 220,
+ ...flex.alignItemsCenter,
+ ...flex.justifyContentCenter,
},
emptyStateCardIllustration: {
@@ -5113,11 +5114,6 @@ const styles = (theme: ThemeColors) =>
textDecorationLine: 'line-through',
},
- tripIllustrationSize: {
- width: 190,
- height: 172,
- },
-
reportListItemTitle: {
color: theme.text,
fontSize: variables.fontSizeNormal,
@@ -5163,14 +5159,27 @@ const styles = (theme: ThemeColors) =>
backgroundColor: theme.emptyFolderBG,
},
+ emptyFolderDarkBG: {
+ backgroundColor: '#782c04',
+ height: 220,
+ },
+
emptyStateVideo: {
borderTopLeftRadius: variables.componentBorderRadiusLarge,
borderTopRightRadius: variables.componentBorderRadiusLarge,
},
- emptyStateFolderIconSize: {
- width: 184,
- height: 112,
+ emptyStateFolderWithPaperIconSize: {
+ width: 160,
+ height: 100,
+ },
+
+ emptyStateFolderWebStyles: {
+ ...sizing.w100,
+ minWidth: 400,
+ ...flex.alignItemsCenter,
+ ...flex.justifyContentCenter,
+ ...display.dFlex,
},
workflowApprovalVerticalLine: {
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 783a9a97ac7d..79b2039f139b 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -87,6 +87,10 @@ export default {
marginVertical: 24,
},
+ mvAuto: {
+ marginVertical: 'auto',
+ },
+
mhv5: {
marginVertical: -20,
},