diff --git a/Mobile-Expensify b/Mobile-Expensify index 7eac4848078b..1c83cec4504d 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 7eac4848078b4208ba36a72ce4ce73ec319ff6f8 +Subproject commit 1c83cec4504d7c129db6be88ac190bd017acb12e diff --git a/android/app/build.gradle b/android/app/build.gradle index 38049ccacc98..7d7f511abf75 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009023605 - versionName "9.2.36-5" + versionCode 1009023704 + versionName "9.2.37-4" // 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/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index bad97b5b7494..6926c3da2420 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -16,7 +16,8 @@ The navigation in the app is built on top of the `react-navigation` library. To - [Debugging](#debugging) - [Reading state when it changes](#reading-state-when-it-changes) - [Finding the code that calls the navigation function](#finding-the-code-that-calls-the-navigation-function) - - [Using `backTo` route param](#using-backto-route-param) + - [How to remove backTo from URL](#how-to-remove-backto-from-url) + - [Separating routes for each screen instance](#separating-routes-for-each-screen-instance) - [Generating state from a path](#generating-state-from-a-path) - [Setting the correct screen underneath RHP](#setting-the-correct-screen-underneath-rhp) - [Performance solutions](#performance-solutions) @@ -71,7 +72,7 @@ Navigation.navigate( ); // Navigation with forceReplace - replaces current screen instead of pushing a new one -Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: nextReportID, backTo}), {forceReplace: true}); +Navigation.navigate(ROUTES.SETTINGS_WALLET, {forceReplace: true}); // Navigation with a callback to handle anonymous users interceptAnonymousUser(() => { @@ -434,10 +435,13 @@ const handleStateChange = (state: NavigationState | undefined) => { The easiest way to find the piece of code from which the navigation method was called is to use a debugger and breakpoints. You should attach a breakpoint in the navigation method and check the call stack, this way you can easily find the navigation method that caused the problem. -## Using `backTo` route param +## How to remove backTo from URL > [!WARNING] -> **Deprecated**: The `backTo` parameter is deprecated and should not be used in new implementations. Most problems that `backTo` solved can be resolved by adding one or more routes for a single screen. If you don't know how to solve your problem, contact someone from the navigation team. +> **Deprecated**: The `backTo` parameter is deprecated and should not be used in new implementations. Most problems that `backTo` solved can be resolved by adding one or more routes for a single screen. If you don't know how to solve your problem, contact someone from the navigation team. Old documentation on how to use `backTo` can be found below. + +
+Using `backTo` route param When a particular screen can be opened from two or more different pages, we can use `backTo` route parameter to handle such case. @@ -494,6 +498,96 @@ function NewSettingsScreen({route}: NewSettingsScreenNavigationProps) { ); } +``` +
+ +### Separating routes for each screen instance + +Often, you will need to reuse a single screen across multiple navigation flows. For example, the `VerifyAccountPage` can be viewed in many different RHP flows. The proper approach to implementing such a mechanism is to create a new route for each screen instance within a single flow. + +Considerations when removing `backTo` from a URL: + +- For RHP screens, check if the correct central screen is under the overlay after refreshing the page. More information on how to set the default screen underneath RHP can be found (here)[#setting-the-correct-screen-underneath-rhp]. +- Ensure that after refreshing the page and pressing the back button in the application, you return to the page from which you initially accessed the currently displayed screen. +- If you use the same component for different routes, be sure to define the correct props type. Here's the example of `ReportScreen` that can be viewed in full screen width in the Inbox tab and in the Reports tab in the RHP. + +```ts +type ReportScreenNavigationProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; +``` + +An example of a screen that is reused in several flows is `VerifyAccountPage`. + +1. Binding one component to multiple screens. + +`src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx` + +```ts +const TravelModalStackNavigator = createModalStackNavigator({ + // ... + [SCREENS.TRAVEL.VERIFY_ACCOUNT]: () => require('../../../../pages/Travel/VerifyAccountPage').default, +}); + +const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator({ + // ... + [SCREENS.TWO_FACTOR_AUTH.VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Security/TwoFactorAuth/VerifyAccountPage').default, +}); +``` + +2. Custom component behavior depending on the current route. + +If we want the component's behavior to change based on the current route, we can extract the component shared by each route and pass properties to it that define the custom behavior, as is done for `VerifyAccountPage`. + +`VerifyAccountPageBase` is a shared component that receives `navigateBackTo` and `navigateForwardTo` props defining behavior that is custom across different flows. + +Here's an example of reusing this component for Wallet and Travel flows: + +1. `src/pages/settings/Wallet/VerifyAccountPage.tsx`. + +```ts +import React from 'react'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; + +function VerifyAccountPage() { + return ( + + ); +} + +VerifyAccountPage.displayName = 'VerifyAccountPage'; + +export default VerifyAccountPage; +``` + +2. `src/pages/Travel/VerifyAccountPage.tsx`. + +```ts +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import type {TravelNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type VerifyAccountPageProps = StackScreenProps; + +function VerifyAccountPage({route}: VerifyAccountPageProps) { + return ( + + ); +} + +VerifyAccountPage.displayName = 'VerifyAccountPage'; +export default VerifyAccountPage; + ``` ## Generating state from a path @@ -507,7 +601,7 @@ In Expensify, we use an extended implementation of this function because: - When opening a link leading to an onboarding screen, all previous screens in this flow have to be present in the navigation state. - In case of opening the RHP, appropriate screens should be pushed to the navigation to be displayed below the overlay. A guide on how to set up a good screen for RHP can be found [here](#how-to-set-a-correct-screen-below-the-rhp). - When opening the settings of a specific workspace, the workspace list needs to be pushed to the state. -- When the `backTo` parameter is in the URL, we need to build a state also for the screen we want to return to. +- When the `backTo` parameter is in the URL, we need to build a state also for the screen we want to return to. (`backTo` parameter is deprecated, more information can be found [here](#how-to-properly-remove-backto-from-url)) Here are examples how the state is generated based on route: @@ -623,7 +717,7 @@ In the above example, we can see that when building a state from a link leading ## Setting the correct screen underneath RHP -RHP screens can usually be opened from a specific central screen. Of course there are cases where one RHP screen can be used in different tabs (then using `backTo` parameter comes in handy). However, most often one RHP screen has a specific central screen assigned underneath. +RHP screens can usually be opened from a specific central screen. Of course there are cases where one RHP screen can be used in different tabs. However, most often one RHP screen has a specific central screen assigned underneath. > [!WARNING] > **Deprecated**: The `backTo` parameter is deprecated and should not be used in new implementations. Most problems that `backTo` solved can be resolved by adding one or more routes for a single screen. If you don't know how to solve your problem, contact someone from the navigation team. diff --git a/docs/articles/Unlisted/Compliance-Documentation.md b/docs/articles/Unlisted/Compliance-Documentation.md index 7036d65c1910..b2c7475ebad8 100644 --- a/docs/articles/Unlisted/Compliance-Documentation.md +++ b/docs/articles/Unlisted/Compliance-Documentation.md @@ -10,7 +10,7 @@ Expensify is committed to keeping your data secure and transparent. You can acce You can view or download the following audit documents to verify our security and operational controls: -- [Bridge Letter for SOC 1 and SOC 2](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1759782614043-Bridge_Letter_for_SOC_1_and_SOC_2_-_Sept_30_2025.pdf) +- [Bridge Letter for SOC 1 and SOC 2 – June 2025](https://drive.google.com/file/d/1kxttniCMLFah4uPNjhknxs0Zor6tWGWE/view?usp=drive_link) - [SOC 1 Type 2 Report – September 30, 2024](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1733950182002-SOC+1+Type+2+Report+09-30-24+-+Expensify.pdf) - [SOC 2 Type 2 Report – September 30, 2024](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1733950193162-SOC+2+Type+2+Report+09-30-24+-+Expensify.pdf) diff --git a/docs/redirects.csv b/docs/redirects.csv index 08e5a82fcf4b..a3284338ba5c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -892,4 +892,3 @@ https://help.expensify.com/classic,https://help.expensify.com/expensify-classic/ https://help.expensify.com/articles/new-expensify/expensify-card/Enable-Expensify-Card-notifications,https://help.expensify.com/articles/new-expensify/expensify-card/Expensify-Card-Notifications https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts.md/Add-or-remove-a-business-bank-account,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account https://help.expensify.com/articles/Unlisted/Compliance-Documentation,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security -https://drive.google.com/file/d/1kxttniCMLFah4uPNjhknxs0Zor6tWGWE/view,https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1759782614043-Bridge_Letter_for_SOC_1_and_SOC_2_-_Sept_30_2025.pdf diff --git a/eslint.config.js b/eslint.config.js index b25598057449..805e877da6e2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -311,7 +311,7 @@ const config = defineConfig([ { selector: 'CallExpression[callee.name="getUrlWithBackToParam"]', message: - 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into documentation and use alternative routing methods instead.', + 'Usage of getUrlWithBackToParam function is prohibited. This is legacy code and no new occurrences should be added. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', }, // These are the original rules from AirBnB's style guide, modified to allow for...of loops and for...in loops @@ -548,6 +548,20 @@ const config = defineConfig([ }, }, + { + files: ['src/libs/Navigation/types.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSPropertySignature[key.name="backTo"]', + message: + 'The `backTo` route param is deprecated. Do not add new `backTo` properties to screen param lists. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', + }, + ], + }, + }, + globalIgnores([ '!**/.storybook', '!**/.github', diff --git a/ios/AppIcon-staging.icon/Assets/Adhoc-Icon-Tinted-1024x1024.png b/ios/AppIcon-staging.icon/Assets/Adhoc-Icon-Tinted-1024x1024.png index 5a6d1d18c4f4..1d2272ff1a58 100644 Binary files a/ios/AppIcon-staging.icon/Assets/Adhoc-Icon-Tinted-1024x1024.png and b/ios/AppIcon-staging.icon/Assets/Adhoc-Icon-Tinted-1024x1024.png differ diff --git a/ios/AppIcon-staging.icon/Assets/App Icon.png b/ios/AppIcon-staging.icon/Assets/App Icon.png index 2c70f1f84775..26b2d3aafedb 100644 Binary files a/ios/AppIcon-staging.icon/Assets/App Icon.png and b/ios/AppIcon-staging.icon/Assets/App Icon.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 2fcdf8852bd7..66bc34e0f8f7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.2.36 + 9.2.37 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.2.36.5 + 9.2.37.4 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 9dc421d894b8..abd58d30ed8a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.2.36 + 9.2.37 CFBundleVersion - 9.2.36.5 + 9.2.37.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 183b76d760fe..7ad21c32b81b 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.2.36 + 9.2.37 CFBundleVersion - 9.2.36.5 + 9.2.37.4 NSExtension NSExtensionAttributes diff --git a/package-lock.json b/package-lock.json index 82c78d0be3ea..e7437bf065db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.2.36-5", + "version": "9.2.37-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.2.36-5", + "version": "9.2.37-4", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -63,7 +63,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.160", + "expensify-common": "2.0.162", "expo": "53.0.11", "expo-asset": "^11.1.2", "expo-av": "^15.1.5", @@ -117,7 +117,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.1.11", - "react-native-onyx": "3.0.6", + "react-native-onyx": "3.0.7", "react-native-pager-view": "6.9.1", "react-native-pdf": "7.0.2", "react-native-performance": "^5.1.4", @@ -21772,9 +21772,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.160", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.160.tgz", - "integrity": "sha512-lpEOgukBTkrJNZanUGJce1D6r7tDjdhClYd0UEDuWMT1Hgs9U9qigr8dUWPfC/Vp1bivFvNyhu62+C4I1kFWdA==", + "version": "2.0.162", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.162.tgz", + "integrity": "sha512-0XS77iCssXx5taDuLxvGFeSCYj9XOtQ7IH6PssyaQPNTWgPf0er9sAKD4KG8HmXx1kgldTB0Amp8IYcf09IsEQ==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", @@ -32384,9 +32384,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.6.tgz", - "integrity": "sha512-AR8tClMVMSOMvZm/7acsJERph1+l488JiT4GCu4XGn9QjMpmL039ohRjO0/mmr++rGp6zC6I61TDW/LyUxHC1w==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.7.tgz", + "integrity": "sha512-vnnf46biD3saa5Whzz4KlJ/Y36UE7nVeRKSr3wLmmfIa5BheyIoExL3fxKhV6mIjiRrWbPCr6kEEksN/CRYD3Q==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", diff --git a/package.json b/package.json index 9ae317b64799..fc05fb10565d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.2.36-5", + "version": "9.2.37-4", "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.", @@ -134,7 +134,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^5.0.3", - "expensify-common": "2.0.160", + "expensify-common": "2.0.162", "expo": "53.0.11", "expo-asset": "^11.1.2", "expo-av": "^15.1.5", @@ -188,7 +188,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.1.11", - "react-native-onyx": "3.0.6", + "react-native-onyx": "3.0.7", "react-native-pager-view": "6.9.1", "react-native-pdf": "7.0.2", "react-native-performance": "^5.1.4", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e1843b2c469e..d08f02a939d9 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1746,6 +1746,7 @@ const CONST = { MAX_PENDING_TIME_MS: 10 * 1000, RECHECK_INTERVAL_MS: 60 * 1000, MAX_REQUEST_RETRIES: 10, + MAX_OPEN_APP_REQUEST_RETRIES: 2, NETWORK_STATUS: { ONLINE: 'online', OFFLINE: 'offline', @@ -7305,24 +7306,11 @@ const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [ CONST.SEARCH.SYNTAX_FILTER_KEYS.ATTENDEE, ] as SearchFilterKey[]; -const FEATURE_IDS = { - CATEGORIES: 'categories', - ACCOUNTING: 'accounting', - COMPANY_CARDS: 'company-cards', - TAGS: 'tags', - WORKFLOWS: 'workflows', - INVOICES: 'invoices', - RULES: 'rules', - PER_DIEM: 'per-diem', - DISTANCE_RATES: 'distance-rates', - EXPENSIFY_CARD: 'expensify-card', -}; - const TASK_TO_FEATURE: Record = { - [CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES]: FEATURE_IDS.CATEGORIES, - [CONST.ONBOARDING_TASK_TYPE.ADD_ACCOUNTING_INTEGRATION]: FEATURE_IDS.ACCOUNTING, - [CONST.ONBOARDING_TASK_TYPE.CONNECT_CORPORATE_CARD]: FEATURE_IDS.COMPANY_CARDS, - [CONST.ONBOARDING_TASK_TYPE.SETUP_TAGS]: FEATURE_IDS.TAGS, + [CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES]: CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED, + [CONST.ONBOARDING_TASK_TYPE.ADD_ACCOUNTING_INTEGRATION]: CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, + [CONST.ONBOARDING_TASK_TYPE.CONNECT_CORPORATE_CARD]: CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED, + [CONST.ONBOARDING_TASK_TYPE.SETUP_TAGS]: CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED, }; const FRAUD_PROTECTION_EVENT = { @@ -7358,6 +7346,6 @@ type CancellationType = ValueOf; export type {Country, IOUAction, IOUType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType, OnboardingInvite, OnboardingAccounting, IOUActionParams}; -export {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS, TASK_TO_FEATURE, FEATURE_IDS, FRAUD_PROTECTION_EVENT}; +export {CONTINUATION_DETECTION_SEARCH_FILTER_KEYS, TASK_TO_FEATURE, FRAUD_PROTECTION_EVENT}; export default CONST; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f2af8e9330ba..267649095ee7 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -48,6 +48,9 @@ const ONYXKEYS = { /** Keeps track if there is modal currently visible or not */ MODAL: 'modal', + /** Keeps track if OpenApp failure modal is opened */ + IS_OPEN_APP_FAILURE_MODAL_OPEN: 'isOpenAppFailureModalOpen', + /** Stores the PIN for an activated UK/EU Expensify card to be shown once after activation */ ACTIVATED_CARD_PIN: 'activatedCardPin', @@ -697,6 +700,9 @@ const ONYXKEYS = { /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_', + + /** Used for identifying user as admin of a domain */ + SHARED_NVP_PRIVATE_ADMIN_ACCESS: 'sharedNVP_private_admin_access_', }, /** List of Form ids */ @@ -1081,6 +1087,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID; [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; }; type OnyxValuesMapping = { @@ -1104,6 +1111,7 @@ type OnyxValuesMapping = { [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.MODAL]: OnyxTypes.Modal; + [ONYXKEYS.IS_OPEN_APP_FAILURE_MODAL_OPEN]: boolean; [ONYXKEYS.FULLSCREEN_VISIBILITY]: boolean; [ONYXKEYS.NETWORK]: OnyxTypes.Network; [ONYXKEYS.NEW_GROUP_CHAT_DRAFT]: OnyxTypes.NewGroupChatDraft; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bb54b133d106..4aba22226059 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3083,7 +3083,7 @@ const ROUTES = { }, POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE: { route: 'workspaces/:policyID/accounting/sage-intacct/import/mapping-type/:mapping', - getRoute: (policyID: string, mapping: string) => `workspaces/${policyID}/accounting/sage-intacct/import/mapping-type/${mapping}` as const, + getRoute: (policyID: string | undefined, mapping: string) => `workspaces/${policyID}/accounting/sage-intacct/import/mapping-type/${mapping}` as const, }, POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT_TAX: { route: 'workspaces/:policyID/accounting/sage-intacct/import/tax', diff --git a/src/components/AnimatedSubmitButton/index.tsx b/src/components/AnimatedSubmitButton/index.tsx index aabe97dca511..8567f82b7616 100644 --- a/src/components/AnimatedSubmitButton/index.tsx +++ b/src/components/AnimatedSubmitButton/index.tsx @@ -22,9 +22,12 @@ type AnimatedSubmitButtonProps = { // Function to call when the animation finishes onAnimationFinish: () => void; + + // Whether the button should be disabled + isDisabled?: boolean; }; -function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunning, onAnimationFinish}: AnimatedSubmitButtonProps) { +function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunning, onAnimationFinish, isDisabled}: AnimatedSubmitButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const isAnimationRunning = isSubmittingAnimationRunning; @@ -128,6 +131,7 @@ function AnimatedSubmitButton({success, text, onPress, isSubmittingAnimationRunn text={text} onPress={onPress} icon={icon} + isDisabled={isDisabled} /> )} diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 9287934ebdfb..ff1c2d800fc7 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -1,15 +1,14 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import {isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; import {isCurrentUserSubmitter, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; -import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; -import TextLink from './TextLink'; +import RenderHTML from './RenderHTML'; type BrokenConnectionDescriptionProps = { /** Transaction id of the corresponding report */ @@ -23,13 +22,14 @@ type BrokenConnectionDescriptionProps = { }; function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { - const styles = useThemeStyles(); const {translate} = useLocalize(); const transactionViolations = useTransactionViolations(transactionID); + const {environmentURL} = useEnvironment(); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); + const workspaceCompanyCardRoute = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id)}`; if (!brokenConnection530Error && !brokenConnectionError) { return ''; @@ -40,16 +40,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn } if (isPolicyAdmin && !isCurrentUserSubmitter(report)) { - return ( - <> - {`${translate('violations.adminBrokenConnectionError')}`} - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id))} - >{`${translate('workspace.common.companyCards')}`} - . - - ); + return ; } if (isReportApproved({report}) || isReportManuallyReimbursed(report)) { diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx new file mode 100644 index 000000000000..79cecbc180f9 --- /dev/null +++ b/src/components/Domain/DomainsListRow.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import TextWithTooltip from '@components/TextWithTooltip'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type DomainsListRowProps = { + /** Name of the domain */ + title: string; + + /** Whether the row is hovered, so we can modify its style */ + isHovered: boolean; + + /** Whether the icon at the end of the row should be displayed */ + shouldShowRightIcon: boolean; +}; + +function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + + + + {shouldShowRightIcon && ( + + + + )} + + ); +} + +DomainsListRow.displayName = 'DomainsListRow'; + +export default DomainsListRow; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index a6130522957f..48edc09d7a39 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -43,7 +43,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona if (!isEmpty(htmlAttribAccountID) && personalDetails?.[htmlAttribAccountID]) { const user = personalDetails[htmlAttribAccountID]; accountID = parseInt(htmlAttribAccountID, 10); - mentionDisplayText = getDisplayNameOrDefault(user) || formatPhoneNumber(user?.login ?? ''); + mentionDisplayText = formatPhoneNumber(user?.login ?? '') || getDisplayNameOrDefault(user); mentionDisplayText = getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID, currentUserPersonalDetails, user?.login ?? '') ?? ''; navigationRoute = ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute()); } else if ('data' in tnode && !isEmptyObject(tnode.data)) { @@ -56,15 +56,9 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona Str.removeSMSDomain(getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID, currentUserPersonalDetails) ?? ''), ); - accountID = getAccountIDsByLogins([mentionDisplayText], false)?.at(0) ?? -1; - if (accountID !== -1) { - const user = personalDetails?.[accountID]; - mentionDisplayText = getDisplayNameOrDefault(user) || formatPhoneNumber(user?.login ?? ''); - mentionDisplayText = getShortMentionIfFound(mentionDisplayText, htmlAttributeAccountID, currentUserPersonalDetails, user?.login ?? '') ?? ''; - } else { - mentionDisplayText = Str.removeSMSDomain(mentionDisplayText); - } + accountID = getAccountIDsByLogins([mentionDisplayText])?.at(0) ?? -1; navigationRoute = ROUTES.PROFILE.getRoute(accountID, Navigation.getReportRHPActiveRoute(), mentionDisplayText); + mentionDisplayText = Str.removeSMSDomain(mentionDisplayText); } else { // If neither an account ID or email is provided, don't render anything return null; @@ -120,7 +114,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona testID="mention-user" href={`/${navigationRoute}`} > - {accountID && accountID !== -1 ? `@${mentionDisplayText}` : } + {htmlAttribAccountID ? `@${mentionDisplayText}` : } diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 11cda3464b7b..5a4fb048812a 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -64,6 +64,8 @@ type LocaleContextProps = { preferredLocale: Locale | undefined; }; +type LocalizedTranslate = LocaleContextProps['translate']; + const LocaleContext = createContext({ translate: () => '', numberFormat: () => '', @@ -224,4 +226,4 @@ LocaleContextProvider.displayName = 'LocaleContextProvider'; export {LocaleContext, LocaleContextProvider}; -export type {Locale, LocaleContextProps}; +export type {Locale, LocaleContextProps, LocalizedTranslate}; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c4c38f091f35..85763d819d4f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -6,6 +6,7 @@ import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDeleteTransactions from '@hooks/useDeleteTransactions'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -22,6 +23,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; +import useStrictPolicyRules from '@hooks/useStrictPolicyRules'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; @@ -37,9 +39,9 @@ import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButt import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, SearchFullscreenNavigatorParamList, SearchReportParamList} from '@libs/Navigation/types'; -import {buildOptimisticNextStepForPreventSelfApprovalsEnabled} from '@libs/NextStepUtils'; -import {selectPaymentType} from '@libs/PaymentUtils'; +import {buildOptimisticNextStepForPreventSelfApprovalsEnabled, buildOptimisticNextStepForStrictPolicyRuleViolations} from '@libs/NextStepUtils'; import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; +import {selectPaymentType} from '@libs/PaymentUtils'; import Permissions from '@libs/Permissions'; import {getConnectedIntegration, getValidConnectedIntegration} from '@libs/PolicyUtils'; import {getIOUActionForReportID, getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -65,6 +67,7 @@ import { navigateOnDeleteExpense, navigateToDetailsPage, rejectMoneyRequestReason, + shouldBlockSubmitDueToStrictPolicyRules, } from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import { @@ -86,7 +89,6 @@ import { canApproveIOU, cancelPayment, canIOUBePaid as canIOUBePaidAction, - deleteMoneyRequest, dismissRejectUseExplanation, getNavigationUrlOnMoneyRequestDelete, initSplitExpense, @@ -199,6 +201,9 @@ function MoneyReportHeader({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Buildings'] as const); const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE, {canBeMissing: true}); const exportTemplates = useMemo(() => getExportTemplates(integrationsExportTemplates ?? [], csvExportLayouts ?? {}, policy), [integrationsExportTemplates, csvExportLayouts, policy]); + const {areStrictPolicyRulesEnabled} = useStrictPolicyRules(); + const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const requestParentReportAction = useMemo(() => { if (!reportActions || !transactionThreadReport?.parentReportActionID) { @@ -215,6 +220,10 @@ function MoneyReportHeader({ return Object.values(reportTransactions); }, [reportTransactions]); + const shouldBlockSubmit = useMemo(() => { + return shouldBlockSubmitDueToStrictPolicyRules(moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, transactions); + }, [moneyRequestReport?.reportID, violations, areStrictPolicyRulesEnabled, transactions]); + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`, { canBeMissing: true, @@ -230,6 +239,7 @@ function MoneyReportHeader({ ); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID)); + const {deleteTransactions} = useDeleteTransactions({report: chatReport, reportActions, policy}); const isExported = useMemo(() => isExportedUtils(reportActions), [reportActions]); // wrapped in useMemo to improve performance because this is an operation on array const integrationNameFromExportMessage = useMemo(() => { @@ -313,7 +323,7 @@ function MoneyReportHeader({ const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isRejectEducationalModalVisible, setIsRejectEducationalModalVisible] = useState(false); - const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey} = useSearchContext(); + const {selectedTransactionIDs, removeTransaction, clearSelectedTransactions, currentSearchQueryJSON, currentSearchKey, currentSearchHash} = useSearchContext(); const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true); const {wideRHPRouteKeys} = useContext(WideRHPContext); @@ -388,7 +398,11 @@ function MoneyReportHeader({ // to avoid any flicker during transitions between online/offline states const nextApproverAccountID = getNextApproverAccountID(moneyRequestReport); const isSubmitterSameAsNextApprover = isReportOwner(moneyRequestReport) && nextApproverAccountID === moneyRequestReport?.ownerAccountID; - const optimisticNextStep = isSubmitterSameAsNextApprover && policy?.preventSelfApproval ? buildOptimisticNextStepForPreventSelfApprovalsEnabled() : nextStep; + let optimisticNextStep = isSubmitterSameAsNextApprover && policy?.preventSelfApproval ? buildOptimisticNextStepForPreventSelfApprovalsEnabled() : nextStep; + + if (shouldBlockSubmit && isReportOwner(moneyRequestReport)) { + optimisticNextStep = buildOptimisticNextStepForStrictPolicyRuleViolations(); + } const shouldShowNextStep = isFromPaidPolicy && !isInvoiceReport && !shouldShowStatusBar; const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); @@ -465,7 +479,7 @@ function MoneyReportHeader({ if (currentSearchQueryJSON) { search({ searchKey: currentSearchKey, - shouldCalculateTotals: true, + shouldCalculateTotals, offset: 0, queryJSON: currentSearchQueryJSON, isOffline, @@ -705,7 +719,7 @@ function MoneyReportHeader({ success text={translate('common.submit')} onPress={() => { - if (!moneyRequestReport) { + if (!moneyRequestReport || shouldBlockSubmit) { return; } startSubmittingAnimation(); @@ -722,6 +736,7 @@ function MoneyReportHeader({ }} isSubmittingAnimationRunning={isSubmittingAnimationRunning} onAnimationFinish={stopAnimation} + isDisabled={shouldBlockSubmit} /> ), [CONST.REPORT.PRIMARY_ACTIONS.APPROVE]: ( @@ -974,7 +989,7 @@ function MoneyReportHeader({ } const currentTransaction = transactions.at(0); - initSplitExpense(currentTransaction); + initSplitExpense(allTransactions, allReports, currentTransaction); }, }, [CONST.REPORT.SECONDARY_ACTIONS.MERGE]: { @@ -1304,15 +1319,7 @@ function MoneyReportHeader({ // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteMoneyRequest( - transaction?.transactionID, - requestParentReportAction, - duplicateTransactions, - duplicateTransactionViolations, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - ); + deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false); removeTransaction(transaction.transactionID); }); goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, requestParentReportAction, iouReport, chatIOUReport, isChatIOUReportArchived, false); diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx index 455f9e443610..f0855a85fd53 100644 --- a/src/components/MoneyReportHeaderStatusBar.tsx +++ b/src/components/MoneyReportHeaderStatusBar.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as NextStepUtils from '@libs/NextStepUtils'; +import {parseMessage} from '@libs/NextStepUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type ReportNextStep from '@src/types/onyx/ReportNextStep'; @@ -14,7 +14,7 @@ import RenderHTML from './RenderHTML'; type MoneyReportHeaderStatusBarProps = { /** The next step for the report */ - nextStep: ReportNextStep; + nextStep: ReportNextStep | undefined; }; type IconName = ValueOf; @@ -29,15 +29,15 @@ function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps) const styles = useThemeStyles(); const theme = useTheme(); const messageContent = useMemo(() => { - const messageArray = nextStep.message; - return NextStepUtils.parseMessage(messageArray); - }, [nextStep.message]); + const messageArray = nextStep?.message; + return parseMessage(messageArray); + }, [nextStep?.message]); return ( { - // Set the default tax code when conditions change - if (!shouldShowTax || !transaction || !transactionID) { + if (!transactionID) { return; } - const defaultTaxCode = getDefaultTaxCode(policy, transaction); - const currentTaxCode = transaction.taxCode ?? ''; - - // Update tax code if it's different from what should be the default - if (defaultTaxCode !== currentTaxCode) { - setMoneyRequestTaxRate(transactionID, defaultTaxCode ?? ''); - } - }, [customUnitRateID, policy, shouldShowTax, transaction, transactionID]); + setMoneyRequestTaxRate(transactionID, defaultTaxCode); + }, [defaultTaxCode, transactionID]); const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 0bdb46715530..5f62518741ee 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -5,6 +5,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDeleteTransactions from '@hooks/useDeleteTransactions'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; @@ -15,7 +16,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; -import {deleteMoneyRequest, deleteTrackExpense, initSplitExpense, markRejectViolationAsResolved} from '@libs/actions/IOU'; +import {deleteTrackExpense, initSplitExpense, markRejectViolationAsResolved} from '@libs/actions/IOU'; import {setupMergeTransactionData} from '@libs/actions/MergeTransaction'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; @@ -111,8 +112,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isOnHold = isOnHoldTransactionUtils(transaction); const isDuplicate = isDuplicateTransactionUtils(transaction); const reportID = report?.reportID; - const {removeTransaction} = useSearchContext(); + const {removeTransaction, currentSearchHash} = useSearchContext(); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction); + const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); + const {deleteTransactions} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy}); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; @@ -308,7 +312,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre icon: Expensicons.ArrowSplit, value: CONST.REPORT.SECONDARY_ACTIONS.SPLIT, onSelected: () => { - initSplitExpense(transaction); + initSplitExpense(allTransactions, allReports, transaction); }, }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.MERGE]: { @@ -455,16 +459,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre isChatIOUReportArchived, }); } else { - deleteMoneyRequest( - transaction.transactionID, - parentReportAction, - duplicateTransactions, - duplicateTransactionViolations, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - true, - ); + deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true); removeTransaction(transaction.transactionID); } onBackButtonPress(); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx index 0c54e5e674dc..4deda521d669 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportNavigation.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import PrevNextButtons from '@components/PrevNextButtons'; import Text from '@components/Text'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,8 +23,7 @@ type MoneyRequestReportNavigationProps = { function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: MoneyRequestReportNavigationProps) { const [lastSearchQuery] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true}); const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`, {canBeMissing: true}); - const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: (s) => s?.accountID}); - + const currentUserDetails = useCurrentUserPersonalDetails(); const {localeCompare, formatPhoneNumber} = useLocalize(); const [exportReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { @@ -37,7 +37,17 @@ function MoneyRequestReportNavigation({reportID, shouldDisplayNarrowVersion}: Mo const {type, status, sortBy, sortOrder, groupBy} = lastSearchQuery?.queryJSON ?? {}; let results: Array = []; if (!!type && !!groupBy && !!currentSearchResults?.data && !!currentSearchResults?.search) { - const searchData = getSections(type, currentSearchResults.data, accountID, formatPhoneNumber, groupBy, exportReportActions, lastSearchQuery?.searchKey, archivedReportsIdSet); + const searchData = getSections( + type, + currentSearchResults.data, + currentUserDetails.accountID, + currentUserDetails.email ?? '', + formatPhoneNumber, + groupBy, + exportReportActions, + lastSearchQuery?.searchKey, + archivedReportsIdSet, + ); results = getSortedSections(type, status ?? '', searchData, localeCompare, sortBy, sortOrder, groupBy).map((value) => value.reportID); } const allReports = results; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 1f377c76c4b6..af8b31c5608a 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -9,12 +9,13 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; import Text from '@components/Text'; import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -152,7 +153,7 @@ function MoneyRequestReportTransactionList({ const formattedCompanySpendAmount = convertToDisplayString(nonReimbursableSpend, report?.currency); const shouldShowBreakdown = !!nonReimbursableSpend && !!reimbursableSpend; const transactionsWithoutPendingDelete = useMemo(() => transactions.filter((t) => !isTransactionPendingDelete(t)), [transactions]); - const session = useSession(); + const currentUserDetails = useCurrentUserPersonalDetails(); const isReportArchived = useReportIsArchived(report?.reportID); const shouldShowAddExpenseButton = canAddTransaction(report, isReportArchived) && isCurrentUserSubmitter(report); const addExpenseDropdownOptions = useMemo(() => getAddExpenseDropdownOptions(report?.reportID, policy), [report?.reportID, policy]); @@ -176,7 +177,7 @@ function MoneyRequestReportTransactionList({ for (const transaction of transactions) { const transactionViolations = violations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]; if (transactionViolations) { - const filteredTransactionViolations = transactionViolations.filter((violation) => shouldShowViolation(report, policy, violation.name)); + const filteredTransactionViolations = transactionViolations.filter((violation) => shouldShowViolation(report, policy, violation.name, currentUserDetails.email ?? '')); if (filteredTransactionViolations.length > 0) { filtered[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] = filteredTransactionViolations; @@ -185,7 +186,7 @@ function MoneyRequestReportTransactionList({ } return filtered; - }, [violations, report, policy, transactions]); + }, [violations, report, policy, transactions, currentUserDetails.email]); const toggleTransaction = useCallback( (transactionID: string) => { @@ -230,9 +231,9 @@ function MoneyRequestReportTransactionList({ }, [newTransactions, sortBy, sortOrder, transactions, localeCompare, report]); const columnsToShow = useMemo(() => { - const columns = getColumnsToShow(session?.accountID, transactions, true); + const columns = getColumnsToShow(currentUserDetails?.accountID, transactions, true); return (Object.keys(columns) as SearchColumnType[]).filter((column) => columns[column]); - }, [transactions, session?.accountID]); + }, [transactions, currentUserDetails?.accountID]); /** * Navigate to the transaction thread for a transaction, creating one optimistically if it doesn't yet exist. diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 888be96db9c4..9e816cf999ec 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -48,9 +48,6 @@ type MoneyRequestReportViewProps = { /** Whether Report footer (that includes Composer) should be displayed */ shouldDisplayReportFooter: boolean; - /** Whether we should wait for the report to sync */ - shouldWaitForReportSync: boolean; - /** The `backTo` route that should be used when clicking back button */ backToRoute: Route | undefined; }; @@ -83,7 +80,7 @@ function InitialLoadingSkeleton({styles}: {styles: ThemeStyles}) { ); } -function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayReportFooter, backToRoute, shouldWaitForReportSync}: MoneyRequestReportViewProps) { +function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayReportFooter, backToRoute}: MoneyRequestReportViewProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -108,7 +105,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); const isSentMoneyReport = useMemo(() => reportActions.some((action) => isSentMoneyReportAction(action)), [reportActions]); - const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, shouldWaitForReportSync ? [] : transactions); + const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, transactions); const parentReportAction = useParentReportAction(report); @@ -167,7 +164,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe [backToRoute, isLoadingInitialReportActions, isTransactionThreadView, parentReportAction, policy, report, reportActions, transactionThreadReportID], ); - if (!!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions || shouldWaitForReportSync) { + if (!!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions) { return ; } diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx index 10fd1775e6ca..9829ab19aa35 100644 --- a/src/components/NumberWithSymbolForm.tsx +++ b/src/components/NumberWithSymbolForm.tsx @@ -450,7 +450,10 @@ function NumberWithSymbolForm({ ); return ( - + {shouldWrapInputInContainer ? ( void; +}; + +function BaseOpenAppFailureModal({onRefreshAndTryAgainButtonPress}: BaseOpenAppFailureModalProps) { + const [isOpenAppFailureModalOpen = false] = useOnyx(ONYXKEYS.IS_OPEN_APP_FAILURE_MODAL_OPEN, {canBeMissing: true}); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to be consistent with BaseModal component + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + return ( + setIsOpenAppFailureModalOpen(false)} + > + +
+ + {`${translate('openAppFailureModal.subtitle')} `} + + {CONST.EMAIL.CONCIERGE} + + +