diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2ec58aab1afa..9d6aed6e96c9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009006000
- versionName "9.0.60-0"
+ versionCode 1009006100
+ versionName "9.0.61-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/attachment-not-found.svg b/assets/images/attachment-not-found.svg
new file mode 100644
index 000000000000..25da973ce9cb
--- /dev/null
+++ b/assets/images/attachment-not-found.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index be71cd4e115a..0a9417820190 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -11,6 +11,7 @@ You can create as many accounts as needed in order to test your changes directly
1. When testing chat functionality in the app please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you.
2. A member of our customer onboarding team gets auto-assigned to every new policy created by a non-paying account to help them set up. Please **do not interact with these teams, ask for calls, or support on your issues.** If you do need to test functionality inside the defaultRooms (#admins & #announce) for any issues you’re working on, please let them know that you are a contributor and don’t need assistance. They will proceed to ignore the chat.
+3. Please **do not post in any Expensify owned public room for testing** (e.g #exfy-roadmap, #new-expensify-feedback). These rooms include real customers and investors. You can create your own public rooms, or [use this test public room](https://staging.new.expensify.com/r/2091104345528462) on either staging or production. Thanks!
#### Generating Multiple Test Accounts
You can generate multiple test accounts by using a `+` postfix, for example if your email is test@test.com, you can create multiple New Expensify accounts connected to the same email address by using test+123@test.com, test+456@test.com, etc.
diff --git a/contributingGuides/PERFORMANCE_METRICS.md b/contributingGuides/PERFORMANCE_METRICS.md
index 6c40e346a3ce..ecebbaae4e0e 100644
--- a/contributingGuides/PERFORMANCE_METRICS.md
+++ b/contributingGuides/PERFORMANCE_METRICS.md
@@ -14,21 +14,16 @@ Project is using Firebase for tracking these metrics. However, not all of them a
| `js_loaded` | ✅ | The time it takes for the JavaScript bundle to load.
**Platforms:** Android, iOS | **Android:** Starts in the `onCreate` method.
**iOS:** Starts in the AppDelegate's `didFinishLaunchingWithOptions` method. | Stops at the first render of the app via native module on the JS side. |
| `_app_in_foreground` | ✅ | The time when the app is running in the foreground and available to the user.
**Platforms:** Android, iOS | **Android:** Starts when the first activity to reach the foreground has its `onResume()` method called.
**iOS:** Starts when the application receives the `UIApplicationDidBecomeActiveNotification` notification. | **Android:** Stops when the last activity to leave the foreground has its `onStop()` method called.
**iOS:** Stops when it receives the `UIApplicationWillResignActiveNotification` notification. |
| `_app_in_background` | ✅ | Time when the app is running in the background.
**Platforms:** Android, iOS | **Android:** Starts when the last activity to leave the foreground has its `onStop()` method called.
**iOS:** Starts when the application receives the `UIApplicationWillResignActiveNotification` notification. | **Android:** Stops when the first activity to reach the foreground has its `onResume()` method called.
**iOS:** Stops when it receives the `UIApplicationDidBecomeActiveNotification` notification. |
-| `homepage_initial_render` | ✅ | Time taken for the initial render of the app for a logged in user.
**Platforms:** All | Starts with the first render of the `AuthScreens` component. | Stops once the `AuthScreens` component is mounted. |
-| `sidebar_loaded` | ❌ | Time taken for the Sidebar to load.
**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the Splash Screen is hidden. |
+| `sidebar_loaded` | ❌ | Time taken for the Sidebar to load.
**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the LHN finishes laying out. |
| `calc_most_recent_last_modified_action` | ✅ | Time taken to find the most recently modified report action or report.
**Platforms:** All | Starts when the app reconnects to the network | Ends when the app reconnects to the network and the most recent report action or report is found. |
-| `search_render` | ✅ | Time taken to render the Chat Finder page.
**Platforms:** All | Starts when the Chat Finder icon in LHN is pressed. | Stops when the list of available options is rendered for the first time. |
-| `load_search_options` | ✅ | Time taken to generate the list of options used in Chat Finder.
**Platforms:** All | Starts when the `getSearchOptions` function is called. | Stops when the list of available options is generated. |
-| `search_filter_options` | ✅ | Time taken to filter search options in Chat Finder by given search value.
**Platforms:** All | Starts when user types something in the Chat Finder search input. | Stops when the list of filtered options is generated. |
+| `open_search` | ✅ | Time taken to open up the Search Router.
**Platforms:** All | Starts when the Search Router icon in LHN is pressed. | Stops when the list of available options finishes laying out. |
+| `load_search_options` | ✅ | Time taken to generate the list of options used in the Search Router.
**Platforms:** All | Starts when the `getSearchOptions` function is called. | Stops when the list of available options is generated. |
+| `search_filter_options` | ✅ | Time taken to filter search options in the Search Router by the given search value.
**Platforms:** All | Starts when user types something in the Search Router search input. | Stops when the list of filtered options is generated. |
| `trie_initialization` | ✅ | Time taken to build the emoji trie.
**Platforms:** All | Starts when emoji trie begins to build. | Stops when emoji trie building is complete. |
-| `open_report` | ❌ | Time taken to open a report.
**Platforms:** All | Starts when the row in the `LHNOptionsList` is pressed. | Stops when the `ReportActionsList` finishes laying out. |
-| `switch_report` | ✅ | Time taken to open report.
**Platforms:** All | Starts when the chat in the LHN is pressed. | Stops when the `ReportActionsList` finishes laying out. |
+| `open_report` | ✅ | Time taken to open a report.
**Platforms:** All | Starts when the row in the `LHNOptionsList` is pressed. | Stops when the `ReportActionsList` finishes laying out. |
| `open_report_from_preview` | ✅ | Time taken to open a report from preview.
(previously `switch_report_from_preview`)
**Platforms:** All | Starts when the user presses the Report Preview. | Stops when the `ReportActionsList` finishes laying out. |
-| `switch_report_from_preview` | ❌ | **[REMOVED]** Time taken to open a report from preview. | Starts when the user presses the Report Preview. | Stops when the `ReportActionsList` finishes laying out. |
-| `chat_render` | ✅ | Time taken to render the Report screen.
**Platforms:** All | Starts when the `ReportScreen` is being rendered for the first time. | Stops once the `ReportScreen` component is mounted. |
-| `report_initial_render` | ❌ | Time taken to render the Report screen.
**Platforms:** All | Starts when the first item is rendered in the `LHNOptionsList`. | Stops when the `ReportActionsList` finishes laying out. |
| `open_report_thread` | ✅ | Time taken to open a thread in a report.
**Platforms:** All | Starts when user presses Report Action Item. | Stops when the `ReportActionsList` finishes laying out. |
-| `message_sent` | ❌ | Time taken to send a message.
**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. |
+| `send_message` | ✅ | Time taken to send a message.
**Platforms:** All | Starts when the new message is sent. | Stops when the message is being rendered in the chat. |
## Documentation Maintenance
@@ -46,4 +41,4 @@ To ensure this documentation remains accurate and useful, please adhere to the f
## Additional Resources
- [Firebase Documentation](https://firebase.google.com/docs)
-- [Firebase Performance Monitoring](https://firebase.google.com/docs/perf-mon)
\ No newline at end of file
+- [Firebase Performance Monitoring](https://firebase.google.com/docs/perf-mon)
diff --git a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md
index 3fd1df0c0a1c..a6e19f8fd549 100644
--- a/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md
+++ b/docs/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online.md
@@ -40,6 +40,7 @@ The following steps help you determine how data will be exported from Expensify
- Journal Entries - This is a single itemized journal entry for each Expensify report.
- _Non-reimbursable expenses_: Non-reimbursable expenses export to QuickBooks Online as:
- Credit Card expenses - Each expense will be exported as a bank transaction with its transaction date.
+ - Note: The Expensify Card transactions will always export as Credit Card charges, even if the non-reimbursable setting is configured differently (such as a Vendor Bill.)
- Debit Card Expenses - Each expense will be exported as a bank transaction with its transaction date.
- Vendor Bills - A single detailed vendor bill is generated for each Expensify report.
- If the accounting period is closed, the vendor bill will be posted on the first day of the next open period. If you choose to export non-reimbursable expenses as Vendor Bills, you can assign a default vendor to the bill.
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 7b263d98cf27..537de56b131c 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.0.60
+ 9.0.61CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.60.0
+ 9.0.61.0FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 90916937f184..328e27f2578f 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.0.60
+ 9.0.61CFBundleSignature????CFBundleVersion
- 9.0.60.0
+ 9.0.61.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 5e3c61e29256..7efe1888d4ae 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.0.60
+ 9.0.61CFBundleVersion
- 9.0.60.0
+ 9.0.61.0NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 0a5aee49900d..b318c1a7f31c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.60-0",
+ "version": "9.0.61-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.60-0",
+ "version": "9.0.61-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 54b3a3c945cb..8d6612308505 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.60-0",
+ "version": "9.0.61-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch b/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch
new file mode 100644
index 000000000000..2ee8aa1fd0de
--- /dev/null
+++ b/patches/react-native+0.75.2+020+keyboard-avoiding-view.patch
@@ -0,0 +1,18 @@
+diff --git a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
+index e26d677..597be5a 100644
+--- a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
++++ b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
+@@ -175,6 +175,13 @@ class KeyboardAvoidingView extends React.Component {
+ }
+
+ componentDidMount(): void {
++ // Fix KeyboardAvoidingView not aware of the keyboard closing after it is unmounted.
++ // Remove this patch after the upstream fix https://github.com/facebook/react-native/commit/08bd8ac47da60121225e7b281bbf566e2c5a291e is released.
++ if (!Keyboard.isVisible()) {
++ this._keyboardEvent = null;
++ this._setBottom(0);
++ }
++
+ if (Platform.OS === 'ios') {
+ this._subscriptions = [
+ Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange),
diff --git a/src/CONST.ts b/src/CONST.ts
index d9c3b72d2d1a..4e873163cc95 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -297,6 +297,7 @@ const CONST = {
DEFAULT_TABLE_NAME: 'keyvaluepairs',
DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt',
DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL],
+ DEFAULT_IMAGE_FILE_NAME: 'image',
DISABLED_MAX_EXPENSE_VALUE: 10000000000,
POLICY_BILLABLE_MODES: {
BILLABLE: 'billable',
@@ -1253,17 +1254,13 @@ const CONST = {
},
TIMING: {
CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action',
- SEARCH_ROUTER_RENDER: 'search_router_render',
- CHAT_RENDER: 'chat_render',
+ OPEN_SEARCH: 'open_search',
OPEN_REPORT: 'open_report',
- HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render',
- REPORT_INITIAL_RENDER: 'report_initial_render',
- SWITCH_REPORT: 'switch_report',
OPEN_REPORT_FROM_PREVIEW: 'open_report_from_preview',
OPEN_REPORT_THREAD: 'open_report_thread',
SIDEBAR_LOADED: 'sidebar_loaded',
LOAD_SEARCH_OPTIONS: 'load_search_options',
- MESSAGE_SENT: 'message_sent',
+ SEND_MESSAGE: 'send_message',
COLD: 'cold',
WARM: 'warm',
REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500,
@@ -1858,7 +1855,6 @@ const CONST = {
JOBS: 'jobs',
},
},
- NETSUITE_CUSTOM_LIST_LIMIT: 8,
NETSUITE_ADD_CUSTOM_LIST_STEP_NAMES: ['1', '2,', '3', '4'],
NETSUITE_ADD_CUSTOM_SEGMENT_STEP_NAMES: ['1', '2,', '3', '4', '5', '6,'],
},
@@ -4547,9 +4543,6 @@ const CONST = {
},
INDENTS: ' ',
PARENT_CHILD_SEPARATOR: ': ',
- CATEGORY_LIST_THRESHOLD: 8,
- TAG_LIST_THRESHOLD: 8,
- TAX_RATES_LIST_THRESHOLD: 8,
COLON: ':',
MAPBOX: {
PADDING: 32,
@@ -4633,11 +4626,6 @@ const CONST = {
*/
MAX_SELECTION_LIST_PAGE_LENGTH: 500,
- /**
- * We only include the members search bar when we have 8 or more members
- */
- SHOULD_SHOW_MEMBERS_SEARCH_INPUT_BREAKPOINT: 8,
-
/**
* Bank account names
*/
@@ -4811,7 +4799,6 @@ const CONST = {
WORKSPACE_SWITCHER: {
NAME: 'Expensify',
SUBSCRIPT_ICON_SIZE: 8,
- MINIMUM_WORKSPACES_TO_SHOW_SEARCH: 8,
},
WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`,
@@ -5840,7 +5827,6 @@ const CONST = {
MAX_TAX_RATE_INTEGER_PLACES: 4,
MAX_TAX_RATE_DECIMAL_PLACES: 4,
- MIN_TAX_RATE_DECIMAL_PLACES: 2,
DOWNLOADS_PATH: '/Downloads',
DOWNLOADS_TIMEOUT: 5000,
@@ -5862,9 +5848,6 @@ const CONST = {
ACTION_TYPES: {
VIEW: 'view',
REVIEW: 'review',
- SUBMIT: 'submit',
- APPROVE: 'approve',
- PAY: 'pay',
DONE: 'done',
PAID: 'paid',
},
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index e07b03a6d405..1d0100add00f 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -30,7 +30,6 @@ import NavigationRoot from './libs/Navigation/NavigationRoot';
import NetworkConnection from './libs/NetworkConnection';
import PushNotification from './libs/Notification/PushNotification';
import './libs/Notification/PushNotification/subscribePushNotification';
-import Performance from './libs/Performance';
import setCrashlyticsUserId from './libs/setCrashlyticsUserId';
import StartupTimer from './libs/StartupTimer';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
@@ -138,7 +137,6 @@ function Expensify() {
const onSplashHide = useCallback(() => {
setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN);
- Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
}, [setSplashScreenState]);
useLayoutEffect(() => {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index cd94035e0fff..103c4b2c3125 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -310,11 +310,13 @@ const ROUTES = {
},
ATTACHMENTS: {
route: 'attachment',
- getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean) => {
+ getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, fileName?: string) => {
const reportParam = reportID ? `&reportID=${reportID}` : '';
const accountParam = accountID ? `&accountID=${accountID}` : '';
const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : '';
- return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}` as const;
+ const fileNameParam = fileName ? `&fileName=${fileName}` : '';
+
+ return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${fileNameParam}` as const;
},
},
REPORT_PARTICIPANTS: {
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
index 4de43a763231..5800e92cc4f4 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
@@ -4,6 +4,7 @@ import {View} from 'react-native';
import AttachmentView from '@components/Attachments/AttachmentView';
import type {Attachment} from '@components/Attachments/types';
import Button from '@components/Button';
+import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import Text from '@components/Text';
@@ -83,6 +84,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr
isHovered={isModalHovered}
isFocused={isFocused}
duration={item.duration}
+ fallbackSource={Expensicons.AttachmentNotFound}
/>
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 0af1a86992e7..1281c017308d 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -10,6 +10,7 @@ import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -127,7 +128,7 @@ function AttachmentView({
const [imageError, setImageError] = useState(false);
- useNetwork({onReconnect: () => setImageError(false)});
+ const {isOffline} = useNetwork({onReconnect: () => setImageError(false)});
useEffect(() => {
FileUtils.getFileResolution(file).then((resolution) => {
@@ -226,15 +227,20 @@ function AttachmentView({
if (isFileImage) {
if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) {
return (
-
+
+
+
+ {translate('attachmentView.attachmentNotFound')}
+
+
);
}
+
let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string);
if (isHighResolution) {
@@ -268,6 +274,9 @@ function AttachmentView({
isImage={isFileImage}
onPress={onPress}
onError={() => {
+ if (isOffline) {
+ return;
+ }
setImageError(true);
}}
/>
diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx
index 33d97c6909f5..0c855507371a 100644
--- a/src/components/CategoryPicker.tsx
+++ b/src/components/CategoryPicker.tsx
@@ -56,7 +56,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr
const categoryData = categoryOptions?.at(0)?.data ?? [];
const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue);
const categoriesCount = OptionsListUtils.getEnabledCategoriesCount(categories);
- const isCategoriesCountBelowThreshold = categoriesCount < CONST.CATEGORY_LIST_THRESHOLD;
+ const isCategoriesCountBelowThreshold = categoriesCount < CONST.STANDARD_LIST_ITEM_LIMIT;
const showInput = !isCategoriesCountBelowThreshold;
return [categoryOptions, header, showInput];
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index be0227375470..52ccf50cd722 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -109,28 +109,31 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
}
}, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]);
- const adjustForMaintainVisibleContentPosition = useCallback(() => {
- if (mvcpMinIndexForVisible == null) {
- return;
- }
+ const adjustForMaintainVisibleContentPosition = useCallback(
+ (animated = true) => {
+ if (mvcpMinIndexForVisible == null) {
+ return;
+ }
- const firstVisibleView = firstVisibleViewRef.current;
- const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current;
- if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) {
- return;
- }
+ const firstVisibleView = firstVisibleViewRef.current;
+ const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current;
+ if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) {
+ return;
+ }
- const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop;
- const delta = firstVisibleViewOffset - prevFirstVisibleOffset;
- if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) {
- const scrollOffset = lastScrollOffsetRef.current;
- prevFirstVisibleOffsetRef.current = firstVisibleViewOffset;
- scrollToOffset(scrollOffset + delta, false, true);
- if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) {
- scrollToOffset(0, true, false);
+ const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop;
+ const delta = firstVisibleViewOffset - prevFirstVisibleOffset;
+ if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) {
+ const scrollOffset = lastScrollOffsetRef.current;
+ prevFirstVisibleOffsetRef.current = firstVisibleViewOffset;
+ scrollToOffset(scrollOffset + delta, false, true);
+ if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) {
+ scrollToOffset(0, animated, false);
+ }
}
- }
- }, [scrollToOffset, mvcpMinIndexForVisible, horizontal]);
+ },
+ [scrollToOffset, mvcpMinIndexForVisible, horizontal],
+ );
const setupMutationObserver = useCallback(() => {
const contentView = getContentView();
@@ -141,6 +144,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
mutationObserverRef.current?.disconnect();
const mutationObserver = new MutationObserver((mutations) => {
+ let isEditComposerAdded = false;
// Check if the first visible view is removed and re-calculate it
// if needed.
mutations.forEach((mutation) => {
@@ -150,6 +154,12 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
}
firstVisibleViewRef.current = null;
});
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType !== Node.ELEMENT_NODE || !(node as HTMLElement).querySelector('#composer')) {
+ return;
+ }
+ isEditComposerAdded = true;
+ });
});
if (firstVisibleViewRef.current == null) {
@@ -162,7 +172,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
return;
}
- adjustForMaintainVisibleContentPosition();
+ adjustForMaintainVisibleContentPosition(!isEditComposerAdded);
prepareForMaintainVisibleContentPosition();
});
mutationObserver.observe(contentView, {
diff --git a/src/components/FocusModeNotification.tsx b/src/components/FocusModeNotification.tsx
index 7b3f567d256b..fe63fb4b487b 100644
--- a/src/components/FocusModeNotification.tsx
+++ b/src/components/FocusModeNotification.tsx
@@ -24,6 +24,8 @@ function FocusModeNotification() {
confirmText={translate('common.buttonConfirm')}
onConfirm={User.clearFocusModeNotification}
shouldShowCancelButton={false}
+ onBackdropPress={User.clearFocusModeNotification}
+ onCancel={User.clearFocusModeNotification}
prompt={
{translate('focusModeUpdateModal.prompt')}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
index 17fbe1656020..f53e490dd0f9 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
@@ -1,4 +1,4 @@
-import React, {memo, useState} from 'react';
+import React, {memo} from 'react';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
@@ -67,9 +67,14 @@ function ImageRenderer({tnode}: ImageRendererProps) {
const fileType = FileUtils.getFileType(attachmentSourceAttribute);
const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Expensicons.Document : Expensicons.GalleryNotFound;
- const [hasLoadFailed, setHasLoadFailed] = useState(true);
const theme = useTheme();
+ let fileName = htmlAttribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src}`);
+ const fileInfo = FileUtils.splitExtensionFromFileName(fileName);
+ if (!fileInfo.fileExtension) {
+ fileName = `${fileInfo?.fileName || CONST.DEFAULT_IMAGE_FILE_NAME}.jpg`;
+ }
+
const thumbnailImageComponent = (
setHasLoadFailed(true)}
- onMeasure={() => setHasLoadFailed(false)}
fallbackIconBackground={theme.highlightBG}
fallbackIconColor={theme.border}
/>
@@ -101,7 +104,7 @@ function ImageRenderer({tnode}: ImageRendererProps) {
return;
}
- const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt);
+ const route = ROUTES.ATTACHMENTS?.getRoute(reportID ?? '-1', type, source, accountID, isAttachmentOrReceipt, fileName);
Navigation.navigate(route);
}}
onLongPress={(event) => {
@@ -113,7 +116,6 @@ function ImageRenderer({tnode}: ImageRendererProps) {
shouldUseHapticsOnLongPress
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
- disabled={hasLoadFailed}
>
{thumbnailImageComponent}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index fa531ce34adf..bd4bb64da050 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -8,6 +8,7 @@ import ArrowRight from '@assets/images/arrow-right.svg';
import ArrowUpLong from '@assets/images/arrow-up-long.svg';
import UpArrow from '@assets/images/arrow-up.svg';
import ArrowsUpDown from '@assets/images/arrows-updown.svg';
+import AttachmentNotFound from '@assets/images/attachment-not-found.svg';
import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg';
import AnnounceRoomAvatar from '@assets/images/avatars/announce-room.svg';
import ConciergeAvatar from '@assets/images/avatars/concierge-avatar.svg';
@@ -217,6 +218,7 @@ export {
ArrowsUpDown,
ArrowUpLong,
ArrowDownLong,
+ AttachmentNotFound,
Wrench,
BackArrow,
Bank,
diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx
index 266ed2eed16a..0bce2fd38432 100644
--- a/src/components/ImageView/index.tsx
+++ b/src/components/ImageView/index.tsx
@@ -196,8 +196,12 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
document.removeEventListener('mouseup', trackPointerPosition);
};
}, [canUseTouchScreen, trackMovement, trackPointerPosition]);
-
- const isLocalFile = FileUtils.isLocalFile(url);
+ // isLocalToUserDeviceFile means the file is located on the user device,
+ // not loaded on the server yet (the user is offline when loading this file in fact)
+ let isLocalToUserDeviceFile = FileUtils.isLocalFile(url);
+ if (isLocalToUserDeviceFile && typeof url === 'string' && url.startsWith('/chat-attachments')) {
+ isLocalToUserDeviceFile = false;
+ }
if (canUseTouchScreen) {
return (
@@ -238,8 +242,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
/>
- {isLoading && (!isOffline || isLocalFile) && }
- {isLoading && !isLocalFile && }
+ {isLoading && (!isOffline || isLocalToUserDeviceFile) && }
+ {isLoading && !isLocalToUserDeviceFile && }
);
}
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 94116181bccb..3e3f4d1b8e5d 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -30,6 +30,7 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import FreeTrial from '@pages/settings/Subscription/FreeTrial';
import variables from '@styles/variables';
+import Timing from '@userActions/Timing';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -193,6 +194,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
ref={popoverAnchor}
onPress={(event) => {
Performance.markStart(CONST.TIMING.OPEN_REPORT);
+ Timing.start(CONST.TIMING.OPEN_REPORT);
event?.preventDefault();
// Enable Composer to focus on clicking the same chat after opening the context menu.
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 9067f1abb11a..d476d1198808 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -27,6 +27,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import HapticFeedback from '@libs/HapticFeedback';
import Navigation from '@libs/Navigation/Navigation';
+import Performance from '@libs/Performance';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -456,6 +457,7 @@ function ReportPreview({
{
+ Performance.markStart(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID));
}}
diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx
index d73937aeadd9..a330be3d5ff6 100644
--- a/src/components/Search/SearchPageHeader.tsx
+++ b/src/components/Search/SearchPageHeader.tsx
@@ -182,7 +182,7 @@ function SearchPageHeader({queryJSON, hash}: SearchPageHeaderProps) {
return;
}
- const reportIDList = selectedReports?.filter((report) => !!report) ?? [];
+ const reportIDList = (selectedReports?.filter((report) => !!report) as string[]) ?? [];
SearchActions.exportSearchItemsToCSV(
{query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']},
() => {
diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx
index 76eacd8b991d..90699e951998 100644
--- a/src/components/Search/SearchRouter/SearchButton.tsx
+++ b/src/components/Search/SearchRouter/SearchButton.tsx
@@ -30,8 +30,8 @@ function SearchButton({style}: SearchButtonProps) {
accessibilityLabel={translate('common.search')}
style={[styles.flexRow, styles.touchableButtonImage, style]}
onPress={Session.checkIfActionIsAllowed(() => {
- Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER);
- Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER);
+ Timing.start(CONST.TIMING.OPEN_SEARCH);
+ Performance.markStart(CONST.TIMING.OPEN_SEARCH);
openSearchRouter();
})}
diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx
index cc854ff926c3..45e30a6bad6d 100644
--- a/src/components/Search/SearchRouter/SearchRouterList.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterList.tsx
@@ -69,8 +69,8 @@ type SearchRouterListProps = {
};
const setPerformanceTimersEnd = () => {
- Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER);
- Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER);
+ Timing.end(CONST.TIMING.OPEN_SEARCH);
+ Performance.markEnd(CONST.TIMING.OPEN_SEARCH);
};
function getContextualSearchQuery(reportName: string) {
diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts
index 130ad7ae6f6e..74bf7b16d020 100644
--- a/src/components/Search/types.ts
+++ b/src/components/Search/types.ts
@@ -24,13 +24,6 @@ type SelectedTransactionInfo = {
/** Model of selected results */
type SelectedTransactions = Record;
-/** Model of payment data used by Search bulk actions */
-type PaymentData = {
- reportID: string;
- amount: number;
- paymentType: ValueOf;
-};
-
type SortOrder = ValueOf;
type SearchColumnType = ValueOf;
type ExpenseSearchStatus = ValueOf;
@@ -124,6 +117,5 @@ export type {
TripSearchStatus,
ChatSearchStatus,
SearchAutocompleteResult,
- PaymentData,
SearchAutocompleteQueryRange,
};
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index 55e2cf6f849d..faafa6159dc1 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -15,9 +15,6 @@ import type {SearchTransactionAction} from '@src/types/onyx/SearchResults';
const actionTranslationsMap: Record = {
view: 'common.view',
review: 'common.review',
- submit: 'common.submit',
- approve: 'iou.approve',
- pay: 'iou.pay',
done: 'common.done',
paid: 'iou.settledExpensify',
};
@@ -29,18 +26,9 @@ type ActionCellProps = {
goToItem: () => void;
isChildListItem?: boolean;
parentAction?: string;
- isLoading?: boolean;
};
-function ActionCell({
- action = CONST.SEARCH.ACTION_TYPES.VIEW,
- isLargeScreenWidth = true,
- isSelected = false,
- goToItem,
- isChildListItem = false,
- parentAction = '',
- isLoading = false,
-}: ActionCellProps) {
+function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false, goToItem, isChildListItem = false, parentAction = ''}: ActionCellProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
@@ -73,8 +61,9 @@ function ActionCell({
);
}
+ const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {};
+
if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) {
- const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {};
return isLargeScreenWidth ? (
- );
+ if (action === CONST.SEARCH.ACTION_TYPES.REVIEW) {
+ return (
+
+ );
+ }
}
ActionCell.displayName = 'ActionCell';
diff --git a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
index 384262a78b15..185b9991e0a6 100644
--- a/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
+++ b/src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
@@ -28,7 +28,6 @@ type ExpenseItemHeaderNarrowProps = {
isDisabled?: boolean | null;
isDisabledCheckbox?: boolean;
handleCheckboxPress?: () => void;
- isLoading?: boolean;
};
function ExpenseItemHeaderNarrow({
@@ -45,7 +44,6 @@ function ExpenseItemHeaderNarrow({
isDisabled,
handleCheckboxPress,
text,
- isLoading = false,
}: ExpenseItemHeaderNarrowProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -104,7 +102,6 @@ function ExpenseItemHeaderNarrow({
goToItem={onButtonPress}
isLargeScreenWidth={false}
isSelected={isSelected}
- isLoading={isLoading}
/>
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index b03932db2532..7add6be940d2 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import Checkbox from '@components/Checkbox';
-import {useSearchContext} from '@components/Search/SearchContext';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
@@ -11,7 +10,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import {handleActionButtonPress} from '@libs/actions/Search';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
@@ -71,7 +69,6 @@ function ReportListItem({
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
const StyleUtils = useStyleUtils();
- const {currentSearchHash} = useSearchContext();
const animatedHighlightStyle = useAnimatedHighlightStyle({
borderRadius: variables.componentBorderRadius,
@@ -96,7 +93,7 @@ function ReportListItem({
];
const handleOnButtonPress = () => {
- handleActionButtonPress(currentSearchHash, reportItem, () => onSelectRow(item));
+ onSelectRow(item);
};
const openReportInRHP = (transactionItem: TransactionListItemType) => {
@@ -168,7 +165,6 @@ function ReportListItem({
action={reportItem.action}
onButtonPress={handleOnButtonPress}
containerStyle={[styles.ph3, styles.pt1half, styles.mb1half]}
- isLoading={reportItem.isActionLoading}
/>
)}
@@ -203,7 +199,6 @@ function ReportListItem({
action={reportItem.action}
goToItem={handleOnButtonPress}
isSelected={item.isSelected}
- isLoading={reportItem.isActionLoading}
/>
)}
diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx
index a23a7048644e..1a27599ed0bd 100644
--- a/src/components/SelectionList/Search/TransactionListItem.tsx
+++ b/src/components/SelectionList/Search/TransactionListItem.tsx
@@ -1,12 +1,10 @@
import React from 'react';
-import {useSearchContext} from '@components/Search/SearchContext';
import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import {handleActionButtonPress} from '@libs/actions/Search';
import variables from '@styles/variables';
import TransactionListItemRow from './TransactionListItemRow';
@@ -28,7 +26,6 @@ function TransactionListItem({
const theme = useTheme();
const {isLargeScreenWidth} = useResponsiveLayout();
- const {currentSearchHash} = useSearchContext();
const listItemPressableStyle = [
styles.selectionListPressableItemWrapper,
@@ -78,14 +75,13 @@ function TransactionListItem({
item={transactionItem}
showTooltip={showTooltip}
onButtonPress={() => {
- handleActionButtonPress(currentSearchHash, transactionItem, () => onSelectRow(item));
+ onSelectRow(item);
}}
onCheckboxPress={() => onCheckboxPress?.(item)}
isDisabled={!!isDisabled}
canSelectMultiple={!!canSelectMultiple}
isButtonSelected={item.isSelected}
shouldShowTransactionCheckbox={false}
- isLoading={transactionItem.isActionLoading}
/>
);
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 6bd071fa5ded..37b0429b7caa 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -59,7 +59,6 @@ type TransactionListItemRowProps = {
isButtonSelected?: boolean;
parentAction?: string;
shouldShowTransactionCheckbox?: boolean;
- isLoading?: boolean;
};
const getTypeIcon = (type?: SearchTransactionType) => {
@@ -258,7 +257,6 @@ function TransactionListItemRow({
isButtonSelected = false,
parentAction = '',
shouldShowTransactionCheckbox,
- isLoading = false,
}: TransactionListItemRowProps) {
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
@@ -282,7 +280,6 @@ function TransactionListItemRow({
isDisabled={item.isDisabled}
isDisabledCheckbox={item.isDisabledCheckbox}
handleCheckboxPress={onCheckboxPress}
- isLoading={isLoading}
/>
)}
@@ -448,7 +445,6 @@ function TransactionListItemRow({
isChildListItem={isChildListItem}
parentAction={parentAction}
goToItem={onButtonPress}
- isLoading={isLoading}
/>
diff --git a/src/components/SelectionList/SearchTableHeaderColumn.tsx b/src/components/SelectionList/SearchTableHeaderColumn.tsx
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx
index 9d3a70d4d50c..a64c4c276606 100644
--- a/src/components/TagPicker/index.tsx
+++ b/src/components/TagPicker/index.tsx
@@ -52,7 +52,7 @@ function TagPicker({selectedTag, tagListName, policyID, tagListIndex, shouldShow
const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]);
const policyTagList = PolicyUtils.getTagList(policyTags, tagListIndex);
const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags);
- const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD;
+ const isTagsCountBelowThreshold = policyTagsCount < CONST.STANDARD_LIST_ITEM_LIMIT;
const shouldShowTextInput = !isTagsCountBelowThreshold;
diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx
index 78ccb1425e21..a91965811d81 100644
--- a/src/components/TaxPicker.tsx
+++ b/src/components/TaxPicker.tsx
@@ -1,33 +1,21 @@
import React, {useCallback, useMemo, useState} from 'react';
-import {withOnyx} from 'react-native-onyx';
-import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {EdgeInsets} from 'react-native-safe-area-context';
import type {ValueOf} from 'type-fest';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import * as IOUUtils from '@libs/IOUUtils';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import type {IOUAction} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy, Transaction} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SelectionList from './SelectionList';
import RadioListItem from './SelectionList/RadioListItem';
-type TaxPickerOnyxProps = {
- /** The policy which the user has access to and which the report is tied to */
- policy: OnyxEntry;
-
- /** All the data for the transaction */
- transaction: OnyxEntry;
-
- /** The draft transaction that holds data to be persisted on the current split transaction */
- splitDraftTransaction: OnyxEntry;
-};
-
-type TaxPickerProps = TaxPickerOnyxProps & {
+type TaxPickerProps = {
/** The selected tax rate of an expense */
selectedTaxRate?: string;
@@ -58,10 +46,16 @@ type TaxPickerProps = TaxPickerOnyxProps & {
onDismiss: () => void;
};
-function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit, action, splitDraftTransaction, iouType, onDismiss}: TaxPickerProps) {
+function TaxPicker({selectedTaxRate = '', policyID, transactionID, insets, onSubmit, action, iouType, onDismiss}: TaxPickerProps) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');
+ const policy = PolicyUtils.getPolicy(policyID);
+ const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`);
+ const [defaultTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
+ const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`);
+
+ const transaction = IOUUtils.shouldUseTransactionDraft(action) ? draftTransaction : defaultTransaction;
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT;
@@ -69,7 +63,7 @@ function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit,
const taxRates = policy?.taxRates;
const taxRatesCount = TransactionUtils.getEnabledTaxRateCount(taxRates?.taxes ?? {});
- const isTaxRatesCountBelowThreshold = taxRatesCount < CONST.TAX_RATES_LIST_THRESHOLD;
+ const isTaxRatesCountBelowThreshold = taxRatesCount < CONST.STANDARD_LIST_ITEM_LIMIT;
const shouldShowTextInput = !isTaxRatesCountBelowThreshold;
@@ -125,19 +119,4 @@ function TaxPicker({selectedTaxRate = '', policy, transaction, insets, onSubmit,
TaxPicker.displayName = 'TaxPicker';
-export default withOnyx({
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- transaction: {
- key: ({transactionID, action}) => {
- if (IOUUtils.shouldUseTransactionDraft(action)) {
- return `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`;
- }
- return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
- },
- },
- splitDraftTransaction: {
- key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
- },
-})(TaxPicker);
+export default TaxPicker;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7f05ea436837..fa78b563c522 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1970,6 +1970,7 @@ const translations = {
afterLinkText: 'to view it.',
formLabel: 'View PDF',
},
+ attachmentNotFound: 'Attachment not found',
},
messages: {
errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US, please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 92ffd8728d97..0c682569263f 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1990,6 +1990,7 @@ const translations = {
afterLinkText: 'para verlo.',
formLabel: 'Ver PDF',
},
+ attachmentNotFound: 'Archivo adjunto no encontrado',
},
messages: {
errorMessageInvalidPhone: `Por favor, introduce un número de teléfono válido sin paréntesis o guiones. Si reside fuera de Estados Unidos, por favor incluye el prefijo internacional (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts
index a12c0fdc32f2..10174f0baa37 100644
--- a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts
+++ b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts
@@ -1,10 +1,11 @@
type PayMoneyRequestOnSearchParams = {
hash: number;
+ paymentType: string;
/**
* Stringified JSON object with type of following structure:
- * Array<{reportID: string, amount: number, paymentType: string}>
+ * Array<{reportID: string, amount: number}>
*/
- paymentData: string;
+ reportsAndAmounts: string;
};
export default PayMoneyRequestOnSearchParams;
diff --git a/src/libs/API/parameters/SetCompanyCardExportAccountParams.ts b/src/libs/API/parameters/SetCompanyCardExportAccountParams.ts
index 861345ff9c55..09f7b1340a36 100644
--- a/src/libs/API/parameters/SetCompanyCardExportAccountParams.ts
+++ b/src/libs/API/parameters/SetCompanyCardExportAccountParams.ts
@@ -1,7 +1,7 @@
type SetCompanyCardExportAccountParams = {
authToken?: string | null;
cardID: number;
- exportAccountDetails: Record;
+ exportAccountDetails: string;
};
export default SetCompanyCardExportAccountParams;
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index b701a32a7c98..f9ac681cb468 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -164,8 +164,7 @@ function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRE
return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,
- minimumFractionDigits: CONST.MIN_TAX_RATE_DECIMAL_PLACES,
- maximumFractionDigits: CONST.MAX_TAX_RATE_DECIMAL_PLACES,
+ minimumFractionDigits: CONST.MAX_TAX_RATE_DECIMAL_PLACES,
});
}
diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts
index cf0c4889aa69..62a01e43755d 100644
--- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts
+++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts
@@ -27,10 +27,9 @@ const test = (config: NativeConfig) => {
console.debug('[E2E] Logged in, getting chat opening metrics and submitting them…');
- const [renderChatPromise, renderChatResolve] = getPromiseWithResolve();
const [chatTTIPromise, chatTTIResolve] = getPromiseWithResolve();
- Promise.all([renderChatPromise, chatTTIPromise]).then(() => {
+ chatTTIPromise.then(() => {
console.debug(`[E2E] Submitting!`);
E2EClient.submitTestDone();
@@ -46,22 +45,6 @@ const test = (config: NativeConfig) => {
console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`);
- if (entry.name === CONST.TIMING.CHAT_RENDER) {
- E2EClient.submitTestResults({
- branch: Config.E2E_BRANCH,
- name: `${name} Chat opening`,
- metric: entry.duration,
- unit: 'ms',
- })
- .then(() => {
- console.debug('[E2E] Done with chat opening, exiting…');
- renderChatResolve();
- })
- .catch((err) => {
- console.debug('[E2E] Error while submitting test results:', err);
- });
- }
-
if (entry.name === CONST.TIMING.OPEN_REPORT) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
diff --git a/src/libs/E2E/tests/linkingTest.e2e.ts b/src/libs/E2E/tests/linkingTest.e2e.ts
index 18ba438c2ca6..2a85a5dabe6c 100644
--- a/src/libs/E2E/tests/linkingTest.e2e.ts
+++ b/src/libs/E2E/tests/linkingTest.e2e.ts
@@ -1,7 +1,6 @@
import {DeviceEventEmitter} from 'react-native';
-import type {NativeConfig} from 'react-native-config';
import Config from 'react-native-config';
-import Timing from '@libs/actions/Timing';
+import type {NativeConfig} from 'react-native-config';
import E2ELogin from '@libs/E2E/actions/e2eLogin';
import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded';
import E2EClient from '@libs/E2E/client';
@@ -32,9 +31,9 @@ const test = (config: NativeConfig) => {
}
const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve();
- const [switchReportPromise, switchReportResolve] = getPromiseWithResolve();
+ const [openReportPromise, openReportResolve] = getPromiseWithResolve();
- Promise.all([appearMessagePromise, switchReportPromise])
+ Promise.all([appearMessagePromise, openReportPromise])
.then(() => {
console.debug('[E2E] Test completed successfully, exiting…');
E2EClient.submitTestDone();
@@ -57,21 +56,15 @@ const test = (config: NativeConfig) => {
Performance.subscribeToMeasurements((entry) => {
if (entry.name === CONST.TIMING.SIDEBAR_LOADED) {
console.debug('[E2E] Sidebar loaded, navigating to a report…');
+ Performance.markStart(CONST.TIMING.OPEN_REPORT);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID));
return;
}
- if (entry.name === CONST.TIMING.REPORT_INITIAL_RENDER) {
- console.debug('[E2E] Navigating to linked report action…');
- Timing.start(CONST.TIMING.SWITCH_REPORT);
- Performance.markStart(CONST.TIMING.SWITCH_REPORT);
-
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(linkedReportID, linkedReportActionID));
- return;
- }
-
- if (entry.name === CONST.TIMING.SWITCH_REPORT) {
+ if (entry.name === CONST.TIMING.OPEN_REPORT) {
console.debug('[E2E] Linking: 1');
+ console.debug('[E2E] Navigating to the linked report action…');
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(linkedReportID, linkedReportActionID));
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
@@ -80,7 +73,7 @@ const test = (config: NativeConfig) => {
unit: 'ms',
});
- switchReportResolve();
+ openReportResolve();
}
});
});
diff --git a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
index de9464c9c286..4fd2b26e63c8 100644
--- a/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
+++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts
@@ -60,7 +60,7 @@ const test = (config: NativeConfig) => {
props.onPress();
}
- if (entry.name === CONST.TIMING.SEARCH_ROUTER_RENDER) {
+ if (entry.name === CONST.TIMING.OPEN_SEARCH) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
name: `${name} Open Search Router TTI`,
diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts
index e042a688c37d..473bf317e6c0 100644
--- a/src/libs/E2E/tests/reportTypingTest.e2e.ts
+++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts
@@ -43,7 +43,7 @@ const test = (config: NativeConfig) => {
});
Performance.subscribeToMeasurements((entry) => {
- if (entry.name === CONST.TIMING.MESSAGE_SENT) {
+ if (entry.name === CONST.TIMING.SEND_MESSAGE) {
E2EClient.submitTestResults({
branch: Config.E2E_BRANCH,
name: `${name} Message sent`,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 58586250f958..a061d5b52d22 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -43,7 +43,6 @@ import * as PriorityMode from '@userActions/PriorityMode';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import toggleTestToolsModal from '@userActions/TestTool';
-import Timing from '@userActions/Timing';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
@@ -248,8 +247,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
// eslint-disable-next-line react-compiler/react-compiler
if (isInitialRender.current) {
- Timing.start(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
-
const currentURL = getCurrentUrl();
if (currentURL) {
initialReportID = new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1);
@@ -313,8 +310,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
}
Download.clearDownloads();
- Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
-
const unsubscribeOnyxModal = onyxSubscribe({
key: ONYXKEYS.MODAL,
callback: (modalArg) => {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index ba859efff944..fc9601424080 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1552,6 +1552,7 @@ type AuthScreensParamList = CentralPaneScreensParamList &
type: ValueOf;
accountID: string;
isAuthTokenRequired?: string;
+ fileName?: string;
};
[SCREENS.PROFILE_AVATAR]: {
accountID: string;
diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts
index 643ed64ae7f6..3f4da20c16e1 100644
--- a/src/libs/Network/SequentialQueue.ts
+++ b/src/libs/Network/SequentialQueue.ts
@@ -138,8 +138,8 @@ function flush() {
return;
}
- if (PersistedRequests.getAll().length === 0) {
- Log.info('[SequentialQueue] Unable to flush. No requests to process.');
+ if (PersistedRequests.getAll().length === 0 && QueuedOnyxUpdates.isEmpty()) {
+ Log.info('[SequentialQueue] Unable to flush. No requests or queued Onyx updates to process.');
return;
}
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index f6decd6fb2f4..6bcb353cf065 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -171,9 +171,6 @@ type GetOptionsConfig = {
taxRates?: TaxRatesWithDefault;
policy?: OnyxEntry;
transaction?: OnyxEntry;
- includePolicyReportFieldOptions?: boolean;
- policyReportFieldOptions?: string[];
- recentlyUsedPolicyReportFieldOptions?: string[];
transactionViolations?: OnyxCollection;
includeInvoiceRooms?: boolean;
includeDomainEmail?: boolean;
@@ -217,7 +214,6 @@ type Options = {
categoryOptions: CategoryTreeSection[];
tagOptions: CategorySection[];
taxRatesOptions: CategorySection[];
- policyReportFieldOptions?: CategorySection[] | null;
};
type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
@@ -1126,7 +1122,7 @@ function getCategoryListSections(
const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name);
const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name));
- if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) {
+ if (numberOfEnabledCategories < CONST.STANDARD_LIST_ITEM_LIMIT) {
const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState);
categorySections.push({
// "All" section when items amount less than the threshold
@@ -1249,7 +1245,7 @@ function getTagListSections(
return tagSections;
}
- if (numberOfTags < CONST.TAG_LIST_THRESHOLD) {
+ if (numberOfTags < CONST.STANDARD_LIST_ITEM_LIMIT) {
tagSections.push({
// "All" section when items amount less than the threshold
title: '',
@@ -1309,81 +1305,6 @@ function hasEnabledTags(policyTagList: Array ({
- text: name,
- keyForList: name,
- searchText: name,
- tooltipText: name,
- isDisabled: false,
- }));
-}
-
-/**
- * Build the section list for report field options
- */
-function getReportFieldOptionsSection(options: string[], recentlyUsedOptions: string[], selectedOptions: Array>, searchInputValue: string) {
- const reportFieldOptionsSections = [];
- const selectedOptionKeys = selectedOptions.map(({text, keyForList, name}) => text ?? keyForList ?? name ?? '').filter((o) => !!o);
- let indexOffset = 0;
-
- if (searchInputValue) {
- const searchOptions = options.filter((option) => option.toLowerCase().includes(searchInputValue.toLowerCase()));
-
- reportFieldOptionsSections.push({
- // "Search" section
- title: '',
- shouldShow: true,
- indexOffset,
- data: getReportFieldOptions(searchOptions),
- });
-
- return reportFieldOptionsSections;
- }
-
- const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((recentlyUsedOption) => !selectedOptionKeys.includes(recentlyUsedOption));
- const filteredOptions = options.filter((option) => !selectedOptionKeys.includes(option));
-
- if (selectedOptionKeys.length) {
- reportFieldOptionsSections.push({
- // "Selected" section
- title: '',
- shouldShow: true,
- indexOffset,
- data: getReportFieldOptions(selectedOptionKeys),
- });
-
- indexOffset += selectedOptionKeys.length;
- }
-
- if (filteredRecentlyUsedOptions.length > 0) {
- reportFieldOptionsSections.push({
- // "Recent" section
- title: Localize.translateLocal('common.recent'),
- shouldShow: true,
- indexOffset,
- data: getReportFieldOptions(filteredRecentlyUsedOptions),
- });
-
- indexOffset += filteredRecentlyUsedOptions.length;
- }
-
- reportFieldOptionsSections.push({
- // "All" section when items amount more than the threshold
- title: Localize.translateLocal('common.all'),
- shouldShow: true,
- indexOffset,
- data: getReportFieldOptions(filteredOptions),
- });
-
- return reportFieldOptionsSections;
-}
-
/**
* Sorts tax rates alphabetically by name.
*/
@@ -1459,7 +1380,7 @@ function getTaxRatesSection(policy: OnyxEntry | undefined, selectedOptio
return policyRatesSections;
}
- if (numberOfTaxRates < CONST.TAX_RATES_LIST_THRESHOLD) {
+ if (numberOfTaxRates < CONST.STANDARD_LIST_ITEM_LIMIT) {
policyRatesSections.push({
// "All" section when items amount less than the threshold
title: '',
@@ -1727,9 +1648,6 @@ function getOptions(
policy,
transaction,
includeSelfDM = false,
- includePolicyReportFieldOptions = false,
- policyReportFieldOptions = [],
- recentlyUsedPolicyReportFieldOptions = [],
includeInvoiceRooms = false,
includeDomainEmail = false,
action,
@@ -1779,20 +1697,6 @@ function getOptions(
};
}
- if (includePolicyReportFieldOptions) {
- const transformedPolicyReportFieldOptions = getReportFieldOptionsSection(policyReportFieldOptions, recentlyUsedPolicyReportFieldOptions, selectedOptions, searchInputValue);
- return {
- recentReports: [],
- personalDetails: [],
- userToInvite: null,
- currentUserOption: null,
- categoryOptions: [],
- tagOptions: [],
- taxRatesOptions: [],
- policyReportFieldOptions: transformedPolicyReportFieldOptions,
- };
- }
-
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue)));
const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchInputValue.toLowerCase();
const topmostReportId = Navigation.getTopmostReportId() ?? '-1';
@@ -2145,9 +2049,6 @@ type FilteredOptionsParams = {
taxRates?: TaxRatesWithDefault;
maxRecentReportsToShow?: number;
includeSelfDM?: boolean;
- includePolicyReportFieldOptions?: boolean;
- policyReportFieldOptions?: string[];
- recentlyUsedPolicyReportFieldOptions?: string[];
includeInvoiceRooms?: boolean;
action?: IOUAction;
sortByReportTypeInSearch?: boolean;
@@ -2186,9 +2087,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
taxRates = {} as TaxRatesWithDefault,
includeSelfDM = false,
- includePolicyReportFieldOptions = false,
- policyReportFieldOptions = [],
- recentlyUsedPolicyReportFieldOptions = [],
includeInvoiceRooms = false,
action,
sortByReportTypeInSearch = false,
@@ -2215,9 +2113,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
includeTaxRates,
taxRates,
includeSelfDM,
- includePolicyReportFieldOptions,
- policyReportFieldOptions,
- recentlyUsedPolicyReportFieldOptions,
includeInvoiceRooms,
action,
sortByReportTypeInSearch,
@@ -2260,9 +2155,6 @@ function getAttendeeOptions(
maxRecentReportsToShow: 0,
taxRates: {} as TaxRatesWithDefault,
includeSelfDM: false,
- includePolicyReportFieldOptions: false,
- policyReportFieldOptions: [],
- recentlyUsedPolicyReportFieldOptions: [],
includeInvoiceRooms,
action,
sortByReportTypeInSearch,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index f038006730af..4592500e9250 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -29,7 +29,6 @@ import type {
Tenant,
} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
-import type {SearchPolicy} from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {hasSynchronizationErrorMessage} from './actions/connections';
import {getCategoryApproverRule} from './CategoryUtils';
@@ -129,7 +128,7 @@ function getNumericValue(value: number | string, toLocaleDigit: (arg: string) =>
if (Number.isNaN(numValue)) {
return NaN;
}
- return numValue;
+ return numValue.toFixed(CONST.CUSTOM_UNITS.RATE_DECIMALS);
}
/**
@@ -161,10 +160,11 @@ function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => stri
}
if (withDecimals) {
- const decimalPart = numValue.toString().split('.').at(1) ?? '';
- // Set the fraction digits to be between 2 and 4 (OD Behavior)
- const fractionDigits = Math.min(Math.max(decimalPart.length, CONST.MIN_TAX_RATE_DECIMAL_PLACES), CONST.MAX_TAX_RATE_DECIMAL_PLACES);
- return Number(numValue).toFixed(fractionDigits).toString().replace('.', toLocaleDigit('.'));
+ const decimalPart = numValue.toString().split('.').at(1);
+ if (decimalPart) {
+ const fixedDecimalPoints = decimalPart.length > 2 && !decimalPart.endsWith('0') ? 3 : 2;
+ return Number(numValue).toFixed(fixedDecimalPoints).toString().replace('.', toLocaleDigit('.'));
+ }
}
return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length);
@@ -184,7 +184,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, isConnecti
return undefined;
}
-function getPolicyRole(policy: OnyxInputOrEntry | SearchPolicy, currentUserLogin: string | undefined) {
+function getPolicyRole(policy: OnyxInputOrEntry, currentUserLogin: string | undefined) {
return policy?.role ?? policy?.employeeList?.[currentUserLogin ?? '-1']?.role;
}
@@ -218,7 +218,7 @@ const isUserPolicyAdmin = (policy: OnyxInputOrEntry, login?: string) =>
/**
* Checks if the current user is an admin of the policy.
*/
-const isPolicyAdmin = (policy: OnyxInputOrEntry | SearchPolicy, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.ADMIN;
+const isPolicyAdmin = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.ADMIN;
/**
* Checks if the current user is of the role "user" on the policy.
@@ -379,7 +379,7 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean {
return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
-function isPaidGroupPolicy(policy: OnyxEntry | SearchPolicy): boolean {
+function isPaidGroupPolicy(policy: OnyxEntry): boolean {
return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
}
@@ -404,7 +404,7 @@ function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry | SearchPolicy): boolean {
+function isInstantSubmitEnabled(policy: OnyxInputOrEntry): boolean {
return policy?.autoReporting === true && policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT;
}
@@ -434,7 +434,7 @@ function getCorrectedAutoReportingFrequency(policy: OnyxInputOrEntry): V
/**
* Checks if policy's approval mode is "optional", a.k.a. "Submit & Close"
*/
-function isSubmitAndClose(policy: OnyxInputOrEntry | SearchPolicy): boolean {
+function isSubmitAndClose(policy: OnyxInputOrEntry): boolean {
return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL;
}
diff --git a/src/libs/ReportFieldOptionsListUtils.ts b/src/libs/ReportFieldOptionsListUtils.ts
new file mode 100644
index 000000000000..b3b302fdaaea
--- /dev/null
+++ b/src/libs/ReportFieldOptionsListUtils.ts
@@ -0,0 +1,90 @@
+import * as Localize from './Localize';
+import type {Option} from './OptionsListUtils';
+import type * as ReportUtils from './ReportUtils';
+
+/**
+ * Transforms the provided report field options into option objects.
+ *
+ * @param reportFieldOptions - an initial report field options array
+ */
+function getReportFieldOptions(reportFieldOptions: string[]): Option[] {
+ return reportFieldOptions.map((name) => ({
+ text: name,
+ keyForList: name,
+ searchText: name,
+ tooltipText: name,
+ isDisabled: false,
+ }));
+}
+
+/**
+ * Build the section list for report field options
+ */
+function getReportFieldOptionsSection({
+ options,
+ recentlyUsedOptions,
+ selectedOptions,
+ searchValue,
+}: {
+ options: string[];
+ recentlyUsedOptions: string[];
+ selectedOptions: Array>;
+ searchValue: string;
+}) {
+ const reportFieldOptionsSections = [];
+ const selectedOptionKeys = selectedOptions.map(({text, keyForList, name}) => text ?? keyForList ?? name ?? '').filter((o) => !!o);
+ let indexOffset = 0;
+
+ if (searchValue) {
+ const searchOptions = options.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase()));
+
+ reportFieldOptionsSections.push({
+ // "Search" section
+ title: '',
+ shouldShow: true,
+ indexOffset,
+ data: getReportFieldOptions(searchOptions),
+ });
+
+ return reportFieldOptionsSections;
+ }
+
+ const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((recentlyUsedOption) => !selectedOptionKeys.includes(recentlyUsedOption));
+ const filteredOptions = options.filter((option) => !selectedOptionKeys.includes(option));
+
+ if (selectedOptionKeys.length) {
+ reportFieldOptionsSections.push({
+ // "Selected" section
+ title: '',
+ shouldShow: true,
+ indexOffset,
+ data: getReportFieldOptions(selectedOptionKeys),
+ });
+
+ indexOffset += selectedOptionKeys.length;
+ }
+
+ if (filteredRecentlyUsedOptions.length > 0) {
+ reportFieldOptionsSections.push({
+ // "Recent" section
+ title: Localize.translateLocal('common.recent'),
+ shouldShow: true,
+ indexOffset,
+ data: getReportFieldOptions(filteredRecentlyUsedOptions),
+ });
+
+ indexOffset += filteredRecentlyUsedOptions.length;
+ }
+
+ reportFieldOptionsSections.push({
+ // "All" section when items amount more than the threshold
+ title: Localize.translateLocal('common.all'),
+ shouldShow: true,
+ indexOffset,
+ data: getReportFieldOptions(filteredOptions),
+ });
+
+ return reportFieldOptionsSections;
+}
+
+export {getReportFieldOptionsSection, getReportFieldOptions};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ce510d0c9624..28e3a0144141 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -54,7 +54,6 @@ import type {Status} from '@src/types/onyx/PersonalDetails';
import type {ConnectionName} from '@src/types/onyx/Policy';
import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report';
import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction';
-import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults';
import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -844,21 +843,21 @@ function isChatReport(report: OnyxEntry): boolean {
return report?.type === CONST.REPORT.TYPE.CHAT;
}
-function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boolean {
+function isInvoiceReport(report: OnyxInputOrEntry): boolean {
return report?.type === CONST.REPORT.TYPE.INVOICE;
}
/**
* Checks if a report is an Expense report.
*/
-function isExpenseReport(report: OnyxInputOrEntry | SearchReport): boolean {
+function isExpenseReport(report: OnyxInputOrEntry): boolean {
return report?.type === CONST.REPORT.TYPE.EXPENSE;
}
/**
* Checks if a report is an IOU report using report or reportID
*/
-function isIOUReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean {
+function isIOUReport(reportOrID: OnyxInputOrEntry | string): boolean {
const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
return report?.type === CONST.REPORT.TYPE.IOU;
}
@@ -967,15 +966,12 @@ function hasParticipantInArray(report: OnyxEntry, memberAccountIDs: numb
/**
* Whether the Money Request report is settled
*/
-function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string | undefined): boolean {
- if (!reportOrID) {
- return false;
- }
- const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
- if (!report) {
+function isSettled(reportID: string | undefined): boolean {
+ const allReports = ReportConnection.getAllReports();
+ if (!allReports || !reportID) {
return false;
}
-
+ const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null;
if (isEmptyObject(report) || report.isWaitingOnBankAccount) {
return false;
}
@@ -1452,7 +1448,7 @@ function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean
* Whether the provided report is an archived room
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-function isArchivedRoom(report: OnyxInputOrEntry | SearchReport, reportNameValuePairs?: OnyxInputOrEntry): boolean {
+function isArchivedRoom(report: OnyxInputOrEntry, reportNameValuePairs?: OnyxInputOrEntry): boolean {
return !!report?.private_isArchived;
}
@@ -1468,7 +1464,7 @@ function isArchivedRoomWithID(reportID?: string) {
/**
* Whether the provided report is a closed report
*/
-function isClosedReport(report: OnyxEntry | SearchReport): boolean {
+function isClosedReport(report: OnyxEntry): boolean {
return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
}
@@ -1656,7 +1652,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean {
/**
* Checks if a report is an IOU or expense report.
*/
-function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean {
+function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | string): boolean {
const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID;
return isIOUReport(report) || isExpenseReport(report);
}
@@ -1705,6 +1701,16 @@ function isOneTransactionThread(reportID: string, parentReportID: string, thread
return reportID === transactionThreadReportID && !ReportActionsUtils.isSentMoneyReportAction(threadParentReportAction);
}
+/**
+ * Get displayed report ID, it will be parentReportID if the report is one transaction thread
+ */
+function getDisplayedReportID(reportID: string): string {
+ const report = getReport(reportID);
+ const parentReportID = report?.parentReportID ?? '';
+ const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, report?.parentReportActionID ?? '');
+ return isOneTransactionThread(reportID, parentReportID, parentReportAction) ? parentReportID : reportID;
+}
+
/**
* Should return true only for personal 1:1 report
*
@@ -1723,9 +1729,9 @@ function isOneOnOneChat(report: OnyxEntry): boolean {
* Checks if the current user is a payer of the expense
*/
-function isPayer(session: OnyxEntry, iouReport: OnyxEntry, onlyShowPayElsewhere = false, reportPolicy?: OnyxInputOrEntry | SearchPolicy) {
+function isPayer(session: OnyxEntry, iouReport: OnyxEntry, onlyShowPayElsewhere = false) {
const isApproved = isReportApproved(iouReport);
- const policy = reportPolicy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`] ?? null;
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`] ?? null;
const policyType = policy?.type;
const isAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN;
const isManager = iouReport?.managerID === session?.accountID;
@@ -7952,7 +7958,7 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct
});
}
-function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | SearchPolicy): boolean {
+function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry): boolean {
if (isEmptyObject(policy)) {
return false;
}
@@ -8564,6 +8570,7 @@ export {
getTaskAssigneeChatOnyxData,
getTransactionDetails,
getTransactionReportName,
+ getDisplayedReportID,
getTransactionsWithReceipts,
getUserDetailTooltipText,
getWhisperDisplayNames,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 29537b9c8368..a7ce065a6d23 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -11,14 +11,12 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type SearchResults from '@src/types/onyx/SearchResults';
-import type {ListItemDataType, ListItemType, SearchDataTypes, SearchPersonalDetails, SearchReport, SearchTransaction, SearchTransactionAction} from '@src/types/onyx/SearchResults';
-import * as IOU from './actions/IOU';
+import type {ListItemDataType, ListItemType, SearchDataTypes, SearchPersonalDetails, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import {translateLocal} from './Localize';
import Navigation from './Navigation/Navigation';
import * as ReportActionsUtils from './ReportActionsUtils';
-import * as ReportUtils from './ReportUtils';
import * as TransactionUtils from './TransactionUtils';
const columnNamesToSortingProperty = {
@@ -211,6 +209,7 @@ function getIOUReportName(data: OnyxTypes.SearchResults['data'], reportItem: Sea
*/
function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']): TransactionListItemType[] {
const shouldShowMerchant = getShouldShowMerchant(data);
+
const doesDataContainAPastYearTransaction = shouldShowYear(data);
return Object.keys(data)
@@ -224,7 +223,6 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
return {
...transactionItem,
- action: getAction(data, key),
from,
to,
formattedFrom,
@@ -242,63 +240,6 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
});
}
-/**
- * @private
- * Returns the action that can be taken on a given transaction or report
- *
- * Do not use directly, use only via `getSections()` facade.
- */
-function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTransactionAction {
- const isTransaction = isTransactionEntry(key);
- if ((!isTransaction && !isReportEntry(key)) || (isTransaction && !data[key].isFromOneTransactionReport)) {
- return CONST.SEARCH.ACTION_TYPES.VIEW;
- }
-
- const transaction = isTransaction ? data[key] : undefined;
- const report = isTransaction ? data[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] : data[key];
-
- // We don't need to run the logic if this is not a transaction or iou/expense report, so let's shortcircuit the logic for performance reasons
- if (!ReportUtils.isMoneyRequestReport(report)) {
- return CONST.SEARCH.ACTION_TYPES.VIEW;
- }
-
- const chatReport = data[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`] ?? {};
- const chatReportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.chatReportID}`] ?? undefined;
-
- if (ReportUtils.isSettled(report)) {
- return CONST.SEARCH.ACTION_TYPES.PAID;
- }
-
- if (ReportUtils.isClosedReport(report)) {
- return CONST.SEARCH.ACTION_TYPES.DONE;
- }
-
- const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? {};
-
- const invoiceReceiverPolicy =
- ReportUtils.isInvoiceReport(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS
- ? data[`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver?.policyID}`]
- : undefined;
-
- const allReportTransactions = (
- isReportEntry(key)
- ? Object.entries(data)
- .filter(([itemKey, value]) => isTransactionEntry(itemKey) && (value as SearchTransaction)?.reportID === report.reportID)
- .map((item) => item[1])
- : []
- ) as SearchTransaction[];
-
- if (IOU.canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy)) {
- return CONST.SEARCH.ACTION_TYPES.PAY;
- }
-
- if (IOU.canApproveIOU(report, policy)) {
- return CONST.SEARCH.ACTION_TYPES.APPROVE;
- }
-
- return CONST.SEARCH.ACTION_TYPES.VIEW;
-}
-
/**
* @private
* Organizes data into List Sections for display, for the ReportActionListItemType of Search Results.
@@ -350,7 +291,6 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
reportIDToTransactions[reportKey] = {
...reportItem,
- action: getAction(data, key),
keyForList: reportItem.reportID,
from: data.personalDetailsList?.[reportItem.accountID ?? -1],
to: reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails,
@@ -368,7 +308,6 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
const transaction = {
...transactionItem,
- action: getAction(data, key),
from,
to,
formattedFrom,
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 25d0d93c99c7..aa46d28e6899 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -27,7 +27,6 @@ import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
import type {Attendee} from '@src/types/onyx/IOU';
-import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -716,7 +715,7 @@ function hasBrokenConnectionViolation(transactionID: string): boolean {
/**
* Check if user should see broken connection violation warning.
*/
-function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean {
+function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry, policy: OnyxEntry): boolean {
return (
hasBrokenConnectionViolation(transactionID) &&
(!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isOpenExpenseReport(report) || (ReportUtils.isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy)))
diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts
index e06382edffdc..eb03d8b6def9 100644
--- a/src/libs/WorkspacesSettingsUtils.ts
+++ b/src/libs/WorkspacesSettingsUtils.ts
@@ -64,7 +64,18 @@ const getBrickRoadForPolicy = (report: Report, altReportActions?: OnyxCollection
const oneTransactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions);
let doesReportContainErrors = Object.keys(reportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined;
- if (oneTransactionThreadReportID) {
+ if (!doesReportContainErrors) {
+ const parentReportActions = (altReportActions ?? allReportActions)?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`];
+ const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1'];
+ const shouldDisplayViolations = ReportUtils.shouldDisplayTransactionThreadViolations(report, allTransactionViolations, parentReportAction);
+ const shouldDisplayReportViolations = ReportUtils.isReportOwner(report) && ReportUtils.hasReportViolations(report.reportID);
+ const hasViolations = shouldDisplayViolations || shouldDisplayReportViolations;
+ if (hasViolations) {
+ doesReportContainErrors = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
+ }
+ }
+
+ if (oneTransactionThreadReportID && !doesReportContainErrors) {
const oneTransactionThreadReport = ReportUtils.getReport(oneTransactionThreadReportID);
if (
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 5a594a19e15a..f778405ee6e8 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -179,7 +179,7 @@ function setSidebarLoaded() {
}
Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true);
- Performance.markStart(CONST.TIMING.REPORT_INITIAL_RENDER);
+ Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
}
let appState: AppStateStatus;
diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts
index 18779a284278..0c0ea1ada08a 100644
--- a/src/libs/actions/CompanyCards.ts
+++ b/src/libs/actions/CompanyCards.ts
@@ -240,6 +240,27 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri
const onyxData: OnyxData = {
optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ value: {
+ [cardID]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: {
+ [cardID]: {
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ },
+ },
+ },
+ ],
+
+ successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
@@ -262,7 +283,7 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri
key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
value: {
[cardID]: {
- ...card,
+ pendingAction: null,
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
@@ -272,7 +293,7 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
- ...card,
+ pendingAction: null,
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
@@ -493,25 +514,23 @@ function updateCompanyCardName(workspaceAccountID: number, cardID: string, newCa
API.write(WRITE_COMMANDS.UPDATE_COMPANY_CARD_NAME, parameters, {optimisticData, finallyData, failureData});
}
-function setCompanyCardExportAccount(workspaceAccountID: number, cardID: string, accountKey: string, newAccount: string, bankName: string) {
+function setCompanyCardExportAccount(policyID: string, workspaceAccountID: number, cardID: string, accountKey: string, newAccount: string, bank: string) {
const authToken = NetworkStore.getAuthToken();
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bank}`,
value: {
[cardID]: {
nameValuePairs: {
pendingFields: {
- exportAccountDetails: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ [accountKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
errorFields: {
- exportAccountDetails: null,
- },
- exportAccountDetails: {
- [accountKey]: newAccount,
+ [accountKey]: null,
},
+ [accountKey]: newAccount,
},
},
},
@@ -521,12 +540,12 @@ function setCompanyCardExportAccount(workspaceAccountID: number, cardID: string,
const finallyData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bank}`,
value: {
[cardID]: {
nameValuePairs: {
pendingFields: {
- exportAccountDetails: null,
+ [accountKey]: null,
},
},
},
@@ -536,15 +555,15 @@ function setCompanyCardExportAccount(workspaceAccountID: number, cardID: string,
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`,
+ key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bank}`,
value: {
[cardID]: {
nameValuePairs: {
pendingFields: {
- exportAccountDetails: null,
+ [accountKey]: newAccount,
},
errorFields: {
- exportAccountDetails: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ [accountKey]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
},
@@ -555,7 +574,7 @@ function setCompanyCardExportAccount(workspaceAccountID: number, cardID: string,
const parameters: SetCompanyCardExportAccountParams = {
authToken,
cardID: Number(cardID),
- exportAccountDetails: {[accountKey]: newAccount},
+ exportAccountDetails: JSON.stringify({[accountKey]: newAccount, [`${accountKey}_policy_id`]: policyID}),
};
API.write(WRITE_COMMANDS.SET_CARD_EXPORT_ACCOUNT, parameters, {optimisticData, finallyData, failureData});
@@ -577,6 +596,15 @@ function clearCompanyCardErrorField(workspaceAccountID: number, cardID: string,
},
},
});
+ Onyx.merge(ONYXKEYS.CARD_LIST, {
+ [cardID]: {
+ nameValuePairs: {
+ errorFields: {
+ [fieldName]: null,
+ },
+ },
+ },
+ });
}
function openPolicyCompanyCardsPage(policyID: string, workspaceAccountID: number) {
diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts
index e294a57e6c5f..ce34d838f4bb 100644
--- a/src/libs/actions/Delegate.ts
+++ b/src/libs/actions/Delegate.ts
@@ -1,3 +1,4 @@
+import {NativeModules} from 'react-native';
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
@@ -6,6 +7,7 @@ import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
import * as NetworkStore from '@libs/Network/NetworkStore';
+import {getCurrentUserEmail} from '@libs/Network/NetworkStore';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -107,6 +109,8 @@ function connect(email: string) {
NetworkStore.setAuthToken(response?.restrictedToken ?? null);
confirmReadyToOpenApp();
openApp();
+
+ NativeModules.HybridAppModule.switchAccount(email);
});
})
.catch((error) => {
@@ -170,6 +174,8 @@ function disconnect() {
NetworkStore.setAuthToken(response?.authToken ?? null);
confirmReadyToOpenApp();
openApp();
+
+ NativeModules.HybridAppModule.switchAccount(getCurrentUserEmail() ?? '');
});
})
.catch((error) => {
@@ -607,4 +613,5 @@ export {
clearDelegateRolePendingAction,
updateDelegateRole,
removeDelegate,
+ KEYS_TO_PRESERVE_DELEGATE_ACCESS,
};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index bc6b2c337041..5ec2e81b8c01 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -65,7 +65,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type ReportAction from '@src/types/onyx/ReportAction';
import type {OnyxData} from '@src/types/onyx/Request';
-import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
+import type {SearchTransaction} from '@src/types/onyx/SearchResults';
import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CachedPDFPaths from './CachedPDFPaths';
@@ -6737,11 +6737,7 @@ function sendMoneyWithWallet(report: OnyxEntry, amount: number
Report.notifyNewAction(params.chatReportID, managerID);
}
-function canApproveIOU(
- iouReport: OnyxTypes.OnyxInputOrEntry | SearchReport,
- policy: OnyxTypes.OnyxInputOrEntry | SearchPolicy,
- chatReportRNVP?: OnyxTypes.ReportNameValuePairs,
-) {
+function canApproveIOU(iouReport: OnyxTypes.OnyxInputOrEntry, policy: OnyxTypes.OnyxInputOrEntry) {
// Only expense reports can be approved
const isPaidGroupPolicy = policy && PolicyUtils.isPaidGroupPolicy(policy);
if (!isPaidGroupPolicy) {
@@ -6758,7 +6754,7 @@ function canApproveIOU(
const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(iouReport);
const isApproved = ReportUtils.isReportApproved(iouReport);
const iouSettled = ReportUtils.isSettled(iouReport?.reportID);
- const reportNameValuePairs = chatReportRNVP ?? ReportUtils.getReportNameValuePairs(iouReport?.reportID);
+ const reportNameValuePairs = ReportUtils.getReportNameValuePairs(iouReport?.reportID);
const isArchivedReport = ReportUtils.isArchivedRoom(iouReport, reportNameValuePairs);
let isTransactionBeingScanned = false;
const reportTransactions = TransactionUtils.getAllReportTransactions(iouReport?.reportID);
@@ -6776,18 +6772,16 @@ function canApproveIOU(
}
function canIOUBePaid(
- iouReport: OnyxTypes.OnyxInputOrEntry | SearchReport,
- chatReport: OnyxTypes.OnyxInputOrEntry | SearchReport,
- policy: OnyxTypes.OnyxInputOrEntry | SearchPolicy,
- transactions?: OnyxTypes.Transaction[] | SearchTransaction[],
+ iouReport: OnyxTypes.OnyxInputOrEntry,
+ chatReport: OnyxTypes.OnyxInputOrEntry,
+ policy: OnyxTypes.OnyxInputOrEntry,
+ transactions?: OnyxTypes.Transaction[],
onlyShowPayElsewhere = false,
- chatReportRNVP?: OnyxTypes.ReportNameValuePairs,
- invoiceReceiverPolicy?: SearchPolicy,
) {
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport);
- const reportNameValuePairs = chatReportRNVP ?? ReportUtils.getReportNameValuePairs(chatReport?.reportID);
+ const reportNameValuePairs = ReportUtils.getReportNameValuePairs(chatReport?.reportID);
const isChatReportArchived = ReportUtils.isArchivedRoom(chatReport, reportNameValuePairs);
- const iouSettled = ReportUtils.isSettled(iouReport);
+ const iouSettled = ReportUtils.isSettled(iouReport?.reportID);
if (isEmptyObject(iouReport)) {
return false;
@@ -6809,7 +6803,7 @@ function canIOUBePaid(
if (chatReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
return chatReport?.invoiceReceiver?.accountID === userAccountID;
}
- return (invoiceReceiverPolicy ?? PolicyUtils.getPolicy(chatReport?.invoiceReceiver?.policyID))?.role === CONST.POLICY.ROLE.ADMIN;
+ return PolicyUtils.getPolicy(chatReport?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN;
}
const isPayer = ReportUtils.isPayer(
@@ -6819,7 +6813,6 @@ function canIOUBePaid(
},
iouReport,
onlyShowPayElsewhere,
- policy,
);
const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport);
@@ -7833,6 +7826,9 @@ function putOnHold(transactionID: string, comment: string, reportID: string, sea
},
{optimisticData, successData, failureData},
);
+
+ const currentReportID = ReportUtils.getDisplayedReportID(reportID);
+ Report.notifyNewAction(currentReportID, userAccountID);
}
/**
@@ -7932,6 +7928,9 @@ function unholdRequest(transactionID: string, reportID: string, searchHash?: num
},
{optimisticData, successData, failureData},
);
+
+ const currentReportID = ReportUtils.getDisplayedReportID(reportID);
+ Report.notifyNewAction(currentReportID, userAccountID);
}
// eslint-disable-next-line rulesdir/no-negated-variables
function navigateToStartStepIfScanFileCannotBeRead(
diff --git a/src/libs/actions/OnyxUpdateManager/index.ts b/src/libs/actions/OnyxUpdateManager/index.ts
index db00a55aa25b..085e05b0a449 100644
--- a/src/libs/actions/OnyxUpdateManager/index.ts
+++ b/src/libs/actions/OnyxUpdateManager/index.ts
@@ -1,11 +1,13 @@
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import Log from '@libs/Log';
+import * as NetworkStore from '@libs/Network/NetworkStore';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
import * as App from '@userActions/App';
+import updateSessionAuthTokens from '@userActions/Session/updateSessionAuthTokens';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {OnyxUpdatesFromServer} from '@src/types/onyx';
+import type {OnyxUpdatesFromServer, Session} from '@src/types/onyx';
import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer';
import * as OnyxUpdateManagerUtils from './utils';
import * as DeferredOnyxUpdates from './utils/DeferredOnyxUpdates';
@@ -90,6 +92,10 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry): void {
+ // Consolidate all of the given Onyx updates
+ const onyxUpdates: OnyxUpdate[] = [];
+ onyxUpdatesFromServer?.updates?.forEach((updateEvent) => onyxUpdates.push(...updateEvent.data));
+ onyxUpdates.push(...(onyxUpdatesFromServer?.response?.onyxData ?? []));
+
+ // Find any session updates
+ const sessionUpdates = onyxUpdates?.filter((onyxUpdate) => onyxUpdate.key === ONYXKEYS.SESSION);
+
+ // If any of the updates changes the authToken, let's update it now
+ sessionUpdates?.forEach((sessionUpdate) => {
+ const session = (sessionUpdate.value ?? {}) as Session;
+ const newAuthToken = session.authToken ?? '';
+ if (!newAuthToken) {
+ return;
+ }
+
+ Log.info('[OnyxUpdateManager] Found an authToken update while handling an Onyx update gap. Updating the authToken.');
+ updateSessionAuthTokens(newAuthToken);
+ NetworkStore.setAuthToken(newAuthToken);
+ });
+}
+
export default () => {
console.debug('[OnyxUpdateManager] Listening for updates from the server');
Onyx.connect({
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 690a55daa1d0..f514a9b27158 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -199,6 +199,12 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedCurrencies = val ?? []),
});
+let activePolicyID: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
+ callback: (value) => (activePolicyID = value),
+});
+
/**
* Stores in Onyx the policy ID of the last workspace that was accessed by the user
*/
@@ -224,15 +230,6 @@ function getPolicy(policyID: string | undefined): OnyxEntry {
return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
}
-/**
- * Returns a primary policy for the user
- */
-function getPrimaryPolicy(activePolicyID: OnyxEntry, currentUserLogin: string | undefined): Policy | undefined {
- const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies, currentUserLogin);
- const primaryPolicy: Policy | null | undefined = activeAdminWorkspaces.find((policy) => policy.id === activePolicyID);
- return primaryPolicy ?? activeAdminWorkspaces.at(0);
-}
-
/** Check if the policy has invoicing company details */
function hasInvoicingDetails(policy: OnyxEntry): boolean {
return !!policy?.invoice?.companyName && !!policy?.invoice?.companyWebsite;
@@ -241,8 +238,8 @@ function hasInvoicingDetails(policy: OnyxEntry): boolean {
/**
* Returns a primary invoice workspace for the user
*/
-function getInvoicePrimaryWorkspace(activePolicyID: OnyxEntry, currentUserLogin: string | undefined): Policy | undefined {
- if (PolicyUtils.canSendInvoiceFromWorkspace(activePolicyID)) {
+function getInvoicePrimaryWorkspace(currentUserLogin: string | undefined): Policy | undefined {
+ if (PolicyUtils.canSendInvoiceFromWorkspace(activePolicyID ?? '-1')) {
return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID ?? '-1'}`];
}
const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies, currentUserLogin);
@@ -1621,6 +1618,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
const optimisticCategoriesData = buildOptimisticPolicyCategories(policyID, CONST.POLICY.DEFAULT_CATEGORIES);
+ const shouldSetCreatedWorkspaceAsActivePolicy = !!activePolicyID && allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]?.type === CONST.POLICY.TYPE.PERSONAL;
+
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.SET,
@@ -1706,8 +1705,21 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${expenseChatReportID}`,
value: null,
},
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${adminsChatReportID}`,
+ value: null,
+ },
];
+ if (shouldSetCreatedWorkspaceAsActivePolicy) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
+ value: policyID,
+ });
+ }
+
const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -1796,6 +1808,14 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
},
];
+ if (shouldSetCreatedWorkspaceAsActivePolicy) {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
+ value: activePolicyID ?? '',
+ });
+ }
+
if (optimisticCategoriesData.optimisticData) {
optimisticData.push(...optimisticCategoriesData.optimisticData);
}
@@ -4597,7 +4617,6 @@ export {
setPolicyCustomTaxName,
clearPolicyErrorField,
isCurrencySupportedForDirectReimbursement,
- getPrimaryPolicy,
getInvoicePrimaryWorkspace,
createDraftWorkspace,
savePreferredExportMethod,
diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts
index 83bc6652cb39..bc19ff12aea1 100644
--- a/src/libs/actions/QueuedOnyxUpdates.ts
+++ b/src/libs/actions/QueuedOnyxUpdates.ts
@@ -19,4 +19,8 @@ function flushQueue(): Promise {
});
}
-export {queueOnyxUpdates, flushQueue};
+function isEmpty() {
+ return queuedOnyxUpdates.length === 0;
+}
+
+export {queueOnyxUpdates, flushQueue, isEmpty};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index e3eb839fe1d8..adaab2cfaa79 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -994,7 +994,10 @@ function openReport(
// eslint-disable-next-line rulesdir/no-multiple-api-calls
API.paginate(CONST.API_REQUEST_TYPE.WRITE, WRITE_COMMANDS.OPEN_REPORT, parameters, {optimisticData, successData, failureData}, paginationConfig, {
checkAndFixConflictingRequest: (persistedRequests) =>
- resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.OPEN_REPORT && request.data?.reportID === reportID),
+ resolveDuplicationConflictAction(
+ persistedRequests,
+ (request) => request.command === WRITE_COMMANDS.OPEN_REPORT && request.data?.reportID === reportID && request.data?.emailList === parameters.emailList,
+ ),
});
}
}
diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts
index ecd0fc26785a..caef871571ed 100644
--- a/src/libs/actions/Search.ts
+++ b/src/libs/actions/Search.ts
@@ -1,21 +1,18 @@
import Onyx from 'react-native-onyx';
-import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
+import type {OnyxUpdate} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {FormOnyxValues} from '@components/Form/types';
-import type {PaymentData, SearchQueryJSON} from '@components/Search/types';
-import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
+import type {SearchQueryJSON} from '@components/Search/types';
import * as API from '@libs/API';
import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
import * as ApiUtils from '@libs/ApiUtils';
import fileDownload from '@libs/fileDownload';
import enhanceParameters from '@libs/Network/enhanceParameters';
-import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm';
-import type {LastPaymentMethod} from '@src/types/onyx';
-import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
+import type {SearchTransaction} from '@src/types/onyx/SearchResults';
import * as Report from './Report';
let currentUserEmail: string;
@@ -26,32 +23,6 @@ Onyx.connect({
},
});
-let lastPaymentMethod: OnyxEntry;
-Onyx.connect({
- key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
- callback: (val) => {
- lastPaymentMethod = val;
- },
-});
-
-function handleActionButtonPress(hash: number, item: TransactionListItemType | ReportListItemType, goToItem: () => void) {
- // The transactionID is needed to handle actions taken on `status:all` where transactions on single expense reports can be approved/paid.
- // We need the transactionID to display the loading indicator for that list item's action.
- const transactionID = isTransactionListItemType(item) ? item.transactionID : undefined;
-
- switch (item.action) {
- case CONST.SEARCH.ACTION_TYPES.PAY: {
- const lastPolicyPaymentMethod = item.policyID ? (lastPaymentMethod?.[item.policyID] as ValueOf) : null;
- const amount = isReportListItemType(item) ? item.total ?? 0 : item.formattedTotal;
- return lastPolicyPaymentMethod ? payMoneyRequestOnSearch(hash, [{reportID: item.reportID, amount, paymentType: lastPolicyPaymentMethod}], transactionID) : goToItem();
- }
- case CONST.SEARCH.ACTION_TYPES.APPROVE:
- return approveMoneyRequestOnSearch(hash, [item.reportID], transactionID);
- default:
- return goToItem();
- }
-}
-
function getOnyxLoadingData(hash: number): {optimisticData: OnyxUpdate[]; finallyData: OnyxUpdate[]} {
const optimisticData: OnyxUpdate[] = [
{
@@ -193,42 +164,20 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com
API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData});
}
-function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionID?: string) {
- const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
- value: {
- data: transactionID
- ? {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {isActionLoading: isLoading}}
- : (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {isActionLoading: isLoading}])) as Partial),
- },
- },
- ];
-
- const optimisticData: OnyxUpdate[] = createActionLoadingData(true);
- const finallyData: OnyxUpdate[] = createActionLoadingData(false);
+// this function will be used once https://github.com/Expensify/App/pull/51445 is merged
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function approveMoneyRequestOnSearch(hash: number, reportIDList: string[]) {
+ const {optimisticData, finallyData} = getOnyxLoadingData(hash);
API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData});
}
-function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], transactionID?: string) {
- const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`,
- value: {
- data: transactionID
- ? {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {isActionLoading: isLoading}}
- : (Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, {isActionLoading: isLoading}])) as Partial),
- },
- },
- ];
-
- const optimisticData: OnyxUpdate[] = createActionLoadingData(true);
- const finallyData: OnyxUpdate[] = createActionLoadingData(false);
+// this function will be used once https://github.com/Expensify/App/pull/51445 is merged
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function payMoneyRequestOnSearch(hash: number, paymentType: string, reportsAndAmounts: string) {
+ const {optimisticData, finallyData} = getOnyxLoadingData(hash);
- API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, paymentData: JSON.stringify(paymentData)}, {optimisticData, finallyData});
+ API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, paymentType, reportsAndAmounts}, {optimisticData, finallyData});
}
function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) {
@@ -312,7 +261,4 @@ export {
deleteSavedSearch,
dismissSavedSearchRenameTooltip,
showSavedSearchRenameTooltip,
- payMoneyRequestOnSearch,
- approveMoneyRequestOnSearch,
- handleActionButtonPress,
};
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index d75c5064f93a..eda761b9637b 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -41,6 +41,7 @@ import Timers from '@libs/Timers';
import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import {KEYS_TO_PRESERVE, openApp} from '@userActions/App';
import * as App from '@userActions/App';
+import {KEYS_TO_PRESERVE_DELEGATE_ACCESS} from '@userActions/Delegate';
import * as Device from '@userActions/Device';
import * as PriorityMode from '@userActions/PriorityMode';
import redirectToSignIn from '@userActions/SignInRedirect';
@@ -483,13 +484,24 @@ function signUpUser() {
function signInAfterTransitionFromOldDot(transitionURL: string) {
const [route, queryParams] = transitionURL.split('?');
- const {email, authToken, encryptedAuthToken, accountID, autoGeneratedLogin, autoGeneratedPassword, clearOnyxOnStart, completedHybridAppOnboarding, isSingleNewDotEntry, primaryLogin} =
- Object.fromEntries(
- queryParams.split('&').map((param) => {
- const [key, value] = param.split('=');
- return [key, value];
- }),
- );
+ const {
+ email,
+ authToken,
+ encryptedAuthToken,
+ accountID,
+ autoGeneratedLogin,
+ autoGeneratedPassword,
+ clearOnyxOnStart,
+ completedHybridAppOnboarding,
+ isSingleNewDotEntry,
+ primaryLogin,
+ shouldRemoveDelegatedAccess,
+ } = Object.fromEntries(
+ queryParams.split('&').map((param) => {
+ const [key, value] = param.split('=');
+ return [key, value];
+ }),
+ );
const clearOnyxForNewAccount = () => {
if (clearOnyxOnStart !== 'true') {
@@ -501,6 +513,12 @@ function signInAfterTransitionFromOldDot(transitionURL: string) {
const setSessionDataAndOpenApp = new Promise((resolve) => {
clearOnyxForNewAccount()
+ .then(() => {
+ if (!shouldRemoveDelegatedAccess) {
+ return;
+ }
+ return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS);
+ })
.then(() =>
Onyx.multiSet({
[ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index eaccbb8497ac..f3b8b1a15c28 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -555,6 +555,16 @@ function validateLogin(accountID: number, validateCode: string) {
Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true});
const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ isLoading: true,
+ },
+ },
+ ];
+
+ const finallyData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.ACCOUNT,
@@ -566,7 +576,7 @@ function validateLogin(accountID: number, validateCode: string) {
const parameters: ValidateLoginParams = {accountID, validateCode};
- API.write(WRITE_COMMANDS.VALIDATE_LOGIN, parameters, {optimisticData});
+ API.write(WRITE_COMMANDS.VALIDATE_LOGIN, parameters, {optimisticData, finallyData});
Navigation.navigate(ROUTES.HOME);
}
diff --git a/src/libs/actions/Welcome/OnboardingFlow.ts b/src/libs/actions/Welcome/OnboardingFlow.ts
index 9b7dfc894b6a..5a1b4fc0474a 100644
--- a/src/libs/actions/Welcome/OnboardingFlow.ts
+++ b/src/libs/actions/Welcome/OnboardingFlow.ts
@@ -57,7 +57,8 @@ function getOnboardingInitialPath(): string {
if (isVsb) {
Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
- return `/${ROUTES.ONBOARDING_EMPLOYEES.route}`;
+ Onyx.set(ONYXKEYS.ONBOARDING_COMPANY_SIZE, CONST.ONBOARDING_COMPANY_SIZE.MICRO);
+ return `/${ROUTES.ONBOARDING_ACCOUNTING.route}`;
}
const isIndividual = onboardingValues.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.INDIVIDUAL;
if (isIndividual) {
diff --git a/src/pages/EditReportFieldDropdown.tsx b/src/pages/EditReportFieldDropdown.tsx
index a6bccdf3fa12..e8364d7d1f37 100644
--- a/src/pages/EditReportFieldDropdown.tsx
+++ b/src/pages/EditReportFieldDropdown.tsx
@@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import localeCompare from '@libs/LocaleCompare';
import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportFieldOptionsListUtils from '@libs/ReportFieldOptionsListUtils';
import ONYXKEYS from '@src/ONYXKEYS';
type EditReportFieldDropdownPageComponentProps = {
@@ -58,7 +59,7 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio
const [sections, headerMessage] = useMemo(() => {
const validFieldOptions = fieldOptions?.filter((option) => !!option)?.sort(localeCompare);
- const {policyReportFieldOptions} = OptionsListUtils.getFilteredOptions({
+ const policyReportFieldOptions = ReportFieldOptionsListUtils.getReportFieldOptionsSection({
searchValue: debouncedSearchValue,
selectedOptions: [
{
@@ -67,21 +68,17 @@ function EditReportFieldDropdownPage({onSubmit, fieldKey, fieldValue, fieldOptio
text: fieldValue,
},
],
-
- includeP2P: false,
- canInviteUser: false,
- includePolicyReportFieldOptions: true,
- policyReportFieldOptions: validFieldOptions,
- recentlyUsedPolicyReportFieldOptions: recentlyUsedOptions,
+ options: validFieldOptions,
+ recentlyUsedOptions,
});
- const policyReportFieldData = policyReportFieldOptions?.[0]?.data ?? [];
+ const policyReportFieldData = policyReportFieldOptions.at(0)?.data ?? [];
const header = OptionsListUtils.getHeaderMessageForNonUserList(policyReportFieldData.length > 0, debouncedSearchValue);
return [policyReportFieldOptions, header];
}, [recentlyUsedOptions, debouncedSearchValue, fieldValue, fieldOptions]);
- const selectedOptionKey = useMemo(() => (sections?.[0]?.data ?? []).filter((option) => option.searchText === fieldValue)?.at(0)?.keyForList, [sections, fieldValue]);
+ const selectedOptionKey = useMemo(() => (sections.at(0)?.data ?? []).filter((option) => option.searchText === fieldValue)?.at(0)?.keyForList, [sections, fieldValue]);
return (
(undefined);
const [error, setError] = useState('');
+ const isVsb = onboardingValues && 'signupQualifier' in onboardingValues && onboardingValues.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB;
+
+ // If the signupQualifier is VSB, the company size step is skip.
+ // So we need to create the new workspace in the accounting step
+ useEffect(() => {
+ if (!isVsb || !!onboardingPolicyID) {
+ return;
+ }
+
+ const {adminsChatReportID, policyID} = Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
+ Welcome.setOnboardingAdminsChatReportID(adminsChatReportID);
+ Welcome.setOnboardingPolicyID(policyID);
+ }, [isVsb, onboardingPolicyID]);
const accountingOptions: OnboardingListItem[] = useMemo(() => {
const policyAccountingOptions = Object.values(CONST.POLICY.CONNECTIONS.NAME)
@@ -147,10 +163,11 @@ function BaseOnboardingAccounting({shouldUseNativeStyles, route}: BaseOnboarding
onboardingCompanySize,
userReportedIntegration,
);
-
- Welcome.setOnboardingAdminsChatReportID();
- Welcome.setOnboardingPolicyID();
-
+ // Avoid creating new WS because onboardingPolicyID is cleared before unmounting
+ InteractionManager.runAfterInteractions(() => {
+ Welcome.setOnboardingAdminsChatReportID();
+ Welcome.setOnboardingPolicyID();
+ });
navigateAfterOnboarding(isSmallScreenWidth, shouldUseNarrowLayout, canUseDefaultRooms, onboardingPolicyID, activeWorkspaceID, route.params?.backTo);
}}
pressOnEnter
diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx
index 405e1dd688f2..1c91023f2d6f 100755
--- a/src/pages/ReportParticipantsPage.tsx
+++ b/src/pages/ReportParticipantsPage.tsx
@@ -91,7 +91,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
});
// Include the search bar when there are 8 or more active members in the selection list
- const shouldShowTextInput = activeParticipants.length >= CONST.SHOULD_SHOW_MEMBERS_SEARCH_INPUT_BREAKPOINT;
+ const shouldShowTextInput = activeParticipants.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
useEffect(() => {
if (!isFocused) {
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 6a89eca6f778..37b96514cb63 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -183,7 +183,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
// When offline, we want to include the pending members with delete action as they are displayed in the list as well
return !pendingMember || isOffline || pendingMember.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
});
- return activeParticipants.length >= CONST.SHOULD_SHOW_MEMBERS_SEARCH_INPUT_BREAKPOINT;
+ return activeParticipants.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
}, [participants, personalDetails, isOffline, report]);
useEffect(() => {
diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx
index d4d59638b592..e418c5d5df59 100644
--- a/src/pages/WorkspaceSwitcherPage/index.tsx
+++ b/src/pages/WorkspaceSwitcherPage/index.tsx
@@ -188,7 +188,7 @@ function WorkspaceSwitcherPage() {
ListItem={UserListItem}
sections={sections}
onSelectRow={selectPolicy}
- textInputLabel={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? translate('common.search') : undefined}
+ textInputLabel={usersWorkspaces.length >= CONST.STANDARD_LIST_ITEM_LIMIT ? translate('common.search') : undefined}
textInputValue={searchTerm}
onChangeText={setSearchTerm}
headerMessage={headerMessage}
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 4c3ed5c705a5..62fd04f7572a 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -29,11 +29,9 @@ import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
-import Timing from '@libs/actions/Timing';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import clearReportNotifications from '@libs/Notification/clearReportNotifications';
-import Performance from '@libs/Performance';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -230,11 +228,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
const [scrollPosition, setScrollPosition] = useState({});
const wasReportAccessibleRef = useRef(false);
- // eslint-disable-next-line react-compiler/react-compiler
- if (firstRenderRef.current) {
- Timing.start(CONST.TIMING.CHAT_RENDER);
- Performance.markStart(CONST.TIMING.CHAT_RENDER);
- }
+
const [isComposerFocus, setIsComposerFocus] = useState(false);
const shouldAdjustScrollView = useMemo(() => isComposerFocus && !modal?.willAlertModalBecomeVisible, [isComposerFocus, modal]);
const viewportOffsetTop = useViewportOffsetTop(shouldAdjustScrollView);
@@ -487,9 +481,6 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
useAppFocusEvent(clearNotifications);
useEffect(() => {
- Timing.end(CONST.TIMING.CHAT_RENDER);
- Performance.markEnd(CONST.TIMING.CHAT_RENDER);
-
const interactionTask = InteractionManager.runAfterInteractions(() => {
ComposerActions.setShouldShowComposeInput(true);
});
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
index 23b059f2fda2..4285916b593d 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx
@@ -43,6 +43,7 @@ import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator';
import variables from '@styles/variables';
import * as EmojiPickerActions from '@userActions/EmojiPickerAction';
import * as Report from '@userActions/Report';
+import Timing from '@userActions/Timing';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -273,7 +274,8 @@ function ReportActionCompose({
Report.addAttachment(reportID, attachmentFileRef.current, newCommentTrimmed);
attachmentFileRef.current = null;
} else {
- Performance.markStart(CONST.TIMING.MESSAGE_SENT, {message: newCommentTrimmed});
+ Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed});
+ Timing.start(CONST.TIMING.SEND_MESSAGE);
onSubmit(newCommentTrimmed);
}
},
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index 2953036f6af7..559d635f73fe 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -52,6 +52,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import SelectionScraper from '@libs/SelectionScraper';
import shouldRenderAddPaymentCard from '@libs/shouldRenderAppPaymentCard';
+import {doesUserHavePaymentCardAdded} from '@libs/SubscriptionUtils';
import {ReactionListContext} from '@pages/home/ReportScreenContext';
import * as BankAccounts from '@userActions/BankAccounts';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
@@ -397,7 +398,7 @@ function ReportActionItem({
const mentionReportContextValue = useMemo(() => ({currentReportID: report?.reportID ?? '-1'}), [report?.reportID]);
const actionableItemButtons: ActionableItem[] = useMemo(() => {
- if (ReportActionsUtils.isActionableAddPaymentCard(action) && shouldRenderAddPaymentCard()) {
+ if (ReportActionsUtils.isActionableAddPaymentCard(action) && !doesUserHavePaymentCardAdded() && shouldRenderAddPaymentCard()) {
return [
{
text: 'subscription.cardSection.addCardButton',
diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx
index 94a8592d9607..13072a653749 100644
--- a/src/pages/home/report/ReportActionItemThread.tsx
+++ b/src/pages/home/report/ReportActionItemThread.tsx
@@ -7,6 +7,7 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Timing from '@libs/actions/Timing';
+import Performance from '@libs/Performance';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import type {Icon} from '@src/types/onyx/OnyxCommon';
@@ -45,8 +46,9 @@ function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childR
{
- Report.navigateToAndOpenChildReport(childReportID);
+ Performance.markStart(CONST.TIMING.OPEN_REPORT_THREAD);
Timing.start(CONST.TIMING.OPEN_REPORT_THREAD);
+ Report.navigateToAndOpenChildReport(childReportID);
}}
role={CONST.ROLE.BUTTON}
accessibilityLabel={`${numberOfReplies} ${replyText}`}
diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index 8896611905ca..31c0bc0fa752 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -6,7 +6,6 @@ import {InteractionManager} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
-import useInitialValue from '@hooks/useInitialValue';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -285,7 +284,6 @@ function ReportActionsView({
const hasMoreCached = reportActions.length < combinedReportActions.length;
const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]);
const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]);
- const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0);
const hasNewestReportAction = reportActions.at(0)?.created === report.lastVisibleActionCreated || reportActions.at(0)?.created === transactionThreadReport?.lastVisibleActionCreated;
const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]);
@@ -427,18 +425,16 @@ function ReportActionsView({
}
didLayout.current = true;
- // Capture the init measurement only once not per each chat switch as the value gets overwritten
- if (!ReportActionsView.initMeasured) {
- Performance.markEnd(CONST.TIMING.OPEN_REPORT);
- Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER);
- ReportActionsView.initMeasured = true;
- } else {
- Performance.markEnd(CONST.TIMING.SWITCH_REPORT);
- }
- Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD);
+
+ Performance.markEnd(CONST.TIMING.OPEN_REPORT);
+ Timing.end(CONST.TIMING.OPEN_REPORT);
+
+ Performance.markEnd(CONST.TIMING.OPEN_REPORT_THREAD);
Timing.end(CONST.TIMING.OPEN_REPORT_THREAD);
+
+ Performance.markEnd(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
Timing.end(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW);
- }, [hasCachedActionOnFirstRender]);
+ }, []);
// Check if the first report action in the list is the one we're currently linked to
const isTheFirstReportActionIsLinked = newestReportAction?.reportActionID === reportActionID;
@@ -501,7 +497,6 @@ function ReportActionsView({
}
ReportActionsView.displayName = 'ReportActionsView';
-ReportActionsView.initMeasured = false;
function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean {
if (!lodashIsEqual(oldProps.reportActions, newProps.reportActions)) {
diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx
index d30d8e9aabc1..871d692b59a3 100644
--- a/src/pages/home/report/ReportAttachments.tsx
+++ b/src/pages/home/report/ReportAttachments.tsx
@@ -20,6 +20,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) {
const isAuthTokenRequired = route.params.isAuthTokenRequired;
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
+ const fileName = route.params?.fileName;
// In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource
const source = Number(route.params.source) || route.params.source;
@@ -48,6 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) {
onCarouselAttachmentChange={onCarouselAttachmentChange}
shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID}
isAuthTokenRequired={!!isAuthTokenRequired}
+ originalFileName={fileName ?? ''}
/>
);
}
diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx
index 530acc46233d..ab06a594a17f 100644
--- a/src/pages/home/report/comment/TextCommentFragment.tsx
+++ b/src/pages/home/report/comment/TextCommentFragment.tsx
@@ -14,6 +14,7 @@ import * as EmojiUtils from '@libs/EmojiUtils';
import Performance from '@libs/Performance';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import variables from '@styles/variables';
+import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage';
import type {Message} from '@src/types/onyx/ReportAction';
@@ -52,7 +53,8 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so
const {shouldUseNarrowLayout} = useResponsiveLayout();
useEffect(() => {
- Performance.markEnd(CONST.TIMING.MESSAGE_SENT, {message: text});
+ Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text});
+ Timing.end(CONST.TIMING.SEND_MESSAGE);
}, [text]);
// If the only difference between fragment.text and fragment.html is tags and emoji tag
diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.tsx
index e62f2525e70b..e2df0ff6f33a 100644
--- a/src/pages/home/sidebar/SidebarLinks.tsx
+++ b/src/pages/home/sidebar/SidebarLinks.tsx
@@ -17,9 +17,6 @@ import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
type SidebarLinksProps = {
- /** Toggles the navigation menu open and closed */
- onLinkClick: () => void;
-
/** Safe area insets required for mobile devices margins */
insets: EdgeInsets;
@@ -40,7 +37,7 @@ type SidebarLinksProps = {
activeWorkspaceID: string | undefined;
};
-function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) {
+function SidebarLinks({insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {updateLocale} = useLocalize();
@@ -75,9 +72,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
return;
}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID));
- onLinkClick();
},
- [shouldUseNarrowLayout, isActiveReport, onLinkClick],
+ [shouldUseNarrowLayout, isActiveReport],
);
const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT;
diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx
index 7dfbdbaf7299..931bfd6c0d66 100644
--- a/src/pages/home/sidebar/SidebarLinksData.tsx
+++ b/src/pages/home/sidebar/SidebarLinksData.tsx
@@ -24,14 +24,11 @@ type SidebarLinksDataOnyxProps = {
};
type SidebarLinksDataProps = SidebarLinksDataOnyxProps & {
- /** Toggles the navigation menu open and closed */
- onLinkClick: () => void;
-
/** Safe area insets required for mobile devices margins */
insets: EdgeInsets;
};
-function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMode = CONST.PRIORITY_MODE.DEFAULT}: SidebarLinksDataProps) {
+function SidebarLinksData({insets, isLoadingApp = true, priorityMode = CONST.PRIORITY_MODE.DEFAULT}: SidebarLinksDataProps) {
const isFocused = useIsFocused();
const styles = useThemeStyles();
const activeWorkspaceID = useActiveWorkspaceFromNavigationState();
@@ -63,7 +60,6 @@ function SidebarLinksData({insets, isLoadingApp = true, onLinkClick, priorityMod
>
({
initialValue: CONST.PRIORITY_MODE.DEFAULT,
},
})(
- /*
+ /*
While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs.
With that we can reduce app start up time by ~2s on heavy account.
More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534
*/
memo(
SidebarLinksData,
- (prevProps, nextProps) =>
- prevProps.isLoadingApp === nextProps.isLoadingApp &&
- prevProps.priorityMode === nextProps.priorityMode &&
- lodashIsEqual(prevProps.insets, nextProps.insets) &&
- prevProps.onLinkClick === nextProps.onLinkClick,
+ (prevProps, nextProps) => prevProps.isLoadingApp === nextProps.isLoadingApp && prevProps.priorityMode === nextProps.priorityMode && lodashIsEqual(prevProps.insets, nextProps.insets),
),
);
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
index e77f2000b85f..057189ae22c1 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx
@@ -12,18 +12,9 @@ import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator
import Navigation from '@libs/Navigation/Navigation';
import Performance from '@libs/Performance';
import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData';
-import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-/**
- * Function called when a pinned chat is selected.
- */
-const startTimer = () => {
- Timing.start(CONST.TIMING.SWITCH_REPORT);
- Performance.markStart(CONST.TIMING.SWITCH_REPORT);
-};
-
function BaseSidebarScreen() {
const styles = useThemeStyles();
const activeWorkspaceID = useActiveWorkspaceFromNavigationState();
@@ -33,7 +24,6 @@ function BaseSidebarScreen() {
useEffect(() => {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
- Timing.start(CONST.TIMING.SIDEBAR_LOADED);
}, []);
useEffect(() => {
@@ -63,10 +53,7 @@ function BaseSidebarScreen() {
shouldDisplaySearch={shouldDisplaySearch}
/>
-
+
>
)}
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
index 69d5407260a8..247bdbf105c6 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
@@ -35,7 +35,6 @@ import variables from '@styles/variables';
import * as App from '@userActions/App';
import * as IOU from '@userActions/IOU';
import * as Link from '@userActions/Link';
-import * as Policy from '@userActions/Policy/Policy';
import * as Report from '@userActions/Report';
import * as Task from '@userActions/Task';
import * as Welcome from '@userActions/Welcome';
@@ -198,6 +197,10 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
return !groupPolicies.some((policy) => !!policy?.isPolicyExpenseChatEnabled);
}, [allPolicies]);
+ const shouldShowNewWorkspaceButton = Object.values(allPolicies ?? {}).every(
+ (policy) => !PolicyUtils.shouldShowPolicy(policy as OnyxEntry, !!isOffline, session?.email),
+ );
+
const quickActionAvatars = useMemo(() => {
if (quickActionReport) {
const avatars = ReportUtils.getIcons(quickActionReport, personalDetails);
@@ -565,7 +568,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
},
]
: []),
- ...(!isLoading && !Policy.hasActiveChatEnabledPolicies(allPolicies)
+ ...(!isLoading && shouldShowNewWorkspaceButton
? [
{
displayInDefaultIconColor: true,
diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
index df8d7797b41f..4478951555ef 100644
--- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
+++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
@@ -241,7 +241,7 @@ function MoneyRequestParticipantsSelector({
];
if (iouType === CONST.IOU.TYPE.INVOICE) {
- const policyID = option.item && ReportUtils.isInvoiceRoom(option.item) ? option.policyID : Policy.getInvoicePrimaryWorkspace(activePolicyID, currentUserLogin)?.id;
+ const policyID = option.item && ReportUtils.isInvoiceRoom(option.item) ? option.policyID : Policy.getInvoicePrimaryWorkspace(currentUserLogin)?.id;
newParticipants.push({
policyID,
isSender: true,
diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx
index 200b6b55363a..3bd3c2aa7000 100644
--- a/src/pages/settings/Wallet/VerifyAccountPage.tsx
+++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx
@@ -21,6 +21,8 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) {
const loginData = loginList?.[contactMethod];
const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin');
const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated});
+ const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID ?? 0});
+
const [isValidateCodeActionModalVisible, setIsValidateCodeActionModalVisible] = useState(true);
const navigateBackTo = route?.params?.backTo;
@@ -28,10 +30,10 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) {
useEffect(() => () => User.clearUnvalidatedNewContactMethodAction(), []);
const handleSubmitForm = useCallback(
- (submitCode: string) => {
- User.validateSecondaryLogin(loginList, contactMethod ?? '', submitCode);
+ (validateCode: string) => {
+ User.validateLogin(accountID ?? 0, validateCode);
},
- [loginList, contactMethod],
+ [accountID],
);
const clearError = useCallback(() => {
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx
index e8f0d9e8315f..0f52f6a11d7c 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorModal.tsx
@@ -61,7 +61,7 @@ function NetSuiteCustomListSelectorModal({isVisible, currentCustomListValue, onC
},
],
headerMessage: isEmpty ? translate('common.noResultsFound') : '',
- showTextInput: customListData.length > CONST.NETSUITE_CONFIG.NETSUITE_CUSTOM_LIST_LIMIT,
+ showTextInput: customListData.length > CONST.STANDARD_LIST_ITEM_LIMIT,
};
}, [debouncedSearchValue, policy?.connections?.netsuite?.options?.data?.customLists, translate, currentCustomListValue]);
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
index 8555be0d3d83..ff8547952155 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
@@ -31,7 +31,9 @@ function QuickbooksDesktopExportPage({policy}: WithPolicyConnectionsProps) {
{
description: translate('workspace.accounting.preferredExporter'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_PREFERRED_EXPORTER.getRoute(policyID)),
- title: qbdConfig?.export?.exporter ?? policyOwner,
+ // We use the logical OR (||) here instead of ?? because `exporter` could be an empty string
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ title: qbdConfig?.export?.exporter || policyOwner,
subscribedSettings: [CONST.QUICKBOOKS_DESKTOP_CONFIG.EXPORTER],
},
{
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
index eef48ee04dcf..b571f67e8350 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
@@ -45,7 +45,9 @@ function QuickbooksDesktopPreferredExporterConfigurationPage({policy}: WithPolic
value: exporter.email,
text: exporter.email,
keyForList: exporter.email,
- isSelected: (currentExporter ?? policy?.owner) === exporter.email,
+ // We use the logical OR (||) here instead of ?? because `exporter` could be an empty string
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ isSelected: (currentExporter || policy?.owner) === exporter.email,
});
return options;
}, []),
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx
index c32cebd7ba18..a39a4401cb6e 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx
@@ -63,7 +63,7 @@ function WorkspaceCompanyCardAccountSelectCardPage({route}: WorkspaceCompanyCard
if (!exportMenuItem?.exportType) {
return;
}
- CompanyCards.setCompanyCardExportAccount(workspaceAccountID, cardID, exportMenuItem.exportType, value, bank);
+ CompanyCards.setCompanyCardExportAccount(policyID, workspaceAccountID, cardID, exportMenuItem.exportType, value, bank);
Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, bank));
},
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
index 4bdf5211a2c0..9fdfa7bec7b3 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx
@@ -130,12 +130,17 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_NAME.getRoute(policyID, cardID, bank))}
/>
- {!!exportMenuItem && (
+ {exportMenuItem?.shouldShowMenuItem ? (
CompanyCards.clearCompanyCardErrorField(workspaceAccountID, cardID, bank, 'exportAccountDetails')}
+ errors={exportMenuItem.exportType ? ErrorUtils.getLatestErrorField(card?.nameValuePairs ?? {}, exportMenuItem.exportType) : undefined}
+ onClose={() => {
+ if (!exportMenuItem.exportType) {
+ return;
+ }
+ CompanyCards.clearCompanyCardErrorField(workspaceAccountID, cardID, bank, exportMenuItem.exportType);
+ }}
>
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EXPORT.getRoute(policyID, cardID, bank))}
/>
- )}
+ ) : null}
) => {
const cardID = Object.keys(cardsList ?? {}).find((id) => cardsList?.[id].cardID === item.cardID);
const cardName = CardUtils.getCompanyCardNumber(cardsList?.cardList ?? {}, item.lastFourPAN);
+ const isCardDeleted = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
return (
{
if (!cardID || !item?.accountID) {
return;
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListRow.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListRow.tsx
index 91eddfd96936..2ce8c289c96e 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListRow.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListRow.tsx
@@ -27,7 +27,7 @@ function WorkspaceCompanyCardsListRow({cardholder, name, cardNumber}: WorkspaceC
;
@@ -38,8 +40,8 @@ function WorkspaceCompanyCardsSettingsFeedNamePage({
const {inputCallbackRef} = useAutoFocusInput();
const policy = usePolicy(policyID);
const workspaceAccountID = policy?.workspaceAccountID ?? -1;
- const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
- const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
+ const [lastSelectedFeed, lastSelectedFeedResult] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
+ const [cardFeeds, cardFeedsResult] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds);
const feedName = cardFeeds?.settings?.companyCardNicknames?.[selectedFeed] ?? translate('workspace.companyCards.feedName', {feedName: CardUtils.getCardFeedName(selectedFeed)});
@@ -62,6 +64,10 @@ function WorkspaceCompanyCardsSettingsFeedNamePage({
Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID));
};
+ if (isLoadingOnyxValue(cardFeedsResult) || isLoadingOnyxValue(lastSelectedFeedResult)) {
+ return ;
+ }
+
return (
void;
data: SelectorType[];
- exportType?: string;
+ exportType?: ValueOf;
+ shouldShowMenuItem?: boolean;
};
function getExportMenuItem(
@@ -32,10 +34,11 @@ function getExportMenuItem(
): ExportIntegration | undefined {
const currentConnectionName = PolicyUtils.getCurrentConnectionName(policy);
- const {nonReimbursableExpensesExportDestination} = policy?.connections?.quickbooksOnline?.config ?? {};
+ const {nonReimbursableExpensesExportDestination, nonReimbursableExpensesAccount} = policy?.connections?.quickbooksOnline?.config ?? {};
const {export: exportConfig} = policy?.connections?.intacct?.config ?? {};
const {export: exportConfiguration} = policy?.connections?.xero?.config ?? {};
const config = policy?.connections?.netsuite?.options.config;
+ const {bankAccounts} = policy?.connections?.xero?.data ?? {};
const {creditCards, bankAccounts: quickbooksOnlineBankAccounts} = policy?.connections?.quickbooksOnline?.data ?? {};
switch (connectionName) {
@@ -43,20 +46,22 @@ function getExportMenuItem(
const type = nonReimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${nonReimbursableExpensesExportDestination}`) : undefined;
const description = currentConnectionName && type ? translate('workspace.moreFeatures.companyCards.integrationExport', {integration: currentConnectionName, type}) : undefined;
let data: Account[];
+ let shouldShowMenuItem = true;
let title: string | undefined = '';
- let exportType: string | undefined = '';
+ let exportType: ValueOf | undefined;
switch (nonReimbursableExpensesExportDestination) {
case CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD:
data = creditCards ?? [];
- title = companyCard?.nameValuePairs?.exportAccountDetails?.quickbooks_desktop_export_account_credit;
+ title = companyCard?.nameValuePairs?.quickbooks_desktop_export_account_credit ?? nonReimbursableExpensesAccount?.name;
exportType = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_QUICKBOOKS_DESKTOP_EXPORT_ACCOUNT_CREDIT;
break;
case CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD:
data = quickbooksOnlineBankAccounts ?? [];
- title = companyCard?.nameValuePairs?.exportAccountDetails?.quickbooks_online_export_account_debit;
+ title = companyCard?.nameValuePairs?.quickbooks_online_export_account_debit ?? nonReimbursableExpensesAccount?.name;
exportType = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_QUICKBOOKS_ONLINE_EXPORT_ACCOUNT_DEBIT;
break;
default:
+ shouldShowMenuItem = false;
data = [];
}
@@ -64,6 +69,7 @@ function getExportMenuItem(
description,
title,
exportType,
+ shouldShowMenuItem,
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT.getRoute(policyID)),
data: data.map((card) => ({
value: card.name,
@@ -77,10 +83,12 @@ function getExportMenuItem(
const type = translate('workspace.xero.xeroBankAccount');
const description = currentConnectionName && type ? translate('workspace.moreFeatures.companyCards.integrationExport', {integration: currentConnectionName, type}) : undefined;
const exportType = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_XERO_EXPORT_BANK_ACCOUNT;
+ const selectedAccount = (bankAccounts ?? []).find((bank) => bank.id === exportConfiguration?.nonReimbursableAccount);
return {
description,
exportType,
- title: companyCard?.nameValuePairs?.exportAccountDetails?.xero_export_bank_account,
+ shouldShowMenuItem: true,
+ title: companyCard?.nameValuePairs?.xero_export_bank_account ?? selectedAccount?.name ?? bankAccounts?.[0]?.name ?? '',
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)),
data: getXeroBankAccounts(policy ?? undefined, exportConfiguration?.nonReimbursableAccount),
};
@@ -90,27 +98,29 @@ function getExportMenuItem(
? translate(`workspace.netsuite.exportDestination.values.${config.nonreimbursableExpensesExportDestination}.label`)
: undefined;
let title: string | undefined = '';
- let exportType: string | undefined = '';
+ let exportType: ValueOf | undefined;
+ let shouldShowMenuItem = true;
const description = currentConnectionName && type ? translate('workspace.moreFeatures.companyCards.integrationExport', {integration: currentConnectionName, type}) : undefined;
let data: SelectorType[];
switch (config?.nonreimbursableExpensesExportDestination) {
case CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL:
- title = companyCard?.nameValuePairs?.exportAccountDetails?.netsuite_export_vendor;
data = getNetSuiteVendorOptions(policy ?? undefined, config?.defaultVendor);
+ title = companyCard?.nameValuePairs?.netsuite_export_vendor ?? data.find((exportVendor) => exportVendor.isSelected)?.text;
exportType = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_NETSUITE_EXPORT_VENDOR;
break;
case CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY:
- title = companyCard?.nameValuePairs?.exportAccountDetails?.netsuite_export_payable_account;
data = getNetSuitePayableAccountOptions(policy ?? undefined, config?.payableAcct);
+ title = companyCard?.nameValuePairs?.netsuite_export_payable_account ?? data.find((exportPayable) => exportPayable.isSelected)?.text;
exportType = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_NETSUITE_EXPORT_ACCOUNT;
-
break;
default:
+ shouldShowMenuItem = false;
data = [];
}
return {
description,
title,
+ shouldShowMenuItem,
exportType,
data,
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)),
@@ -121,13 +131,16 @@ function getExportMenuItem(
const type = exportConfig?.nonReimbursable ? translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${exportConfig.nonReimbursable}`) : undefined;
const description = currentConnectionName && type ? translate('workspace.moreFeatures.companyCards.integrationExport', {integration: currentConnectionName, type}) : undefined;
const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy);
+ const data = isVendor ? getSageIntacctVendors(policy, activeDefaultVendor) : getSageIntacctCreditCards(policy, exportConfig?.nonReimbursableAccount);
+ const selectedAccount = data.find((account) => account.isSelected)?.text;
return {
description,
+ shouldShowMenuItem: true,
exportType: isVendor ? CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_INTACCT_EXPORT_VENDOR : CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_INTACCT_EXPORT_CHARGE_CARD,
- title: isVendor ? companyCard?.nameValuePairs?.exportAccountDetails?.intacct_export_vendor : companyCard?.nameValuePairs?.exportAccountDetails?.intacct_export_charge_card,
+ title: isVendor ? companyCard?.nameValuePairs?.intacct_export_vendor ?? selectedAccount : companyCard?.nameValuePairs?.intacct_export_charge_card ?? selectedAccount,
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)),
- data: isVendor ? getSageIntacctVendors(policy, activeDefaultVendor) : getSageIntacctCreditCards(policy, exportConfig?.nonReimbursableAccount),
+ data,
};
}
default:
diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
index e079fdee90a0..3e72beee812b 100644
--- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
+++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@@ -219,7 +219,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
return ;
}
- const shouldShowCardsSection = (!!policy?.areExpensifyCardsEnabled && !!paymentAccountID) ?? (!!policy?.areCompanyCardsEnabled && hasMultipleFeeds);
+ const shouldShowCardsSection = (!!policy?.areExpensifyCardsEnabled && !!paymentAccountID) || (!!policy?.areCompanyCardsEnabled && hasMultipleFeeds);
return (
- {(memberCards as MemberCard[]).map((memberCard) => (
-