diff --git a/.eslintrc.js b/.eslintrc.js
index cb121953327..d9e25cc596f 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 8bedceb894f..bdd5713be43 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 00000000000..82361e8e220
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 00000000000..d62ff9d980b
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 00000000000..8a07a6ad10d
Binary files /dev/null and b/assets/animations/TripsEmptyState.lottie differ
diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md
index 6af3a82c2ff..30481133291 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 85ffbb30c36..2d876700580 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 00000000000..2ae2fcd2426
--- /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/assets/images/addbankaccount_01.png b/docs/assets/images/addbankaccount_01.png
new file mode 100644
index 00000000000..d1646127373
Binary files /dev/null and b/docs/assets/images/addbankaccount_01.png differ
diff --git a/docs/assets/images/addbankaccount_02.png b/docs/assets/images/addbankaccount_02.png
new file mode 100644
index 00000000000..6fe22ca5b0d
Binary files /dev/null and b/docs/assets/images/addbankaccount_02.png differ
diff --git a/docs/assets/images/addbankaccount_03.png b/docs/assets/images/addbankaccount_03.png
new file mode 100644
index 00000000000..3f571621eaa
Binary files /dev/null and b/docs/assets/images/addbankaccount_03.png differ
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 00000000000..bef3c05f826
--- /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 a5ae221a1e1..87c7dd3d260 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 3d808af65ba..056d29a36c1 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 9254044ee99..5a0c6f395eb 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,
@@ -5764,6 +5765,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 95dc53f979a..146d35611a7 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 23c9794fa91..49697867787 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 b5b4c2d7e71..3ca4d3bb554 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 92c35d7c768..d6c1e1f9255 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 9b60d15b383..0d8acd5eef3 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 578ea13fece..876f1a74540 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 a30a9222c01..4926d3b002b 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 95a6ea4516a..9ba0597bd3d 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 afbc9cd56e2..b9e1410809c 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/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 878fdd8a9c1..9559a19d08a 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);
@@ -357,7 +362,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 1b4d5108801..50ab8e9ee08 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 c2dc4937503..648c1dad36c 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 c3d6c22abf8..b5e3f333c44 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 bd5ff405529..eae87b70ecc 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 2f11de46fae..d94db88f272 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 00000000000..04a114cddaf
--- /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 00000000000..166e43c0510
--- /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 3e425162839..871a2a1da2b 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 fb847e4059b..37bdf6b81d6 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 e0c9a36fe96..8632234d0f7 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 ef8685878aa..3d97b7be2db 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 473d5928f3c..09ed50a5739 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 7882d90c44a..a1fa710428d 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 ddcf1c0298b..45e285f6459 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1008,7 +1008,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 c18b6c41b42..8746490ecd3 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[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 edee4a7fca6..38e5d0a3ab9 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -6450,7 +6450,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 146562990ca..eae625388f3 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 88430b94183..d6f67e496b9 100644
--- a/src/libs/actions/Policy/Tag.ts
+++ b/src/libs/actions/Policy/Tag.ts
@@ -230,7 +230,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 89eec7835e0..b038f16d003 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';
@@ -2694,32 +2693,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 1f259c96d62..edcf6ab2321 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 0460ecae531..50810ae6f17 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 c5d38184663..0591d8cf2fc 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 6828e10e7e3..d01efdf28e9 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';
@@ -626,7 +627,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 21ce3a7907c..b156a6c7b2f 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 c14c20ffc99..df89a1719ff 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 ce4f2aec88d..38dec5fe064 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 316c0b66b95..cf13c29ffb2 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 9de54750976..5cee6f58d1d 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 340bd991c60..f82591a4fc0 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 52ff521dfb4..36997348549 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 00000000000..6fff3085b47
--- /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 00000000000..5f8f38de0b8
--- /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 1ee20f1180f..937a2d5efec 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 0ffdb362ae9..e20f3e70b2f 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 84722a1f0b5..7bc4f9d9e60 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 ad6c0552aa2..05bedf7cf3a 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 783a9a97ac7..79b2039f139 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,
},