diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index b3adf0f59b9c..a2c7365f7de8 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -32,10 +32,6 @@ "/": "/iou/*", "comment": "I Owe You reports" }, - { - "/": "/request/*", - "comment": "Money request" - }, { "/": "/enable-payments/*", "comment": "Payments setup" @@ -54,11 +50,11 @@ }, { "/": "/split/*", - "comment": "Split Bill" + "comment": "Split Expense" }, { "/": "/request/*", - "comment": "Request Money" + "comment": "Submit Expense" }, { "/": "/new/*", @@ -82,7 +78,7 @@ }, { "/": "/send/*", - "comment": "Send money" + "comment": "Pay someone" }, { "/": "/money2020/*", diff --git a/android/app/build.gradle b/android/app/build.gradle index 32dfb496daa8..237c284700ed 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046213 - versionName "1.4.62-13" + versionCode 1001046214 + versionName "1.4.62-14" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/redirects.csv b/docs/redirects.csv index 51c8c7515e10..af595ecc5f83 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -152,7 +152,6 @@ https://help.expensify.com/articles/expensify-classic/manage-employees-and-repor https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Removing-Members https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/ -https://help.expensify.com/articles/expensify-classic/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing-Reports,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Reimbursing-Reports https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 0b59d7e8316d..fa153e5ec674 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.62.13 + 1.4.62.14 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2a6db0634f2f..eed52462a526 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.62.13 + 1.4.62.14 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 50171f3eb050..35dbb3c0b6cd 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.62 CFBundleVersion - 1.4.62.13 + 1.4.62.14 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ac5f3fdd397e..5a8c67088f16 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1818,7 +1818,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.47): + - RNLiveMarkdown (0.1.62): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1836,9 +1836,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.47) + - RNLiveMarkdown/common (= 0.1.62) - Yoga - - RNLiveMarkdown/common (0.1.47): + - RNLiveMarkdown/common (0.1.62): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2547,7 +2547,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: f172c7199283dc9d21bccf7e21ea10741fd19e1d + RNLiveMarkdown: 47dfb50244f9ba1caefbc0efc6404ba41bf6620a RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 @@ -2564,7 +2564,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/package-lock.json b/package-lock.json index 478ae3c12b3c..64dd4fb0c885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.62-13", + "version": "1.4.62-14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.62-13", + "version": "1.4.62-14", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.49", + "@expensify/react-native-live-markdown": "0.1.62", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -57,7 +57,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -3570,9 +3570,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.49", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.49.tgz", - "integrity": "sha512-5l+/NtUTuSxWkdsT2JOlhKD5NW1hZ+nQcmgrCSz5e/TNIcfkYjJNiW/nEf8qmBV54afiTmTTwKYrh2DwM/BQ0g==", + "version": "0.1.62", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.62.tgz", + "integrity": "sha512-o70/tFIGZJ1U8U8aqTQu1HAZed6nt5LYWk74mrceRxQHOqsKhZgn2q5EuEy8EMIcnCGKjwxuDyZJbuRexgHx/A==", "engines": { "node": ">= 18.0.0" }, @@ -16462,10 +16462,8 @@ }, "node_modules/classnames": { "version": "2.5.0", - "license": "MIT", - "workspaces": [ - "benchmarks" - ] + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", + "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -16551,7 +16549,8 @@ }, "node_modules/clipboard": { "version": "2.0.11", - "license": "MIT", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", "dependencies": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -18058,7 +18057,8 @@ }, "node_modules/delegate": { "version": "3.2.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" }, "node_modules/delegates": { "version": "1.0.0", @@ -20212,8 +20212,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", - "integrity": "sha512-/NAZoAXqeqFWHvC61dueqq9VjRugF69urUtDdDhsfvu1sQE2PCnBoM7a+ACoAEWRYrnP82cyHHhdSA8e7fPuAg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -20266,6 +20266,8 @@ }, "node_modules/expensify-common/node_modules/ua-parser-js": { "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", "funding": [ { "type": "opencollective", @@ -20280,7 +20282,6 @@ "url": "https://github.com/sponsors/faisalman" } ], - "license": "MIT", "engines": { "node": "*" } @@ -21728,7 +21729,8 @@ }, "node_modules/good-listener": { "version": "1.2.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", "dependencies": { "delegate": "^3.1.2" } @@ -22779,7 +22781,8 @@ }, "node_modules/immediate": { "version": "3.0.6", - "license": "MIT" + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -26838,7 +26841,8 @@ }, "node_modules/lie": { "version": "3.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", "dependencies": { "immediate": "~3.0.5" } @@ -26981,7 +26985,8 @@ }, "node_modules/localforage": { "version": "1.10.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", "dependencies": { "lie": "3.1.1" } @@ -33311,7 +33316,8 @@ }, "node_modules/select": { "version": "1.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, "node_modules/select-hose": { "version": "2.0.0", diff --git a/package.json b/package.json index e5092e132eae..78e1a3a13e2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.62-13", + "version": "1.4.62-14", "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.", @@ -64,7 +64,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.49", + "@expensify/react-native-live-markdown": "0.1.62", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -108,7 +108,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#13de5b0606662df33fa1392ad82cc11daadff52e", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", diff --git a/src/CONST.ts b/src/CONST.ts index 556a161876f4..a6df33987c8d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -879,7 +879,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - SEARCH_RENDER: 'search_render', + CHAT_FINDER_RENDER: 'search_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', @@ -1363,7 +1363,7 @@ const CONST = { }, KYC_WALL_SOURCE: { - REPORT: 'REPORT', // The user attempted to pay a money request + REPORT: 'REPORT', // The user attempted to pay an expense ENABLE_WALLET: 'ENABLE_WALLET', // The user clicked on the `Enable wallet` button on the Wallet page TRANSFER_BALANCE: 'TRANSFER_BALANCE', // The user attempted to transfer their wallet balance to their bank account or debit card }, @@ -1399,7 +1399,7 @@ const CONST = { }, IOU: { - // This is the transactionID used when going through the create money request flow so that it mimics a real transaction (like the edit flow) + // This is the transactionID used when going through the create expense flow so that it mimics a real transaction (like the edit flow) OPTIMISTIC_TRANSACTION_ID: '1', // Note: These payment types are used when building IOU reportAction message values in the server and should // not be changed. @@ -3549,12 +3549,11 @@ const CONST = { ONBOARDING_CONCIERGE: { [onboardingChoices.TRACK]: - "# Welcome to Expensify, let's start tracking your expenses!\n" + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + "# Let's start tracking your expenses!\n" + '\n' + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + '1. From the home screen, click the green + button > New Workspace\n' + - '2. Give your workspace a name (e.g. "My business expenses”).\n' + + '2. Give your workspace a name (e.g. "My business expenses").\n' + '\n' + 'Then, add expenses to your workspace:\n' + '1. Find your workspace using the search field.\n' + @@ -3563,8 +3562,7 @@ const CONST = { '\n' + "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", [onboardingChoices.EMPLOYER]: - '# Welcome to Expensify, the fastest way to get paid back!\n' + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '# Expensify is the fastest way to get paid back!\n' + '\n' + 'To submit expenses for reimbursement:\n' + '1. From the home screen, click the green + button > Request money.\n' + @@ -3572,21 +3570,19 @@ const CONST = { '\n' + "That'll send a request to get you paid back. Let me know if you have any questions!", [onboardingChoices.MANAGE_TEAM]: - "# Welcome to Expensify, let's start managing your team's expenses!\n" + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + "# Let's start managing your team's expenses!\n" + '\n' + "To manage your team's expenses, create a workspace to keep everything in one place. Here's how:\n" + '1. From the home screen, click the green + button > New Workspace\n' + - '2. Give your workspace a name (e.g. “Sales team expenses”).\n' + + '2. Give your workspace a name (e.g. "Sales team expenses").\n' + '\n' + - 'Then, invite your team to your workspace via the Members pane and connect a business bank account to reimburse them. Let me know if you have any questions!', + 'Then, invite your team to your workspace via the Members pane and [connect a business bank account](https://help.expensify.com/articles/new-expensify/bank-accounts/Connect-a-Bank-Account) to reimburse them. Let me know if you have any questions!', [onboardingChoices.PERSONAL_SPEND]: - "# Welcome to Expensify, let's start tracking your expenses!\n" + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + "# Let's start tracking your expenses! \n" + '\n' + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + '1. From the home screen, click the green + button > New Workspace\n' + - '2. Give your workspace a name (e.g. "My expenses”).\n' + + '2. Give your workspace a name (e.g. "My expenses").\n' + '\n' + 'Then, add expenses to your workspace:\n' + '1. Find your workspace using the search field.\n' + @@ -3595,19 +3591,13 @@ const CONST = { '\n' + "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", [onboardingChoices.CHAT_SPLIT]: - '# Welcome to Expensify, where splitting the bill is an easy conversation!\n' + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '# Splitting the bill is as easy as a conversation!\n' + '\n' + 'To split an expense:\n' + '1. From the home screen, click the green + button > Request money.\n' + '2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' + '\n' + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", - [onboardingChoices.LOOKING_AROUND]: - '# Welcome to Expensify!\n' + - "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + - '\n' + - "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", }, REPORT_FIELD_TITLE_FIELD_ID: 'text_title', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 89114700e0d4..7c9247bcdbd7 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -126,7 +126,7 @@ const ONYXKEYS = { /** The NVP with the last payment method used per policy */ NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod', - /** This NVP holds to most recent waypoints that a person has used when creating a distance request */ + /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ @@ -318,6 +318,11 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_', + // Whether the policy's connection data was attempted to be fetched in + // the current user session. As this state only exists client-side, it + // should not be included as part of the policy object. The policy + // object should mirror the data as it's stored in the database. + POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED: 'policyHasConnectionsDataBeenFetched_', OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', POLICY_CONNECTION_SYNC_PROGRESS: 'policyConnectionSyncProgress_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', @@ -346,7 +351,7 @@ const ONYXKEYS = { PRIVATE_NOTES_DRAFT: 'privateNotesDraft_', NEXT_STEP: 'reportNextStep_', - // Manual request tab selector + // Manual expense tab selector SELECTED_TAB: 'selectedTab_', /** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */ @@ -524,7 +529,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategories; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTagList; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; + [ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED]: boolean; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyEmployeeList; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3b745512bcc2..7d73d8e55503 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -28,7 +28,7 @@ const ROUTES = { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const, }, - SEARCH: 'search', + CHAT_FINDER: 'chat-finder', DETAILS: { route: 'details', getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 27d8331b5473..d474945d332e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -95,7 +95,7 @@ const SCREENS = { ROOT: 'SaveTheWorld_Root', }, LEFT_MODAL: { - SEARCH: 'Search', + CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, WORKSPACE_SWITCHER: { @@ -321,7 +321,7 @@ const SCREENS = { }, ROOM_MEMBERS_ROOT: 'RoomMembers_Root', ROOM_INVITE_ROOT: 'RoomInvite_Root', - SEARCH_ROOT: 'Search_Root', + CHAT_FINDER_ROOT: 'ChatFinder_Root', FLAG_COMMENT_ROOT: 'FlagComment_Root', REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', GET_ASSISTANCE: 'GetAssistance', diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index 17c5097b8154..351129a5ee3e 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -24,7 +24,7 @@ type ConfirmedRoutePropsOnyxProps = { }; type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & { - /** Transaction that stores the distance request data */ + /** Transaction that stores the distance expense data */ transaction: OnyxEntry; }; diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index c7816b710692..6170b81073a2 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -34,7 +34,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', - sections: [{data: yearsList.sort((a, b) => b.value - a.value)}], + sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], }; }, [years, searchText, translate]); diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 20b927913bfb..f6adac9b6034 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -20,7 +20,7 @@ import ScrollView from './ScrollView'; import Text from './Text'; type DistanceEReceiptProps = { - /** The transaction for the distance request */ + /** The transaction for the distance expense */ transaction: Transaction; }; diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx index 624ea940888b..e21bdc1c9e56 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx @@ -27,7 +27,7 @@ type DistanceRequestFooterOnyxProps = { }; type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & { - /** The waypoints for the distance request */ + /** The waypoints for the distance expense */ waypoints?: WaypointCollection; /** Function to call when the user wants to add a new waypoint */ diff --git a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx index f4b11c159326..fa143192eafc 100644 --- a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx +++ b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx @@ -7,7 +7,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; type DistanceRequestProps = { - /** The waypoints for the distance request */ + /** The waypoints for the distance expense */ waypoints?: WaypointCollection; /** The index of the item */ diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 9551e9d8f7c8..51f9981f1524 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -92,6 +92,9 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagsStyles={styles.webViewStyles.tagStyles} enableCSSInlineProcessing={false} systemFonts={Object.values(FontUtils.fontFamily.single)} + htmlParserOptions={{ + recognizeSelfClosing: true, + }} domVisitors={{ // eslint-disable-next-line no-param-reassign onText: (text) => (text.data = convertToLTR(text.data)), diff --git a/src/components/HoldBanner.tsx b/src/components/HoldBanner.tsx index af77d9076629..d760d4efbe96 100644 --- a/src/components/HoldBanner.tsx +++ b/src/components/HoldBanner.tsx @@ -12,7 +12,7 @@ function HoldBanner() { return ( {translate('iou.hold')} - {translate('iou.requestOnHold')} + {translate('iou.expenseOnHold')} ); } diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 14227d6a2051..56dc6bf0075d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -43,7 +43,7 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { /** The report currently being looked at */ report: OnyxTypes.Report; - /** The policy tied to the money request report */ + /** The policy tied to the expense report */ policy: OnyxEntry; /** Array of report actions for the report */ @@ -288,7 +288,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money danger /> setIsDeleteRequestModalVisible(false)} diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index aa6c75edbf5d..4b3e4096484f 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -342,12 +342,12 @@ function MoneyRequestConfirmationList({ if (isSplitBill && iouAmount === 0) { text = translate('iou.split'); } else if ((!!receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.request'); + text = translate('iou.expense'); if (iouAmount !== 0) { - text = translate('iou.requestAmount', {amount: formattedAmount}); + text = translate('iou.submitAmount', {amount: formattedAmount}); } } else { - const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; + const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.submitAmount'; text = translate(translationKey, {amount: formattedAmount}); } return [ diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f451f5f15581..f7825ef2f622 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -63,7 +63,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isOnHold = TransactionUtils.isOnHold(transaction); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); - // Only the requestor can take delete the request, admins can only edit it. + // Only the requestor can take delete the expense, admins can only edit it. const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID; const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; @@ -118,14 +118,14 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, - text: translate('iou.unholdRequest'), + text: translate('iou.unholdExpense'), onSelected: () => changeMoneyRequestStatus(), }); } if (!isOnHold && (isRequestIOU || canModifyStatus)) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, - text: translate('iou.holdRequest'), + text: translate('iou.holdExpense'), onSelected: () => changeMoneyRequestStatus(), }); } @@ -196,7 +196,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, {isOnHold && } setIsDeleteModalVisible(false)} diff --git a/src/components/MoneyRequestSkeletonView.tsx b/src/components/MoneyRequestSkeletonView.tsx index e11e7bcecc07..400e3c9534d7 100644 --- a/src/components/MoneyRequestSkeletonView.tsx +++ b/src/components/MoneyRequestSkeletonView.tsx @@ -13,8 +13,8 @@ function MoneyRequestSkeletonView() { animate width={styles.w100.width} height={variables.moneyRequestSkeletonHeight} - backgroundColor={theme.borderLighter} - foregroundColor={theme.border} + backgroundColor={theme.skeletonLHNIn} + foregroundColor={theme.skeletonLHNOut} > ; - /** Unit and rate used for if the money request is a distance request */ + /** Unit and rate used for if the expense is a distance expense */ mileageRate: OnyxEntry; }; @@ -73,7 +73,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Callback to inform parent modal of success */ onConfirm?: (selectedParticipants: Participant[]) => void; - /** Callback to parent modal to send money */ + /** Callback to parent modal to pay someone */ onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; /** Callback to inform a participant is selected */ @@ -112,7 +112,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Selected participants from MoneyRequestModal with login / accountID */ selectedParticipants: Participant[]; - /** Payee of the money request with login */ + /** Payee of the expense with login */ payeePersonalDetails?: OnyxTypes.PersonalDetails; /** Can the participants be modified or not */ @@ -139,16 +139,16 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** List styles for OptionsSelector */ listStyles?: StyleProp; - /** Transaction that represents the money request */ + /** Transaction that represents the expense */ transaction?: OnyxEntry; - /** Whether the money request is a distance request */ + /** Whether the expense is a distance expense */ isDistanceRequest?: boolean; - /** Whether the money request is a scan request */ + /** Whether the expense is a scan expense */ isScanRequest?: boolean; - /** Whether we're editing a split bill */ + /** Whether we're editing a split expense */ isEditingSplitBill?: boolean; /** Whether we should show the amount, date, and merchant fields. */ @@ -236,7 +236,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - // Do not hide fields in case of send money request + // Do not hide fields in case of paying someone const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; @@ -354,7 +354,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ [iouAmount, iouCurrencyCode], ); - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again + // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); } @@ -364,14 +364,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (isTypeTrackExpense) { text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { - text = translate('iou.split'); + text = translate('iou.splitExpense'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.request'); + text = translate('iou.submitExpense'); if (iouAmount !== 0) { - text = translate('iou.requestAmount', {amount: formattedAmount}); + text = translate('iou.submitAmount', {amount: formattedAmount}); } } else { - const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.requestAmount'; + const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; text = translate(translationKey, {amount: formattedAmount}); } return [ @@ -552,7 +552,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ Log.info(`[IOU] Sending money via: ${paymentMethod}`); onSendMoney?.(paymentMethod); } else { - // validate the amount for distance requests + // validate the amount for distance expenses const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); @@ -907,7 +907,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing source={resolvedThumbnail || resolvedReceiptImage || ''} // AuthToken is required when retrieving the image from the server - // but we don't need it to load the blob:// or file:// image when starting a money request / split bill + // but we don't need it to load the blob:// or file:// image when starting an expense/split // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!!receiptThumbnail} fileExtension={fileExtension} diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 436f4c147931..7bbd3e344c3f 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -184,7 +184,7 @@ function BaseOptionsList( option={item} showTitleTooltip={showTitleTooltip} hoverStyle={optionHoveredStyle} - optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset} + optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)} onSelectRow={onSelectRow} isSelected={isSelected} showSelectedState={canSelectMultipleOptions} diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index b7180e6281b4..7f23da965f39 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -22,7 +22,7 @@ type Section = { type SectionWithIndexOffset = Section & { /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; + indexOffset?: number; }; type OptionsListProps = { diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 0ad32f18659b..cbc9e1352f21 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -35,7 +35,12 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct onPress={() => { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? ''); const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? ''); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, isVisibleAction && !isOffline ? parentReportActionID : undefined)); + // Pop the thread report screen before navigating to the chat report. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); + if (isVisibleAction && !isOffline) { + // Pop the chat report screen before navigating to the linked report action. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID)); + } }} accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})} role={CONST.ROLE.LINK} diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 7d9ba2697c7a..4f91b2084b45 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -37,7 +37,7 @@ type MoneyRequestActionProps = MoneyRequestActionOnyxProps & { /** The ID of the associated chatReport */ chatReportID: string; - /** The ID of the associated request report */ + /** The ID of the associated expense report */ requestReportID: string; /** The ID of the current report */ @@ -114,10 +114,8 @@ function MoneyRequestAction({ let message: TranslationPaths; if (isReversedTransaction) { message = 'parentReportAction.reversedTransaction'; - } else if (isTrackExpenseAction) { - message = 'parentReportAction.deletedExpense'; } else { - message = 'parentReportAction.deletedRequest'; + message = 'parentReportAction.deletedExpense'; } return ${translate(message)}`} />; } diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 97287e64b829..8994d456904a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -101,7 +101,7 @@ function MoneyRequestPreviewContent({ /* Show the merchant for IOUs and expenses only if: - the merchant is not empty, is custom, or is not related to scanning smartscan; - - the request is not a distance request with a pending route and amount = 0 - in this case, + - the expense is not a distance expense with a pending route and amount = 0 - in this case, the merchant says: "Route pending...", which is already shown in the amount field; */ const shouldShowMerchant = diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index 3b3eda4ec30a..0e3eb37ce6e3 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -53,7 +53,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** Extra styles to pass to View wrapper */ containerStyles?: StyleProp; - /** True if this is this IOU is a split instead of a 1:1 request */ + /** True if this IOU has a type of split */ isBillSplit: boolean; /** Whether this IOU is a track expense */ @@ -62,7 +62,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** True if the IOU Preview card is hovered */ isHovered?: boolean; - /** Whether or not an IOU report contains money requests in a different currency + /** Whether or not an IOU report contains expenses in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ shouldShowPendingConversionMessage?: boolean; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 73fc7e9bae6e..c5cad0eccdeb 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -132,7 +132,7 @@ function MoneyRequestView({ ? transaction && TransactionUtils.getDefaultTaxName(taxRates, transaction) : transactionTaxCode && TransactionUtils.getTaxName(taxRates?.taxes, transactionTaxCode)); - // Flags for allowing or disallowing editing a money request + // Flags for allowing or disallowing editing an expense const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); const isCancelled = moneyRequestReport && moneyRequestReport.isCancelledIOU; @@ -220,7 +220,7 @@ function MoneyRequestView({ const getErrorForField = useCallback( (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => { - // Checks applied when creating a new money request + // Checks applied when creating a new expense // NOTE: receipt field can return multiple violations, so we need to handle it separately const fieldChecks: Partial> = { amount: { diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index ee8cb0849ca0..e2bcce9b9f1b 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -30,7 +30,7 @@ type ReportActionItemImagesProps = { /** * This component displays a row of images in a report action item like a card, such - * as report previews or money request previews which contain receipt images. The maximum of images + * as report previews or expense previews which contain receipt images. The maximum of images * shown in this row is dictated by the size prop, which, if not passed, is just the number of images. * Otherwise, if size is passed and the number of images is over size, we show a small overlay on the * last image of how many additional images there are. If passed, total prop can be used to change how this diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 190343e48abd..d14d2df1bb43 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -36,7 +36,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; type ReportPreviewOnyxProps = { - /** The policy tied to the money request report */ + /** The policy tied to the expense report */ policy: OnyxEntry; /** ChatReport associated with iouReport */ @@ -216,11 +216,11 @@ function ReportPreview({ const shouldShowRBR = !iouSettled && hasErrors; /* - Show subtitle if at least one of the money requests is not being smart scanned, and either: - - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; - - There is only one money request, it has a receipt and is not being smart scanned – in this case, the request merchant or description is shown; + Show subtitle if at least one of the expenses is not being smart scanned, and either: + - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown; + - There is only one expense, it has a receipt and is not being smart scanned – in this case, the expense merchant or description is shown; - * There is an edge case when there is only one distance request with a pending route and amount = 0. + * There is an edge case when there is only one distance expense with a pending route and amount = 0. In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field. */ const shouldShowSingleRequestMerchantOrDescription = @@ -237,7 +237,7 @@ function ReportPreview({ } return { isSupportTextHtml: false, - supportText: translate('iou.requestCount', { + supportText: translate('iou.expenseCount', { count: numberOfRequests - numberOfScanningReceipts - numberOfPendingRequests, scanningReceipts: numberOfScanningReceipts, pendingReceipts: numberOfPendingRequests, diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx index e85a2e708feb..e1b36713592f 100644 --- a/src/components/ReportActionItem/TaskAction.tsx +++ b/src/components/ReportActionItem/TaskAction.tsx @@ -1,3 +1,4 @@ +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -15,10 +16,15 @@ type TaskActionProps = { function TaskAction({action}: TaskActionProps) { const styles = useThemeStyles(); const message = TaskUtils.getTaskReportActionMessage(action); + const parser = new ExpensiMark(); return ( - {message.html ? ${message.html}`} /> : {message.text}} + {message.html ? ( + ${parser.replace(message.html)}`} /> + ) : ( + {message.text} + )} ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 62f098e76228..a5bdf46450ae 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -52,6 +52,7 @@ function BaseSelectionList( onConfirm, headerContent, footerContent, + listFooterContent, showScrollIndicator = true, showLoadingPlaceholder = false, showConfirmButton = false, @@ -294,7 +295,7 @@ function BaseSelectionList( * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ - const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { + const getItemLayout = (data: Array>> | null, flatDataArrayIndex: number) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -313,6 +314,10 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (section.CustomSectionHeader) { + return ; + } + if (!section.title || isEmptyObject(section.data)) { return null; } @@ -329,7 +334,7 @@ function BaseSelectionList( }; const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { - const normalizedIndex = index + section.indexOffset; + const normalizedIndex = index + (section?.indexOffset ?? 0); const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? '')); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. @@ -603,7 +608,7 @@ function BaseSelectionList( testID="selection-list" onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} - ListFooterComponent={ShowMoreButtonInstance} + ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} /> {children} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 119634c61fd8..9d1d9e15ca0b 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -12,6 +12,11 @@ import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; +type TRightHandSideComponent = { + /** Component to display on the right side */ + rightHandSideComponent?: ((item: TItem) => ReactElement | null | undefined) | ReactElement | null; +}; + type CommonListItemProps = { /** Whether this item is focused (for arrow key controls) */ isFocused?: boolean; @@ -34,9 +39,6 @@ type CommonListItemProps = { /** Callback to fire when an error is dismissed */ onDismissError?: (item: TItem) => void; - /** Component to display on the right side */ - rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; - /** Styles for the pressable component */ pressableStyle?: StyleProp; @@ -54,7 +56,7 @@ type CommonListItemProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; -}; +} & TRightHandSideComponent; type ListItem = { /** Text to display */ @@ -185,12 +187,12 @@ type Section = { type SectionWithIndexOffset = Section & { /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; + indexOffset?: number; }; type BaseSelectionListProps = Partial & { /** Sections for the section list */ - sections: Array>> | typeof CONST.EMPTY_ARRAY; + sections: Array> | typeof CONST.EMPTY_ARRAY; /** Default renderer for every item in the list */ ListItem: ValidListItem; @@ -282,6 +284,9 @@ type BaseSelectionListProps = Partial & { /** Custom content to display in the footer */ footerContent?: ReactNode; + /** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */ + listFooterContent?: React.JSX.Element | null; + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ shouldUseDynamicMaxToRenderPerBatch?: boolean; @@ -294,9 +299,6 @@ type BaseSelectionListProps = Partial & { /** Whether focus event should be delayed */ shouldDelayFocus?: boolean; - /** Component to display on the right side of each child */ - rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; - /** Whether to show the loading indicator for new options */ isLoadingNewOptions?: boolean; @@ -323,7 +325,7 @@ type BaseSelectionListProps = Partial & { * When false, the list will render immediately and scroll to the bottom which works great for small lists. */ shouldHideListOnInitialRender?: boolean; -}; +} & TRightHandSideComponent; type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; @@ -344,7 +346,11 @@ type FlattenedSectionsReturn = { type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; -type SectionListDataType = SectionListData>; +type ExtendedSectionListData> = SectionListData & { + CustomSectionHeader?: ({section}: {section: TSection}) => ReactElement; +}; + +type SectionListDataType = ExtendedSectionListData>; export type { BaseSelectionListProps, diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 84c57ae381e3..f56c4dd1a863 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -180,13 +180,13 @@ function SettlementButton({ }; const canUseWallet = !isExpenseReport && currency === CONST.CURRENCY.USD; - // Only show the Approve button if the user cannot pay the request + // Only show the Approve button if the user cannot pay the expense if (shouldHidePaymentOptions && shouldShowApproveButton) { return [approveButtonOption]; } // To achieve the one tap pay experience we need to choose the correct payment type as default. - // If the user has previously chosen a specific payment option or paid for some request or expense, + // If the user has previously chosen a specific payment option or paid for some expense, // let's use the last payment method or use default. const paymentMethod = nvpLastPaymentMethod?.[policyID] ?? ''; if (canUseWallet) { diff --git a/src/components/TagPicker/index.tsx b/src/components/TagPicker/index.tsx index f968af4f6030..97cd9aa5c691 100644 --- a/src/components/TagPicker/index.tsx +++ b/src/components/TagPicker/index.tsx @@ -32,7 +32,7 @@ type TagPickerProps = TagPickerOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string; - /** The selected tag of the money request */ + /** The selected tag of the expense */ selectedTag: string; /** The name of tag list we are getting tags for */ diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js index 7eb1b776358c..f951837503f3 100644 --- a/src/components/transactionPropTypes.js +++ b/src/components/transactionPropTypes.js @@ -39,7 +39,7 @@ export default PropTypes.shape({ /** The text of the comment */ comment: PropTypes.string, - /** The waypoints defining the distance request */ + /** The waypoints defining the distance expense */ waypoints: PropTypes.shape({ /** The latitude of the waypoint */ lat: PropTypes.number, diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index 21c8d02e9194..b1f430e232e4 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -49,6 +49,10 @@ function useMarkdownStyle(message: string | null = null): MarkdownStyle { color: theme.mentionText, backgroundColor: theme.mentionBG, }, + mentionReport: { + color: theme.mentionText, + backgroundColor: theme.mentionBG, + }, }), [theme, emojiFontSize], ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0ae49fee7398..dd09a0c470fd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -50,6 +50,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, + PaySomeoneParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -418,7 +419,7 @@ export default { }, login: { hero: { - header: 'Split bills, request payments, and chat with friends.', + header: 'Manage spend, split expenses, and chat with your team.', body: 'Welcome to the future of Expensify, your new go-to place for financial collaboration with friends and teammates alike.', }, }, @@ -470,14 +471,9 @@ export default { copyEmailToClipboard: 'Copy email to clipboard', markAsUnread: 'Mark as unread', markAsRead: 'Mark as read', - editAction: ({action}: EditActionParams) => - `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, - deleteAction: ({action}: DeleteActionParams) => - `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, - deleteConfirmation: ({action}: DeleteConfirmationParams) => - `Are you sure you want to delete this ${ - action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment' - }?`, + editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, + deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, + deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', joinThread: 'Join thread', @@ -505,7 +501,7 @@ export default { beginningOfChatHistory: 'This is the beginning of your chat with ', beginningOfChatHistoryPolicyExpenseChatPartOne: 'Collaboration between ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' and ', - beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, request money and settle up.', + beginningOfChatHistoryPolicyExpenseChatPartThree: ' starts here! 🎉 This is the place to chat, submit expenses and settle up.', beginningOfChatHistorySelfDM: 'This is your personal space. Use it for notes, tasks, drafts, and reminders.', chatWithAccountManager: 'Chat with your account manager here', sayHello: 'Say hello!', @@ -513,9 +509,9 @@ export default { welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`, usePlusButton: ({additionalText}: UsePlusButtonParams) => `\nYou can also use the + button to ${additionalText}, or assign a task!`, iouTypes: { - send: 'send money', - split: 'split a bill', - request: 'request money', + send: 'pay expenses', + split: 'split an expense', + request: 'submit an expense', // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'track an expense', }, @@ -595,15 +591,15 @@ export default { quickAction: { scanReceipt: 'Scan receipt', recordDistance: 'Record distance', - requestMoney: 'Request money', - splitBill: 'Split bill', + requestMoney: 'Submit expense', + splitBill: 'Split expense', splitScan: 'Split receipt', splitDistance: 'Split distance', - sendMoney: 'Send money', + sendMoney: 'Pay someone', assignTask: 'Assign task', header: 'Quick action', - trackManual: 'Track manual', - trackScan: 'Track scan', + trackManual: 'Track expense', + trackScan: 'Track receipt', trackDistance: 'Track distance', }, iou: { @@ -616,14 +612,13 @@ export default { card: 'Card', original: 'Original', split: 'Split', - addToSplit: 'Add to split', - splitBill: 'Split bill', - request: 'Request', + splitExpense: 'Split expense', + paySomeone: ({name}: PaySomeoneParams) => `Pay ${name ?? 'someone'}`, + expense: 'Expense', categorize: 'Categorize', share: 'Share', participants: 'Participants', - requestMoney: 'Request money', - sendMoney: 'Send money', + submitExpense: 'Submit expense', trackExpense: 'Track expense', pay: 'Pay', cancelPayment: 'Cancel payment', @@ -642,20 +637,20 @@ export default { receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", receiptScanningFailed: 'Receipt scanning failed. Enter the details manually.', transactionPendingText: 'It takes a few days from the date the card was used for the transaction to post.', - requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => - `${count} ${Str.pluralize('request', 'requests', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ + expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => + `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' }`, - deleteRequest: 'Delete request', - deleteConfirmation: 'Are you sure that you want to delete this request?', + deleteExpense: 'Delete expense', + deleteConfirmation: 'Are you sure that you want to delete this expense?', settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), nextStep: 'Next Steps', finished: 'Finished', - requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, - requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`, + submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`, + submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `submitted ${formattedAmount}${comment ? ` for ${comment}` : ''}`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, @@ -680,16 +675,16 @@ export default { paidWithExpensifyWithAmount: ({payer, amount}: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} using Expensify`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", - changedTheRequest: 'changed the request', + changedTheExpense: 'changed the expense', setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, - threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'request'}`, + threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`, threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`, - threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, + threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, tagSelection: 'Select a tag to better organize your spend.', categorySelection: 'Select a category to better organize your spend.', error: { @@ -698,36 +693,36 @@ export default { invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`, invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', - genericCreateFailureMessage: 'Unexpected error requesting money, please try again later', + genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.', receiptFailureMessage: "The receipt didn't upload. ", saveFileMessage: 'Download the file ', loseFileMessage: 'or dismiss this error and lose it', - genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', - genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', + genericDeleteFailureMessage: 'Unexpected error deleting this expense, please try again later', + genericEditFailureMessage: 'Unexpected error editing this expense, please try again later', genericSmartscanFailureMessage: 'Transaction is missing fields', duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints', atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses', - splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.', + splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.', invalidMerchant: 'Please enter a correct merchant.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', hold: 'Hold', - holdRequest: 'Hold request', - unholdRequest: 'Unhold request', - heldRequest: 'held this request', - unheldRequest: 'unheld this request', - explainHold: "Explain why you're holding this request.", + holdExpense: 'Hold expense', + unholdExpense: 'Unhold expense', + heldExpense: 'held this expense', + unheldExpense: 'unheld this expense', + explainHold: "Explain why you're holding this expense.", reason: 'Reason', holdReasonRequired: 'A reason is required when holding.', - requestOnHold: 'This request was put on hold. Review the comments for next steps.', + expenseOnHold: 'This expense was put on hold. Review the comments for next steps.', confirmApprove: 'Confirm what to approve', confirmApprovalAmount: 'Approve the entire report total or only the amount not on hold.', confirmPay: 'Confirm what to pay', confirmPayAmount: 'Pay all out-of-pocket spend or only the amount not on hold.', payOnly: 'Pay only', approveOnly: 'Approve only', - holdEducationalTitle: 'This request is on', + holdEducationalTitle: 'This expense is on', whatIsHoldTitle: 'What is hold?', whatIsHoldExplain: 'Hold is our way of streamlining financial collaboration. "Reject" is so harsh!', holdIsTemporaryTitle: 'Hold is usually temporary', @@ -938,8 +933,7 @@ export default { reasonForLeavingPrompt: 'We’d hate to see you go! Would you kindly tell us why, so we can improve?', enterMessageHere: 'Enter message here', closeAccountWarning: 'Closing your account cannot be undone.', - closeAccountPermanentlyDeleteData: - 'This will permanently delete all of your unsubmitted expense data and will cancel and decline any outstanding money requests. Are you sure you want to delete the account?', + closeAccountPermanentlyDeleteData: 'Are you sure you want to delete your account? This will permanently delete any outstanding expenses.', enterDefaultContactToConfirm: 'Please type your default contact method to confirm you wish to close your account. Your default contact method is:', enterDefaultContact: 'Enter your default contact method', defaultContact: 'Default contact method:', @@ -1267,19 +1261,6 @@ export default { }, chooseThemeBelowOrSync: 'Choose a theme below, or sync with your device settings.', }, - signInPage: { - expensifyDotCash: 'New Expensify', - theCode: 'the code', - openJobs: 'open jobs', - heroHeading: 'Split bills\nand chat with friends.', - heroDescription: { - phrase1: "Money talks. And now that chat and payments are in one place, it's also easy. Your payments get to you as fast as you can get your point across.", - phrase2: 'The New Expensify is open source. View', - phrase3: 'the code', - phrase4: 'View', - phrase5: 'open jobs', - }, - }, termsOfUse: { phrase1: 'By logging in, you agree to the', phrase2: 'Terms of Service', @@ -1345,7 +1326,7 @@ export default { [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer', [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses", [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget personal spend', - [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends', + [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chat and split expenses with friends', [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: "I'm just looking around", }, error: { @@ -2540,7 +2521,6 @@ export default { parentReportAction: { deletedReport: '[Deleted report]', deletedMessage: '[Deleted message]', - deletedRequest: '[Deleted request]', deletedExpense: '[Deleted expense]', reversedTransaction: '[Reversed transaction]', deletedTask: '[Deleted task]', @@ -2589,7 +2569,7 @@ export default { decline: 'Decline', }, actionableMentionTrackExpense: { - request: 'Request someone to pay it', + submit: 'Submit it to someone', categorize: 'Categorize it', share: 'Share it with my accountant', nothing: 'Nothing for now', @@ -2668,27 +2648,27 @@ export default { body: `Get paid to talk to your friends! Start a chat with a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: { - buttonText1: 'Request money, ', + buttonText1: 'Submit expense, ', buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`, - header: `Request money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `It pays to get paid! Request money from a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, + header: `Submit an expense, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, + body: `It pays to get paid! Submit an expense to a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: { - buttonText1: 'Send money, ', + buttonText1: 'Pay Someone, ', buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`, - header: `Send money, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `You gotta send money to make money! Send money to a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, + header: `Pay Someone, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, + body: `You gotta spend money to make money! Pay someone with Expensify and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: { buttonText1: 'Invite a friend, ', buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`, header: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `Be the first to chat, send or request money, split a bill, or share your invite link with a friend, and you'll get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. You can post your invite link on social media, too!`, + body: `Chat, pay, submit, or split an expense with a friend and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. Otherwise, just share your invite link!`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]: { buttonText1: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`, header: `Get $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `Be the first to chat, send or request money, split a bill, or share your invite link with a friend, and you'll get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. You can post your invite link on social media, too!`, + body: `Chat, pay, submit, or split an expense with a friend and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer. Otherwise, just share your invite link!`, }, copyReferralLink: 'Copy invite link', }, @@ -2696,7 +2676,7 @@ export default { [CONST.INTRO_CHOICES.TRACK]: 'Track business spend for taxes', [CONST.INTRO_CHOICES.SUBMIT]: 'Get paid back by my employer', [CONST.INTRO_CHOICES.MANAGE_TEAM]: "Manage my team's expenses", - [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends', + [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split expenses with friends', welcomeMessage: 'Welcome to Expensify', welcomeSubtitle: 'What would you like to do?', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 8955841559fe..b526a2785495 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -49,6 +49,7 @@ import type { PayerPaidAmountParams, PayerPaidParams, PayerSettledParams, + PaySomeoneParams, RemovedTheRequestParams, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, @@ -409,7 +410,7 @@ export default { }, login: { hero: { - header: 'Divida las facturas, solicite pagos y chatee con sus amigos.', + header: 'Gestiona, divide gastos y chatea con tu equipo.', body: 'Bienvenido al futuro de Expensify, tu nuevo lugar de referencia para la colaboración financiera con amigos y compañeros de equipo por igual.', }, }, @@ -461,18 +462,10 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => - `Editar ${ - action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' - }`, - deleteAction: ({action}: DeleteActionParams) => - `Eliminar ${ - action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' - }`, + editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`, + deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => - `¿Estás seguro de que quieres eliminar esta ${ - action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' - }`, + `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', @@ -501,7 +494,7 @@ export default { beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ', - beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear, pedir dinero y pagar.', + beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! 🎉 Este es el lugar donde chatear y presentar o pagar gastos.', beginningOfChatHistorySelfDM: 'Este es tu espacio personal. Úsalo para notas, tareas, borradores y recordatorios.', chatWithAccountManager: 'Chatea con tu gestor de cuenta aquí', sayHello: '¡Saluda!', @@ -509,9 +502,9 @@ export default { welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `¡Bienvenido a ${roomName}!`, usePlusButton: ({additionalText}: UsePlusButtonParams) => `\n¡También puedes usar el botón + de abajo para ${additionalText}, o asignar una tarea!`, iouTypes: { - send: 'enviar dinero', - split: 'dividir una factura', - request: 'pedir dinero', + send: 'pagar gastos', + split: 'dividir un gasto', + request: 'presentar un gasto', // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'rastrear un gasto', }, @@ -591,11 +584,11 @@ export default { quickAction: { scanReceipt: 'Escanear recibo', recordDistance: 'Grabar distancia', - requestMoney: 'Solicitar dinero', - splitBill: 'Dividir cuenta', + requestMoney: 'Presentar gasto', + splitBill: 'Dividir gasto', splitScan: 'Dividir recibo', splitDistance: 'Dividir distancia', - sendMoney: 'Enviar dinero', + sendMoney: 'Pagar a alguien', assignTask: 'Assignar tarea', header: 'Acción rápida', trackManual: 'Crear gasto', @@ -612,14 +605,13 @@ export default { card: 'Tarjeta', original: 'Original', split: 'Dividir', - addToSplit: 'Añadir para dividir', - splitBill: 'Dividir factura', - request: 'Solicitar', + splitExpense: 'Dividir gasto', + expense: 'Gasto', categorize: 'Categorizar', share: 'Compartir', participants: 'Participantes', - requestMoney: 'Pedir dinero', - sendMoney: 'Enviar dinero', + submitExpense: 'Presentar gasto', + paySomeone: ({name}: PaySomeoneParams) => `Pagar a ${name ?? 'alguien'}`, trackExpense: 'Seguimiento de gastos', pay: 'Pagar', cancelPayment: 'Cancelar el pago', @@ -638,11 +630,11 @@ export default { receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.', transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', - requestCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => - `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ + expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => + `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' }`, - deleteRequest: 'Eliminar solicitud', + deleteExpense: 'Eliminar gasto', deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', @@ -650,9 +642,9 @@ export default { payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), nextStep: 'Pasos Siguientes', finished: 'Finalizado', - requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, - requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`, - trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, + submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `realizó un seguimiento de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, @@ -676,7 +668,7 @@ export default { paidWithExpensifyWithAmount: ({payer, amount}: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount} con Expensify`, noReimbursableExpenses: 'El importe de este informe no es válido', pendingConversionMessage: 'El total se actualizará cuando estés online', - changedTheRequest: 'cambió la solicitud', + changedTheExpense: 'cambió el gasto', setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, @@ -685,9 +677,9 @@ export default { `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, - threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Solicitud de ${formattedAmount}`}`, + threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`, threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`, - threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, + threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, tagSelection: 'Selecciona una etiqueta para organizar mejor tu dinero.', categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.', error: { @@ -696,28 +688,28 @@ export default { invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`, invalidSplit: 'La suma de las partes no equivale al importe total', other: 'Error inesperado, por favor inténtalo más tarde', - genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde', + genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.', receiptFailureMessage: 'El recibo no se subió. ', saveFileMessage: 'Guarda el archivo ', loseFileMessage: 'o descarta este error y piérdelo', - genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', - genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', + genericDeleteFailureMessage: 'Error inesperado al eliminar este gasto. Por favor, inténtalo más tarde', + genericEditFailureMessage: 'Error inesperado al editar este gasto. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados', atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes', - splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', + splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', - holdRequest: 'Bloquear solicitud', - unholdRequest: 'Desbloquear solicitud', - heldRequest: 'bloqueó esta solicitud', - unheldRequest: 'desbloqueó esta solicitud', + holdExpense: 'Bloquear gasto', + unholdExpense: 'Desbloquear gasto', + heldExpense: 'bloqueó este gasto', + unheldExpense: 'desbloqueó este gasto', explainHold: 'Explica la razón para bloquear esta solicitud.', reason: 'Razón', holdReasonRequired: 'Se requiere una razón para bloquear.', - requestOnHold: 'Este solicitud está bloqueada. Revisa los comentarios para saber como proceder.', + expenseOnHold: 'Este gasto está bloqueado. Revisa los comentarios para saber como proceder.', confirmApprove: 'Confirma que quieres aprobar', confirmApprovalAmount: 'Aprobar el total o solo la parte no bloqueada.', confirmPay: 'Confirma que quieres pagar', @@ -725,7 +717,7 @@ export default { payOnly: 'Solo pagar', approveOnly: 'Solo aprobar', hold: 'Bloqueada', - holdEducationalTitle: 'Esta solicitud está', + holdEducationalTitle: 'Este gasto está', whatIsHoldTitle: '¿Qué es Bloquear?', whatIsHoldExplain: 'Bloquear es nuestra forma de agilizar la colaboración financiera. ¡"Rechazar" es tan duro!', holdIsTemporaryTitle: 'Bloquear suele ser temporal', @@ -936,8 +928,7 @@ export default { reasonForLeavingPrompt: '¡Lamentamos verte partir! ¿Serías tan amable de decirnos por qué, para que podamos mejorar?', enterMessageHere: 'Escribe aquí tu mensaje', closeAccountWarning: 'Una vez cerrada tu cuenta no se puede revertir.', - closeAccountPermanentlyDeleteData: - 'Esta acción eliminará permanentemente toda la información de tus gastos no enviados y cancelará o rechazará cualquier solicitud de dinero pendiente. ¿Estás seguro de que quieres eliminar tu cuenta?', + closeAccountPermanentlyDeleteData: '¿Estás seguro de que quieres eliminar tu cuenta? Esta acción eliminará permanentemente toda la información de cualquier gasto pendiente.', enterDefaultContactToConfirm: 'Por favor, escribe tu método de contacto predeterminado para confirmar que deseas eliminar tu cuenta. Tu método de contacto predeterminado es:', enterDefaultContact: 'Tu método de contacto predeterminado', defaultContact: 'Método de contacto predeterminado:', @@ -1269,19 +1260,6 @@ export default { }, chooseThemeBelowOrSync: 'Elige un tema a continuación o sincronízalo con los ajustes de tu dispositivo.', }, - signInPage: { - expensifyDotCash: 'Nuevo Expensify', - theCode: 'el código', - openJobs: 'trabajos disponibles', - heroHeading: 'Dividir cuentas\ny chatear con amigos.', - heroDescription: { - phrase1: 'El dinero habla. Y ahora que el chat y los pagos están en un solo lugar, también es fácil. Tus pagos te llegan tan rápido como puedes hacer llegar tu mensaje', - phrase2: 'Nuevo Expensify es de código abierto. Vista', - phrase3: 'el código', - phrase4: 'Vista', - phrase5: 'vacantes', - }, - }, termsOfUse: { phrase1: 'Al iniciar sesión, estás accediendo a los', phrase2: 'Términos de Servicio', @@ -1347,7 +1325,7 @@ export default { [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa', [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo', [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar los gastos personales', - [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chatea y divide cuentas con tus amigos', + [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chatea y divide gastos con tus amigos', [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: 'Sólo estoy mirando', }, error: { @@ -3032,7 +3010,6 @@ export default { parentReportAction: { deletedReport: '[Informe eliminado]', deletedMessage: '[Mensaje eliminado]', - deletedRequest: '[Solicitud eliminada]', deletedExpense: '[Gasto eliminado]', reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', @@ -3059,7 +3036,7 @@ export default { decline: 'Rechazar', }, actionableMentionTrackExpense: { - request: 'Pedirle a alguien que lo pague', + submit: 'Pedirle a alguien que lo pague', categorize: 'Categorizarlo', share: 'Compartirlo con mi contador', nothing: 'Por ahora, nada', @@ -3161,27 +3138,27 @@ export default { body: `¡Gana dinero por hablar con tus amigos! Inicia un chat con una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: { - buttonText1: 'Pide dinero, ', + buttonText1: 'Presentar gasto, ', buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - header: `Pide dinero y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `¡Vale la pena cobrar! Pide dinero a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, + header: `Presenta un gasto y consigue $${CONST.REFERRAL_PROGRAM.REVENUE}`, + body: `¡Vale la pena cobrar! Envia un gasto a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: { - buttonText1: 'Envía dinero, ', + buttonText1: 'Pagar a alguien, ', buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - header: `Envía dinero y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `¡Hay que enviar dinero para ganar dinero! Envía dinero a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, + header: `Paga a alguien y recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, + body: `¡Hay que gastar dinero para ganar dinero! Paga a alguien con Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: { buttonText1: 'Invita a un amigo y ', buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, header: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `Sé el primero en chatear, enviar o pedir dinero, dividir una factura o compartir tu enlace de invitación con un amigo, y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`, + body: `Chatea, paga, presenta y divide gastos con un amigo y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]: { buttonText1: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, header: `Recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, - body: `Sé el primero en chatear, enviar o pedir dinero, dividir una factura o compartir tu enlace de invitación con un amigo, y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`, + body: `Chatea, paga, presenta y divide gastos con un amigo y recibirás $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se convierta en cliente. También puedes publicar tu enlace de invitación en las redes sociales.`, }, copyReferralLink: 'Copiar enlace de invitación', }, diff --git a/src/languages/types.ts b/src/languages/types.ts index c365363f84af..30b7f842db4c 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -247,6 +247,8 @@ type ViolationsTagOutOfPolicyParams = {tagName?: string}; type ViolationsTaxOutOfPolicyParams = {taxName?: string}; +type PaySomeoneParams = {name?: string}; + type TaskCreatedActionParams = {title: string}; /* Translation Object types */ @@ -400,4 +402,5 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, + PaySomeoneParams, }; diff --git a/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts b/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts new file mode 100644 index 000000000000..0ebaa58ef0d1 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyAccountingPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyAccountingPageParams = { + policyID: string; +}; + +export default OpenPolicyAccountingPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index cc3ec2767490..bfa89b5d3bd3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -212,3 +212,4 @@ export type {default as ConvertTrackedExpenseToRequestParams} from './ConvertTra export type {default as ShareTrackedExpenseParams} from './ShareTrackedExpenseParams'; export type {default as CategorizeTrackedExpenseParams} from './CategorizeTrackedExpenseParams'; export type {default as LeavePolicyParams} from './LeavePolicyParams'; +export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 3d32d27084c3..f91b694548ba 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -461,6 +461,7 @@ const READ_COMMANDS = { OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', + OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', } as const; type ReadCommand = ValueOf; @@ -502,6 +503,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; + [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 9a7e0a568627..12a240ae9041 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -79,7 +79,7 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string } /** - * @param hasRoute Whether the route exists for the distance request + * @param hasRoute Whether the route exists for the distance expense * @param distanceInMeters Distance traveled * @param unit Unit that should be used to display the distance * @param rate Expensable amount allowed per unit @@ -100,7 +100,7 @@ function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit } /** - * @param hasRoute Whether the route exists for the distance request + * @param hasRoute Whether the route exists for the distance expense * @param distanceInMeters Distance traveled * @param unit Unit that should be used to display the distance * @param rate Expensable amount allowed per unit @@ -133,12 +133,12 @@ function getDistanceMerchant( } /** - * Calculates the request amount based on distance, unit, and rate. + * Calculates the expense amount based on distance, unit, and rate. * * @param distance - The distance traveled in meters * @param unit - The unit of measurement for the distance - * @param rate - Rate used for calculating the request amount - * @returns The computed request amount (rounded) in "cents". + * @param rate - Rate used for calculating the expense amount + * @returns The computed expense amount (rounded) in "cents". */ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): number { const convertedDistance = convertDistanceUnit(distance, unit); diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 776e3de74f06..9d5b0be0d2e7 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -35,7 +35,7 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - [E2EConfig.TEST_NAMES.OpenSearchPage]: require('./tests/openSearchPageTest.e2e').default, + [E2EConfig.TEST_NAMES.OpenChatFinderPage]: require('./tests/openChatFinderPageTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/E2E/tests/openSearchPageTest.e2e.ts b/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts similarity index 82% rename from src/libs/E2E/tests/openSearchPageTest.e2e.ts rename to src/libs/E2E/tests/openChatFinderPageTest.e2e.ts index 86da851396f6..9d2b117a7044 100644 --- a/src/libs/E2E/tests/openSearchPageTest.e2e.ts +++ b/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts @@ -9,7 +9,7 @@ import ROUTES from '@src/ROUTES'; const test = () => { // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for search'); + console.debug('[E2E] Logging in for chat finder'); E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { @@ -19,24 +19,24 @@ const test = () => { ); } - console.debug('[E2E] Logged in, getting search metrics and submitting them…'); + console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…'); Performance.subscribeToMeasurements((entry) => { if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to search route…`); - Navigation.navigate(ROUTES.SEARCH); + console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`); + Navigation.navigate(ROUTES.CHAT_FINDER); return; } console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name !== CONST.TIMING.SEARCH_RENDER) { + if (entry.name !== CONST.TIMING.CHAT_FINDER_RENDER) { return; } console.debug(`[E2E] Submitting!`); E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Search Page TTI', + name: 'Open Chat Finder Page TTI', duration: entry.duration, }) .then(() => { diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 27af031d19a8..27eff132ef40 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -63,8 +63,8 @@ function calculateAmount(numberOfParticipants: number, total: number, currency: * For example: if user1 owes user2 $10, then we have: {ownerAccountID: user2, managerID: user1, total: $10 (a positive amount, owed to user2)} * If user1 requests $17 from user2, then we have: {ownerAccountID: user1, managerID: user2, total: $7 (still a positive amount, but now owed to user1)} * - * @param isDeleting - whether the user is deleting the request - * @param isUpdating - whether the user is updating the request + * @param isDeleting - whether the user is deleting the expense + * @param isUpdating - whether the user is updating the expense */ function updateIOUOwnerAndTotal>( iouReport: TReport, @@ -102,7 +102,7 @@ function updateIOUOwnerAndTotal>( } /** - * Returns whether or not an IOU report contains money requests in a different currency + * Returns whether or not an IOU report contains expenses in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 0d961ea27115..d73771734636 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -253,7 +253,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr buildMessageFragmentForValue( reportActionOriginalMessage?.billable ?? '', reportActionOriginalMessage?.oldBillable ?? '', - Localize.translateLocal('iou.request'), + Localize.translateLocal('iou.expense'), true, setFragments, removalFragments, @@ -266,7 +266,7 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); if (message === '') { - return Localize.translateLocal('iou.changedTheRequest'); + return Localize.translateLocal('iou.changedTheExpense'); } return `${message.substring(1, message.length)}`; } diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index 1a573ce74628..ec934cb87888 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -78,14 +78,14 @@ function replaceAllDigits(text: string, convertFn: (char: string) => string): st } /** - * Check if distance request or not + * Check if distance expense or not */ function isDistanceRequest(iouType: ValueOf, selectedTab: OnyxEntry): boolean { return iouType === CONST.IOU.TYPE.REQUEST && selectedTab === CONST.TAB_REQUEST.DISTANCE; } /** - * Check if scan request or not + * Check if scan expense or not */ function isScanRequest(selectedTab: SelectedTabRequest): boolean { return selectedTab === CONST.TAB_REQUEST.SCAN; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index fde0202d3d2f..9157d7486c9e 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -238,7 +238,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { - Modal.close(Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.SEARCH))); + Modal.close(Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER))); }, shortcutsOverviewShortcutConfig.descriptionKey, shortcutsOverviewShortcutConfig.modifiers, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 470d06037c6d..a596acf0a3ac 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -4,6 +4,7 @@ import {createStackNavigator} from '@react-navigation/stack'; import React from 'react'; import type { AddPersonalBankAccountNavigatorParamList, + ChatFinderNavigatorParamList, DetailsNavigatorParamList, EditRequestNavigatorParamList, EnablePaymentsNavigatorParamList, @@ -22,7 +23,6 @@ import type { ReportSettingsNavigatorParamList, RoomInviteNavigatorParamList, RoomMembersNavigatorParamList, - SearchNavigatorParamList, SettingsNavigatorParamList, SignInNavigatorParamList, SplitDetailsNavigatorParamList, @@ -147,8 +147,8 @@ const RoomInviteModalStackNavigator = createModalStackNavigator require('../../../../pages/RoomInvitePage').default as React.ComponentType, }); -const SearchModalStackNavigator = createModalStackNavigator({ - [SCREENS.SEARCH_ROOT]: () => require('../../../../pages/SearchPage').default as React.ComponentType, +const ChatFinderModalStackNavigator = createModalStackNavigator({ + [SCREENS.CHAT_FINDER_ROOT]: () => require('../../../../pages/ChatFinderPage').default as React.ComponentType, }); const NewChatModalStackNavigator = createModalStackNavigator({ @@ -343,7 +343,7 @@ export { ReportDescriptionModalStackNavigator, RoomInviteModalStackNavigator, RoomMembersModalStackNavigator, - SearchModalStackNavigator, + ChatFinderModalStackNavigator, SettingsModalStackNavigator, SignInModalStackNavigator, SplitDetailsModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx index 8f76d8fbdd7b..159430a66a43 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx @@ -32,8 +32,8 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) { Navigation.navigate(ROUTES.SEARCH))} + onPress={Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER))} > | undefined): NavigationPartialRoute | undefined { - const bottomTabNavigatorRoute = state?.routes[0]; + const bottomTabNavigatorRoute = state?.routes.findLast((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR); // The bottomTabNavigatorRoute state may be empty if we just logged in. - if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== 'BottomTabNavigator' || bottomTabNavigatorRoute.state === undefined) { + if (!bottomTabNavigatorRoute || bottomTabNavigatorRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR || bottomTabNavigatorRoute.state === undefined) { return undefined; } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 85f0759a5f92..67ffcf43dece 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -71,9 +71,9 @@ const config: LinkingOptions['config'] = { [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { screens: { - [SCREENS.LEFT_MODAL.SEARCH]: { + [SCREENS.LEFT_MODAL.CHAT_FINDER]: { screens: { - [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH, + [SCREENS.CHAT_FINDER_ROOT]: ROUTES.CHAT_FINDER, }, }, [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ebc324945146..7dd2f274aa9e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -300,8 +300,8 @@ type NewChatNavigatorParamList = { }; }; -type SearchNavigatorParamList = { - [SCREENS.SEARCH_ROOT]: undefined; +type ChatFinderNavigatorParamList = { + [SCREENS.CHAT_FINDER_ROOT]: undefined; }; type DetailsNavigatorParamList = { @@ -614,7 +614,7 @@ type PrivateNotesNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.SEARCH]: NavigatorScreenParams; + [SCREENS.LEFT_MODAL.CHAT_FINDER]: NavigatorScreenParams; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: NavigatorScreenParams; }; @@ -790,7 +790,7 @@ type AuthScreensParamList = SharedScreensParamList & { }; }; -type RootStackParamList = PublicScreensParamList & AuthScreensParamList & SearchNavigatorParamList; +type RootStackParamList = PublicScreensParamList & AuthScreensParamList & ChatFinderNavigatorParamList; type BottomTabName = keyof BottomTabNavigatorParamList; @@ -834,7 +834,7 @@ export type { ParticipantsNavigatorParamList, RoomMembersNavigatorParamList, RoomInviteNavigatorParamList, - SearchNavigatorParamList, + ChatFinderNavigatorParamList, NewChatNavigatorParamList, NewTaskNavigatorParamList, TeachersUniteNavigatorParamList, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index aa16d7b2dc5a..c11a1499a88f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -117,6 +117,7 @@ type TaxSection = { type CategoryTreeSection = CategorySectionBase & { data: OptionTree[]; + indexOffset?: number; }; type Category = { @@ -1023,11 +1024,13 @@ function getCategoryListSections( const numberOfEnabledCategories = enabledCategories.length; if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data, + indexOffset: data.length, }); return categorySections; @@ -1046,22 +1049,26 @@ function getCategoryListSections( }); }); + const data = getCategoryOptionTree(searchCategories, true); categorySections.push({ // "Search" section title: '', shouldShow: true, - data: getCategoryOptionTree(searchCategories, true), + data, + indexOffset: data.length, }); return categorySections; } if (selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data, + indexOffset: data.length, }); } @@ -1069,11 +1076,13 @@ function getCategoryListSections( const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1089,19 +1098,23 @@ function getCategoryListSections( if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); + const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); categorySections.push({ // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getCategoryOptionTree(cutRecentlyUsedCategories, true), + data, + indexOffset: data.length, }); } + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1707,7 +1720,7 @@ function getOptions( return; } - // In case user needs to add credit bank account, don't allow them to request more money from the workspace. + // In case user needs to add credit bank account, don't allow them to submit an expense from the workspace. if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) { return; } @@ -2356,4 +2369,4 @@ export { getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 32972a81bcb5..d4a2afafb420 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -20,7 +20,6 @@ import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/Rep import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import * as CollectionUtils from './CollectionUtils'; import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; @@ -59,16 +58,16 @@ Onyx.connect({ }, }); -const allReportActions: OnyxCollection = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = actions; + allReportActions = actions; }, }); @@ -195,7 +194,7 @@ function getParentReportAction(report: OnyxEntry | EmptyObject): ReportA if (!report?.parentReportID || !report.parentReportActionID) { return {}; } - return allReportActions?.[report.parentReportID]?.[report.parentReportActionID] ?? {}; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] ?? {}; } /** @@ -225,7 +224,7 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty /** * Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. */ -function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[]): string | null { +function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | null { // If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report. const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE) { @@ -250,7 +249,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (iouRequestTypes.includes(action.originalMessage.type) ?? []) && action.childReportID && - action.originalMessage.IOUTransactionID, + (Boolean(action.originalMessage.IOUTransactionID) || (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline))), ); // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report @@ -518,7 +517,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return false; } - // Ignore markedAsReimbursed action here since we're already display message that explains the request was paid + // Ignore markedAsReimbursed action here since we're already display message that explains the expense was paid // elsewhere in the IOU reportAction if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) { return false; @@ -593,7 +592,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo } function getLastVisibleAction(reportID: string, actionsToMerge: OnyxCollection = {}): OnyxEntry { - const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge ?? {}, true)); + const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { @@ -714,7 +713,7 @@ function getLatestReportActionFromOnyxData(onyxData: OnyxUpdate[] | null): OnyxE * Find the transaction associated with this reportAction, if one exists. */ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry, reportID?: string): string | null { - const reportAction = typeof reportActionOrID === 'string' ? allReportActions?.[reportID ?? '']?.[reportActionOrID] : reportActionOrID; + const reportAction = typeof reportActionOrID === 'string' ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]?.[reportActionOrID] : reportActionOrID; if (!reportAction || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { return null; } @@ -722,7 +721,7 @@ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry { - return allReportActions?.[reportID]?.[reportActionID] ?? null; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]?.[reportActionID] ?? null; } function getMostRecentReportActionLastModified(): string { @@ -769,7 +768,7 @@ function getMostRecentReportActionLastModified(): string { */ function getReportPreviewAction(chatReportID: string, iouReportID: string): OnyxEntry { return ( - Object.values(allReportActions?.[chatReportID] ?? {}).find( + Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] ?? {}).find( (reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && reportAction.originalMessage.linkedReportID === iouReportID, ) ?? null ); @@ -794,7 +793,7 @@ function isMessageDeleted(reportAction: OnyxEntry): boolean { } /** - * Returns the number of money requests associated with a report preview + * Returns the number of expenses associated with a report preview */ function getNumberOfMoneyRequests(reportPreviewAction: OnyxEntry): number { return reportPreviewAction?.childMoneyRequestCount ?? 0; @@ -823,7 +822,7 @@ function isTaskAction(reportAction: OnyxEntry): boolean { * If there are no visible actions left (including system messages), we can hide the report from view entirely */ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { - const reportActions = Object.values(fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge, true)); + const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action)); // Exclude the task system message and the created message @@ -832,7 +831,7 @@ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportAc } function getAllReportActions(reportID: string): ReportActions { - return allReportActions?.[reportID] ?? {}; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}; } /** @@ -997,7 +996,7 @@ function getMemberChangeMessagePlainText(reportAction: OnyxEntry): } /** - * Helper method to determine if the provided accountID has made a request on the specified report. + * Helper method to determine if the provided accountID has submitted an expense on the specified report. * * @param reportID * @param currentAccountID diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f31b4a780c5a..e1c7d8a3f287 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -55,7 +55,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import * as IOU from './actions/IOU'; import * as store from './actions/ReimbursementAccount/store'; -import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; @@ -537,16 +536,15 @@ Onyx.connect({ }, }); -const reportActionsByReport: OnyxCollection = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - - const reportID = CollectionUtils.extractCollectionItemID(key); - reportActionsByReport[reportID] = actions; + allReportActions = actions; }, }); @@ -1172,6 +1170,15 @@ function isJoinRequestInAdminRoom(report: OnyxEntry): boolean { if (!report) { return false; } + // If this policy isn't owned by Expensify, + // Account manager/guide should not have the workspace join request pinned to their LHN, + // since they are not a part of the company, and should not action it on their behalf. + if (report.policyID) { + const policy = getPolicy(report.policyID); + if (!PolicyUtils.isExpensifyTeam(policy.owner) && PolicyUtils.isExpensifyTeam(currentUserPersonalDetails?.login)) { + return false; + } + } return ReportActionsUtils.isActionableJoinRequestPending(report.reportID); } @@ -1320,7 +1327,7 @@ function isMoneyRequestReport(reportOrID: OnyxEntry | EmptyObject | stri * Checks if a report has only one transaction associated with it */ function isOneTransactionReport(reportID: string): boolean { - const reportActions = reportActionsByReport?.[reportID] ?? ([] as ReportAction[]); + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null; } @@ -1328,7 +1335,7 @@ function isOneTransactionReport(reportID: string): boolean { * Checks if a report is a transaction thread associated with a report that has only one transaction */ function isOneTransactionThread(reportID: string, parentReportID: string): boolean { - const parentReportActions = reportActionsByReport?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]); + const parentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? ([] as ReportAction[]); const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(parentReportID, parentReportActions); return reportID === transactionThreadReportID; } @@ -1352,7 +1359,7 @@ function isOneOnOneChat(report: OnyxEntry): boolean { } /** - * Checks if the current user is a payer of the request + * Checks if the current user is a payer of the expense */ function isPayer(session: OnyxEntry, iouReport: OnyxEntry) { @@ -1442,9 +1449,10 @@ function canDeleteReportAction(reportAction: OnyxEntry, reportID: return false; } + const linkedReport = isThreadFirstChat(reportAction, reportID) ? getReport(report?.parentReportID) : report; if (isActionOwner) { - if (!isEmptyObject(report) && isMoneyRequestReport(report)) { - return canAddOrDeleteTransactions(report); + if (!isEmptyObject(linkedReport) && isMoneyRequestReport(linkedReport)) { + return canAddOrDeleteTransactions(linkedReport); } return true; } @@ -2130,7 +2138,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo * Determines if the option requires action from the current user. This can happen when it: * - is unread and the user was mentioned in one of the unread comments * - is for an outstanding task waiting on the user - * - has an outstanding child money request that is waiting for an action from the current user (e.g. pay, approve, add bank account) + * - has an outstanding child expense that is waiting for an action from the current user (e.g. pay, approve, add bank account) * * @param option (report or optionItem) * @param parentReportAction (the report action the current report is a thread of) @@ -2390,7 +2398,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } /** - * Gets transaction created, amount, currency, comment, and waypoints (for distance request) + * Gets transaction created, amount, currency, comment, and waypoints (for distance expense) * into a flat object. Used for displaying transactions and sending them in API commands */ @@ -2478,11 +2486,11 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { } /** - * Checks if the current user can edit the provided property of a money request + * Checks if the current user can edit the provided property of an expense * */ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, fieldToEdit: ValueOf): boolean { - // A list of fields that cannot be edited by anyone, once a money request has been settled + // A list of fields that cannot be edited by anyone, once an expense has been settled const restrictedFields: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, CONST.EDIT_REQUEST_FIELD.CURRENCY, @@ -2536,7 +2544,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, field * * - It was written by the current user * - It's an ADDCOMMENT that is not an attachment - * - It's money request where conditions for editability are defined in canEditMoneyRequest method + * - It's an expense where conditions for editability are defined in canEditMoneyRequest method * - It's not pending deletion */ function canEditReportAction(reportAction: OnyxEntry): boolean { @@ -2566,7 +2574,7 @@ function getTransactionsWithReceipts(iouReportID: string | undefined): Transacti * instead of the report total only when we have no report total ready to show. This is the case when * all requests are receipts that are being SmartScanned. As soon as we have a non-receipt request, * or as soon as one receipt request is done scanning, we have at least one - * "ready" money request, and we remove this indicator to show the partial report total. + * "ready" expense, and we remove this indicator to show the partial report total. */ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewAction: OnyxEntry): boolean { const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); @@ -2610,10 +2618,7 @@ function getTransactionReportName(reportAction: OnyxEntry, policy: OnyxEntry = nu return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport)).join(', '); } +/** + * Get the payee name given a report. + */ +function getPayeeName(report: OnyxEntry): string | undefined { + if (isEmptyObject(report)) { + return undefined; + } + + const participantAccountIDs = report?.participantAccountIDs ?? []; + const participantsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== currentUserAccountID); + if (participantsWithoutCurrentUser.length === 0) { + return undefined; + } + return getDisplayNameForParticipant(participantsWithoutCurrentUser[0], true); +} + /** * Get either the policyName or domainName the chat is tied to */ @@ -3122,6 +3143,27 @@ function hasReportNameError(report: OnyxEntry): boolean { return !isEmptyObject(report?.errorFields?.reportName); } +/** + * Adds a domain to a short mention, converting it into a full mention with email or SMS domain. + * @param mention The user mention to be converted. + * @returns The converted mention as a full mention string or undefined if conversion is not applicable. + */ +function addDomainToShortMention(mention: string): string | undefined { + if (!Str.isValidEmail(mention) && currentUserPrivateDomain) { + const mentionWithEmailDomain = `${mention}@${currentUserPrivateDomain}`; + if (allPersonalDetailLogins.includes(mentionWithEmailDomain)) { + return mentionWithEmailDomain; + } + } + if (Str.isValidE164Phone(mention)) { + const mentionWithSmsDomain = PhoneNumber.addSMSDomainIfPhoneNumber(mention); + if (allPersonalDetailLogins.includes(mentionWithSmsDomain)) { + return mentionWithSmsDomain; + } + } + return undefined; +} + /** * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! @@ -3130,21 +3172,8 @@ function getParsedComment(text: string): string { const parser = new ExpensiMark(); const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => { const mention = match.substring(1); - - if (!Str.isValidEmail(mention) && currentUserPrivateDomain) { - const mentionWithEmailDomain = `${mention}@${currentUserPrivateDomain}`; - if (allPersonalDetailLogins.includes(mentionWithEmailDomain)) { - return `@${mentionWithEmailDomain}`; - } - } - if (Str.isValidE164Phone(mention)) { - const mentionWithSmsDomain = PhoneNumber.addSMSDomainIfPhoneNumber(mention); - if (allPersonalDetailLogins.includes(mentionWithSmsDomain)) { - return `@${mentionWithSmsDomain}`; - } - } - - return match; + const mentionWithDomain = addDomainToShortMention(mention); + return mentionWithDomain ? `@${mentionWithDomain}` : match; }); return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText: !shouldAllowRawHTMLMessages()}) : lodashEscape(text); @@ -3307,7 +3336,7 @@ function buildOptimisticTaskCommentReportAction(taskReportID: string, taskTitle: * @param total - IOU amount in the smallest unit of the currency. * @param chatReportID - Report ID of the chat where the IOU is. * @param currency - IOU currency. - * @param isSendingMoney - If we send money the IOU should be created as settled + * @param isSendingMoney - If we pay someone the IOU should be created as settled */ function buildOptimisticIOUReport(payeeAccountID: number, payerAccountID: number, total: number, chatReportID: string, currency: string, isSendingMoney = false): OptimisticIOUReport { @@ -3460,7 +3489,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num iouMessage = `submitted ${amount}`; break; case CONST.IOU.REPORT_ACTION_TYPE.CREATE: - iouMessage = `requested ${amount}${comment && ` for ${comment}`}`; + iouMessage = `submitted ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.TRACK: iouMessage = `tracking ${amount}${comment && ` for ${comment}`}`; @@ -3469,7 +3498,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num iouMessage = `split ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.DELETE: - iouMessage = `deleted the ${amount} request${comment && ` for ${comment}`}`; + iouMessage = `deleted the ${amount} expense${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.PAY: iouMessage = isSettlingUp ? `paid ${amount}${paymentMethodMessage}` : `sent ${amount}${comment && ` for ${comment}`}${paymentMethodMessage}`; @@ -3500,7 +3529,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num * @param [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, Expensify). * @param [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. * @param [isSettlingUp] - Whether we are settling up an IOU. - * @param [isSendMoneyFlow] - Whether this is send money flow + * @param [isSendMoneyFlow] - Whether this is pay someone flow * @param [receipt] * @param [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat */ @@ -3532,7 +3561,7 @@ function buildOptimisticIOUReportAction( }; if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { - // In send money flow, we store amount, comment, currency in IOUDetails when type = pay + // In pay someone flow, we store amount, comment, currency in IOUDetails when type = pay if (isSendMoneyFlow) { const keys = ['amount', 'comment', 'currency'] as const; keys.forEach((key) => { @@ -3541,7 +3570,7 @@ function buildOptimisticIOUReportAction( originalMessage.IOUDetails = {amount, comment, currency}; originalMessage.paymentType = paymentType; } else { - // In case of pay money request action, we dont store the comment + // In case of pay someone action, we dont store the comment // and there is no single transctionID to link the action to. delete originalMessage.IOUTransactionID; delete originalMessage.comment; @@ -3552,7 +3581,7 @@ function buildOptimisticIOUReportAction( // IOUs of type split only exist in group DMs and those don't have an iouReport so we need to delete the IOUReportID key if (type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT) { delete originalMessage.IOUReportID; - // Split bill made from a policy expense chat only have the payee's accountID as the participant because the payer could be any policy admin + // Split expense made from a policy expense chat only have the payee's accountID as the participant because the payer could be any policy admin if (isOwnPolicyExpenseChat) { originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : []; } else { @@ -3855,7 +3884,7 @@ function updateReportPreview(iouReport: OnyxEntry, reportPreviewAction: ...previousTransactions, } : recentReceiptTransactions, - // As soon as we add a transaction without a receipt to the report, it will have ready money requests, + // As soon as we add a transaction without a receipt to the report, it will have ready expenses, // so we remove the whisper whisperedToAccountIDs: hasReceipt ? reportPreviewAction?.whisperedToAccountIDs : [], }; @@ -4073,7 +4102,7 @@ function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): Optim { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: Localize.translateLocal('iou.heldRequest'), + text: Localize.translateLocal('iou.heldExpense'), }, ], person: [ @@ -4135,7 +4164,7 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: Localize.translateLocal('iou.unheldRequest'), + text: Localize.translateLocal('iou.unheldExpense'), }, ], person: [ @@ -4411,7 +4440,7 @@ function buildTransactionThread( } /** - * Build optimistic money request entities: + * Build optimistic expense entities: * * 1. CREATED action for the chatReport * 2. CREATED action for the iouReport @@ -4554,13 +4583,13 @@ function canAccessReport(report: OnyxEntry, policies: OnyxCollection, currentReportId: string): boolean { const currentReport = getReport(currentReportId); const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : null); - const reportActions = reportActionsByReport?.[report?.reportID ?? ''] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {}; const isChildReportHasComment = Object.values(reportActions ?? {})?.some((reportAction) => (reportAction?.childVisibleActionCount ?? 0) > 0); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } /** - * Checks to see if a report's parentAction is a money request that contains a violation + * Checks to see if a report's parentAction is an expense that contains a violation */ function doesTransactionThreadHaveViolations(report: OnyxEntry, transactionViolations: OnyxCollection, parentReportAction: OnyxEntry): boolean { if (parentReportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { @@ -4580,7 +4609,7 @@ function doesTransactionThreadHaveViolations(report: OnyxEntry, transact } /** - * Checks if we should display violation - we display violations when the money request has violation and it is not settled + * Checks if we should display violation - we display violations when the expense has violation and it is not settled */ function shouldDisplayTransactionThreadViolations( report: OnyxEntry, @@ -4728,7 +4757,7 @@ function shouldReportBeInOptionList({ } /** - * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, money request, room, and policy expense chat. + * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, expense, room, and policy expense chat. */ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports): OnyxEntry { const sortedNewParticipantList = newParticipantList.sort(); @@ -4949,20 +4978,20 @@ function hasIOUWaitingOnCurrentUserBankAccount(chatReport: OnyxEntry): b } /** - * Users can request money: + * Users can submit an expense: * - in policy expense chats only if they are in a role of a member in the chat (in other words, if it's their policy expense chat) * - in an open or submitted expense report tied to a policy expense chat the user owns - * - employee can request money in submitted expense report only if the policy has Instant Submit settings turned on + * - employee can submit expenses in a submitted expense report only if the policy has Instant Submit settings turned on * - in an IOU report, which is not settled yet * - in a 1:1 DM chat */ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, otherParticipants: number[]): boolean { - // User cannot request money in chat thread or in task report or in chat room + // User cannot submit expenses in a chat thread, task report or in a chat room if (isChatThread(report) || isTaskReport(report) || isChatRoom(report) || isSelfDM(report) || isGroupChat(report)) { return false; } - // Users can only request money in DMs if they are a 1:1 DM + // Users can only submit expenses in DMs if they are a 1:1 DM if (isDM(report)) { return otherParticipants.length === 1; } @@ -4977,19 +5006,19 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o isOwnPolicyExpenseChat = Boolean(getParentReport(report)?.isOwnPolicyExpenseChat); } - // In case there are no other participants than the current user and it's not user's own policy expense chat, they can't request money from such report + // In case there are no other participants than the current user and it's not user's own policy expense chat, they can't submit expenses from such report if (otherParticipants.length === 0 && !isOwnPolicyExpenseChat) { return false; } - // User can request money in any IOU report, unless paid, but user can only request money in an expense report + // User can submit expenses in any IOU report, unless paid, but the user can only submit expenses in an expense report // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { const canAddTransactions = canAddOrDeleteTransactions(report); return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions; } - // In case of policy expense chat, users can only request money from their own policy expense chat + // In the case of policy expense chat, users can only submit expenses from their own policy expense chat return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } @@ -5004,19 +5033,19 @@ function isGroupChatAdmin(report: OnyxEntry, accountID: number) { } /** - * Helper method to define what money request options we want to show for particular method. - * There are 4 money request options: Request, Split, Send and Track expense: - * - Request option should show for: + * Helper method to define what expense options we want to show for particular method. + * There are 4 expense options: Submit, Split, Pay and Track expense: + * - Submit option should show for: * - DMs * - own policy expense chats * - open and processing expense reports tied to own policy expense chat * - unsettled IOU reports - * - Send option should show for: + * - Pay option should show for: * - DMs * - Split options should show for: * - DMs * - chat/policy rooms with more than 1 participant - * - groups chats with 3 and more participants + * - groups chats with 2 and more participants * - corporate workspace chats * - Track expense option should show for: * - Self DMs @@ -5027,7 +5056,7 @@ function isGroupChatAdmin(report: OnyxEntry, accountID: number) { * as a participant of the report. */ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], canUseTrackExpense = true): Array> { - // In any thread or task report, we do not allow any new money requests yet + // In any thread or task report, we do not allow any new expenses yet if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) { return []; } @@ -5047,10 +5076,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 0) || (isDM(report) && otherParticipants.length > 0) || @@ -5069,7 +5098,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry): Error } /** - * Return true if the Money Request report is marked for deletion. + * Return true if the expense report is marked for deletion. */ function isMoneyRequestReportPendingDeletion(report: OnyxEntry | EmptyObject): boolean { if (!isMoneyRequestReport(report)) { @@ -5200,7 +5229,7 @@ function isMoneyRequestReportPendingDeletion(report: OnyxEntry | EmptyOb function canUserPerformWriteAction(report: OnyxEntry) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); - // If the Money Request report is marked for deletion, let us prevent any further write action. + // If the expense report is marked for deletion, let us prevent any further write action. if (isMoneyRequestReportPendingDeletion(report)) { return false; } @@ -5212,7 +5241,7 @@ function canUserPerformWriteAction(report: OnyxEntry) { * Returns ID of the original report from which the given reportAction is first created. */ function getOriginalReportID(reportID: string, reportAction: OnyxEntry): string | undefined { - const reportActions = reportActionsByReport?.[reportID]; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; const currentReportAction = reportActions?.[reportAction?.reportActionID ?? ''] ?? null; const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions ?? ([] as ReportAction[])); if (transactionThreadReportID !== null) { @@ -5228,7 +5257,7 @@ function getOriginalReportID(reportID: string, reportAction: OnyxEntry): Repo } /** - * Check if the report can create the request with type is iouType + * Check if the report can create the expense with type is iouType */ function canCreateRequest(report: OnyxEntry, policy: OnyxEntry, iouType: (typeof CONST.IOU.TYPE)[keyof typeof CONST.IOU.TYPE]): boolean { const participantAccountIDs = report?.participantAccountIDs ?? []; @@ -5436,7 +5465,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { // The `REPORT_ACTION_TYPE.PAY` action type is used for both fulfilling existing requests and sending money. To // differentiate between these two scenarios, we check if the `originalMessage` contains the `IOUDetails` - // property. If it does, it indicates that this is a 'Send money' action. + // property. If it does, it indicates that this is a 'Pay someone' action. const {amount, currency} = originalMessage.IOUDetails ?? originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(amount), currency) ?? ''; @@ -5483,7 +5512,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, } else if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { translationKey = 'iou.trackedAmount'; } else { - translationKey = 'iou.requestedAmount'; + translationKey = 'iou.submittedAmount'; } return Localize.translateLocal(translationKey, { formattedAmount, @@ -5497,7 +5526,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry, * A report is a group chat if it meets the following conditions: * - Not a chat thread. * - Not a task report. - * - Not a money request / IOU report. + * - Not an expense / IOU report. * - Not an archived room. * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). * - More than 2 participants. @@ -5701,7 +5730,7 @@ function getNonHeldAndFullAmount(iouReport: OnyxEntry, policy: OnyxEntry * Disable reply in thread action if: * * - The action is listed in the thread-disabled list - * - The action is a split bill action + * - The action is a split expense action * - The action is deleted and is not threaded * - The report is archived and the action is not threaded * - The action is a whisper action and it's neither a report preview nor IOU action @@ -5714,9 +5743,10 @@ function shouldDisableThread(reportAction: OnyxEntry, reportID: st const isIOUAction = ReportActionsUtils.isMoneyRequestAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction) || ReportActionsUtils.isActionableTrackExpense(reportAction); const isArchivedReport = isArchivedRoom(getReport(reportID)); + const isActionDisabled = CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction?.actionName); return ( - CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction?.actionName) || + isActionDisabled || isSplitBillAction || (isDeletedAction && !reportAction?.childVisibleActionCount) || (isArchivedReport && !reportAction?.childVisibleActionCount) || @@ -5889,12 +5919,12 @@ function getIndicatedMissingPaymentMethod(userWallet: OnyxEntry, rep * Checks if report chat contains missing payment method */ function hasMissingPaymentMethod(userWallet: OnyxEntry, iouReportID: string): boolean { - const reportActions = reportActionsByReport?.[iouReportID] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`] ?? {}; return Object.values(reportActions).some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined); } /** - * Used from money request actions to decide if we need to build an optimistic money request report. + * Used from expense actions to decide if we need to build an optimistic expense report. Create a new report if: - we don't have an iouReport set in the chatReport - we have one, but it's waiting on the payee adding a bank account @@ -5908,7 +5938,7 @@ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxEntry * Checks if report contains actions with errors */ function hasActionsWithErrors(reportID: string): boolean { - const reportActions = reportActionsByReport?.[reportID ?? ''] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}; return Object.values(reportActions).some((action) => !isEmptyObject(action.errors)); } @@ -5931,7 +5961,7 @@ function getReportActionActorAccountID(reportAction: OnyxEntry, io function createDraftTransactionAndNavigateToParticipantSelector(transactionID: string, reportID: string, actionName: ValueOf, reportActionID: string): void { const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction); - const reportActions = reportActionsByReport?.[reportID] ?? ([] as ReportAction[]); + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]); if (!transaction || !reportActions) { return; @@ -6109,6 +6139,7 @@ export { getDefaultWorkspaceAvatarTestID, getCommentLength, getParsedComment, + addDomainToShortMention, getMoneyRequestOptions, canCreateRequest, hasIOUWaitingOnCurrentUserBankAccount, @@ -6227,6 +6258,7 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, + getPayeeName, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 3f1b354d70fc..9fc83d50f1e1 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -23,18 +23,14 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as UserUtils from './UserUtils'; -const reportActionsByReport: OnyxCollection = {}; const visibleReportActionItems: ReportActions = {}; - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { - if (!key || !actions) { + if (!actions || !key) { return; } - const reportID = CollectionUtils.extractCollectionItemID(key); - reportActionsByReport[reportID] = actions; const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); @@ -47,6 +43,7 @@ Onyx.connect({ reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); + visibleReportActionItems[reportID] = reportActionsForDisplay[reportActionsForDisplay.length - 1]; }, }); @@ -87,7 +84,7 @@ function getOrderedReportIDs( const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`; const parentReportActions = allReportActions?.[parentReportActionsKey]; - const reportActions = reportActionsByReport?.[report.reportID] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; const parentReportAction = parentReportActions?.find((action) => action && action?.reportActionID === report.parentReportActionID); const doesReportHaveViolations = !!( betas?.includes(CONST.BETAS.VIOLATIONS) && @@ -398,7 +395,7 @@ function getOptionData({ result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result as Report); - if (ReportActionsUtils.isActionableJoinRequestPending(report.reportID)) { + if (ReportUtils.isJoinRequestInAdminRoom(report)) { result.isPinned = true; result.isUnread = true; result.brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 89b89ff7b584..a5b85b87e37e 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -36,7 +36,7 @@ Onyx.connect({ }); function isDistanceRequest(transaction: OnyxEntry): boolean { - // This is used during the request creation flow before the transaction has been saved to the server + // This is used during the expense creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; } @@ -48,7 +48,7 @@ function isDistanceRequest(transaction: OnyxEntry): boolean { } function isScanRequest(transaction: OnyxEntry): boolean { - // This is used during the request creation flow before the transaction has been saved to the server + // This is used during the expense creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN; } @@ -68,7 +68,7 @@ function getRequestType(transaction: OnyxEntry): IOURequestType { } function isManualRequest(transaction: Transaction): boolean { - // This is used during the request creation flow before the transaction has been saved to the server + // This is used during the expense creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { return transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL; } @@ -80,7 +80,7 @@ function isManualRequest(transaction: Transaction): boolean { * Optimistically generate a transaction. * * @param amount – in cents - * @param [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have + * @param [existingTransactionID] When creating a distance expense, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have * it's transactionID match what was already generated. */ function buildOptimisticTransaction( @@ -176,7 +176,7 @@ function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean { } /** - * Given the edit made to the money request, return an updated transaction object. + * Given the edit made to the expnse, return an updated transaction object. */ function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean, shouldUpdateReceiptState = true): Transaction { // Only changing the first level fields so no need for deep clone now @@ -215,12 +215,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra if (Object.hasOwn(transactionChanges, 'taxAmount') && typeof transactionChanges.taxAmount === 'number') { updatedTransaction.taxAmount = isFromExpenseReport ? -transactionChanges.taxAmount : transactionChanges.taxAmount; - shouldStopSmartscan = true; } if (Object.hasOwn(transactionChanges, 'taxCode') && typeof transactionChanges.taxCode === 'string') { updatedTransaction.taxCode = transactionChanges.taxCode; - shouldStopSmartscan = true; } if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') { diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index db41bb18aeef..8205a9473e1b 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -6,7 +6,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, ReimbursementAccount, Report, ReportActions} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; -import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import type {Phrase, PhraseParameters} from './Localize'; import * as OptionsListUtils from './OptionsListUtils'; @@ -42,16 +41,15 @@ Onyx.connect({ }, }); -const reportActionsByReport: OnyxCollection = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - - const reportID = CollectionUtils.extractCollectionItemID(key); - reportActionsByReport[reportID] = actions; + allReportActions = actions; }, }); @@ -60,7 +58,7 @@ Onyx.connect({ * @returns BrickRoad for the policy passed as a param */ const getBrickRoadForPolicy = (report: Report): BrickRoad => { - const reportActions = reportActionsByReport?.[report.reportID] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`] ?? {}; const reportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); const doesReportContainErrors = Object.keys(reportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; if (doesReportContainErrors) { @@ -70,7 +68,7 @@ const getBrickRoadForPolicy = (report: Report): BrickRoad => { // To determine if the report requires attention from the current user, we need to load the parent report action let itemParentReportAction = {}; if (report.parentReportID) { - const itemParentReportActions = reportActionsByReport[report.parentReportID] ?? {}; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`] ?? {}; itemParentReportAction = report.parentReportActionID ? itemParentReportActions[report.parentReportActionID] : {}; } const reportOption = {...report, isUnread: ReportUtils.isUnread(report), isUnreadWithMention: ReportUtils.isUnreadWithMention(report)}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 896b88988818..83caa65e1d77 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -25,7 +25,6 @@ import type { UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; -import * as CollectionUtils from '@libs/CollectionUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -259,16 +258,15 @@ Onyx.connect({ callback: (value) => (allPolicies = value), }); -const reportActionsByReport: OnyxCollection = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - - const reportID = CollectionUtils.extractCollectionItemID(key); - reportActionsByReport[reportID] = actions; + allReportActions = actions; }, }); @@ -286,7 +284,7 @@ function getPolicy(policyID: string | undefined): OnyxTypes.Policy | EmptyObject * Find the report preview action from given chat report and iou report */ function getReportPreviewAction(chatReportID: string, iouReportID: string): OnyxEntry { - const reportActions = reportActionsByReport?.[chatReportID] ?? {}; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] ?? {}; // Find the report preview action from the chat report return ( @@ -297,7 +295,7 @@ function getReportPreviewAction(chatReportID: string, iouReportID: string): Onyx } /** - * Initialize money request info + * Initialize expense info * @param reportID to attach the transaction to * @param policy * @param isFromGlobalCreate @@ -311,7 +309,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, const created = currentDate || format(new Date(), 'yyyy-MM-dd'); const comment: Comment = {}; - // Add initial empty waypoints when starting a distance request + // Add initial empty waypoints when starting a distance expense if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { comment.waypoints = { waypoint0: {}, @@ -357,7 +355,7 @@ function clearMoneyRequest(transactionID: string) { } /** - * Update money request-related pages IOU type params + * Update money expense-related pages IOU type params */ function updateMoneyRequestTypeParams(routes: StackNavigationState['routes'] | NavigationPartialRoute[], newIouType: string, tab?: string) { routes.forEach((route) => { @@ -373,7 +371,7 @@ function updateMoneyRequestTypeParams(routes: StackNavigationState, iouReport: OnyxTypes.Report, @@ -1170,7 +1168,7 @@ function getDeleteTrackExpenseInformation( // STEP 2: Decide if we need to: // 1. Delete the transactionThread - delete if there are no visible comments in the thread and we're not moving the transaction - // 2. Update the moneyRequestPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted and we're not moving the transaction + // 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted and we're not moving the transaction const shouldDeleteTransactionThread = !isMovingTransactionFromTrackExpense && (transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false); const shouldShowDeletedRequestMessage = !isMovingTransactionFromTrackExpense && !!transactionThreadID && !shouldDeleteTransactionThread; @@ -1316,7 +1314,7 @@ function getDeleteTrackExpenseInformation( } /** - * Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then + * Gathers all the data needed to submit an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead */ function getMoneyRequestInformation( @@ -1364,8 +1362,8 @@ function getMoneyRequestInformation( chatReport = ReportUtils.buildOptimisticChatReport([payerAccountID]); } - // STEP 2: Get the money request report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report. - // If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic money request report. + // STEP 2: Get the Expense/IOU report. If the moneyRequestReportID has been provided, we want to add the transaction to this specific report. + // If no such reportID has been provided, let's use the chatReport.iouReportID property. In case that is not present, build a new optimistic Expense/IOU report. let iouReport: OnyxEntry = null; if (moneyRequestReportID) { iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReportID}`] ?? null; @@ -1568,8 +1566,8 @@ function getTrackExpenseInformation( return {}; } - // STEP 2: If not in the self-DM flow, we need to use the money request report. - // For this, first use the chatReport.iouReportID property. Build a new optimistic money request report if needed. + // STEP 2: If not in the self-DM flow, we need to use the expense report. + // For this, first use the chatReport.iouReportID property. Build a new optimistic expense report if needed. const shouldUseMoneyReport = !!isPolicyExpenseChat; let iouReport: OnyxEntry = null; @@ -2287,7 +2285,7 @@ function getUpdateTrackExpenseParams( }; } -/** Updates the created date of a money request */ +/** Updates the created date of an expense */ function updateMoneyRequestDate( transactionID: string, transactionThreadReportID: string, @@ -2310,7 +2308,7 @@ function updateMoneyRequestDate( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); } -/** Updates the billable field of a money request */ +/** Updates the billable field of an expense */ function updateMoneyRequestBillable( transactionID: string, transactionThreadReportID: string, @@ -2326,7 +2324,7 @@ function updateMoneyRequestBillable( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE, params, onyxData); } -/** Updates the merchant field of a money request */ +/** Updates the merchant field of an expense */ function updateMoneyRequestMerchant( transactionID: string, transactionThreadReportID: string, @@ -2349,7 +2347,7 @@ function updateMoneyRequestMerchant( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT, params, onyxData); } -/** Updates the tag of a money request */ +/** Updates the tag of an expense */ function updateMoneyRequestTag( transactionID: string, transactionThreadReportID: string, @@ -2365,7 +2363,7 @@ function updateMoneyRequestTag( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_TAG, params, onyxData); } -/** Updates the created tax amount of a money request */ +/** Updates the created tax amount of an expense */ function updateMoneyRequestTaxAmount( transactionID: string, optimisticReportActionID: string, @@ -2381,7 +2379,7 @@ function updateMoneyRequestTaxAmount( API.write('UpdateMoneyRequestTaxAmount', params, onyxData); } -/** Updates the created tax rate of a money request */ +/** Updates the created tax rate of an expense */ function updateMoneyRequestTaxRate( transactionID: string, optimisticReportActionID: string, @@ -2406,7 +2404,7 @@ type UpdateMoneyRequestDistanceParams = { policyCategories?: OnyxEntry; }; -/** Updates the waypoints of a distance money request */ +/** Updates the waypoints of a distance expense */ function updateMoneyRequestDistance({ transactionID, transactionThreadReportID, @@ -2429,7 +2427,7 @@ function updateMoneyRequestDistance({ API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE, params, onyxData); } -/** Updates the category of a money request */ +/** Updates the category of an expense */ function updateMoneyRequestCategory( transactionID: string, transactionThreadReportID: string, @@ -2445,7 +2443,7 @@ function updateMoneyRequestCategory( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY, params, onyxData); } -/** Updates the description of a money request */ +/** Updates the description of an expense */ function updateMoneyRequestDescription( transactionID: string, transactionThreadReportID: string, @@ -2468,7 +2466,7 @@ function updateMoneyRequestDescription( API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION, params, onyxData); } -/** Edits an existing distance request */ +/** Edits an existing distance expense */ function updateDistanceRequest( transactionID: string, transactionThreadReportID: string, @@ -2762,7 +2760,7 @@ function shareTrackedExpense( } /** - * Request money from another user + * Submit expense to another user */ function requestMoney( report: OnyxEntry, @@ -3129,7 +3127,7 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, * {email: 'user3', amount: 100, iouReportID: '200', chatReportID: '210', transactionID: '220', reportActionID: '230'} * ] * @param amount - always in the smallest unit of the currency - * @param existingSplitChatReportID - the report ID where the split bill happens, could be a group chat or a workspace chat + * @param existingSplitChatReportID - the report ID where the split expense happens, could be a group chat or a workspace chat */ function createSplitsAndOnyxData( participants: Participant[], @@ -3160,7 +3158,7 @@ function createSplitsAndOnyxData( created, '', '', - merchant || Localize.translateLocal('iou.request'), + merchant || Localize.translateLocal('iou.expense'), undefined, undefined, undefined, @@ -3374,7 +3372,7 @@ function createSplitsAndOnyxData( created, CONST.IOU.TYPE.SPLIT, splitTransaction.transactionID, - merchant || Localize.translateLocal('iou.request'), + merchant || Localize.translateLocal('iou.expense'), undefined, undefined, undefined, @@ -3641,7 +3639,7 @@ type StartSplitBilActionParams = { currency: string; }; -/** Used exclusively for starting a split bill request that contains a receipt, the split request will be completed once the receipt is scanned +/** Used exclusively for starting a split expense request that contains a receipt, the split request will be completed once the receipt is scanned * or user enters details manually. * * @param existingSplitChatReportID - Either a group DM or a workspace chat @@ -3929,7 +3927,7 @@ function startSplitBill({ Report.notifyNewAction(splitChatReport.chatReportID ?? '', currentUserAccountID); } -/** Used for editing a split bill while it's still scanning or when SmartScan fails, it completes a split bill started by startSplitBill above. +/** Used for editing a split expense while it's still scanning or when SmartScan fails, it completes a split expense started by startSplitBill above. * * @param chatReportID - The group chat or workspace reportID * @param reportAction - The split action that lives in the chatReport above @@ -4031,7 +4029,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA let oneOnOneChatReport: OnyxTypes.Report | null; let isNewOneOnOneChatReport = false; if (isPolicyExpenseChat) { - // The workspace chat reportID is saved in the splits array when starting a split bill with a workspace + // The workspace chat reportID is saved in the splits array when starting a split expense with a workspace oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`] ?? null; } else { const existingChatReport = ReportUtils.getChatByParticipants(participant.accountID ? [participant.accountID] : []); @@ -4456,7 +4454,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { policyCategories?: OnyxEntry; }; -/** Updates the amount and currency fields of a money request */ +/** Updates the amount and currency fields of an expense */ function updateMoneyRequestAmountAndCurrency({ transactionID, transactionThreadReportID, @@ -4498,7 +4496,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor // STEP 2: Decide if we need to: // 1. Delete the transactionThread - delete if there are no visible comments in the thread - // 2. Update the moneyRequestPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted + // 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted const shouldDeleteTransactionThread = transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false; const shouldShowDeletedRequestMessage = !!transactionThreadID && !shouldDeleteTransactionThread; @@ -5346,7 +5344,7 @@ function canIOUBePaid(iouReport: OnyxEntry | EmptyObject, chat } function hasIOUToApproveOrPay(chatReport: OnyxEntry | EmptyObject, excludedIOUReportID: string): boolean { - const chatReportActions = reportActionsByReport?.[chatReport?.reportID ?? ''] ?? {}; + const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; return Object.values(chatReportActions).some((action) => { const iouReport = ReportUtils.getReport(action.childReportID ?? ''); @@ -5801,7 +5799,7 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param report attached to the transaction */ function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry) { - // If the report is iou or expense report, we should get the chat report to set participant for request money + // If the report is iou or expense report, we should get the chat report to set participant for expense const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); @@ -5846,7 +5844,7 @@ function setShownHoldUseExplanation() { } /** - * Put money request on HOLD + * Put expense on HOLD */ function putOnHold(transactionID: string, comment: string, reportID: string) { const currentTime = DateUtils.getDBTime(); @@ -5910,7 +5908,7 @@ function putOnHold(transactionID: string, comment: string, reportID: string) { } /** - * Remove money request from HOLD + * Remove expense from HOLD */ function unholdRequest(transactionID: string, reportID: string) { const createdReportAction = ReportUtils.buildOptimisticUnHoldReportAction(); diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index dc8cefc0c30e..c5a74bdc6ace 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -10,7 +10,6 @@ import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/AddDebitCardForm'; import type {BankAccountList, FundList} from '@src/types/onyx'; @@ -30,7 +29,7 @@ const kycWallRef: MutableRefObject = createRef(); /** * When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set. */ -function continueSetup(fallbackRoute: Route = ROUTES.HOME) { +function continueSetup(fallbackRoute?: Route) { if (!kycWallRef.current?.continueAction) { Navigation.goBack(fallbackRoute); return; diff --git a/src/libs/actions/PolicyConnections.ts b/src/libs/actions/PolicyConnections.ts new file mode 100644 index 000000000000..7ccf9f2506bd --- /dev/null +++ b/src/libs/actions/PolicyConnections.ts @@ -0,0 +1,38 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {OpenPolicyAccountingPageParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function openPolicyAccountingPage(policyID: string) { + const hasConnectionsDataBeenFetchedKey = `${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policyID}` as const; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: hasConnectionsDataBeenFetchedKey, + value: false, + }, + ]; + const finallyData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: hasConnectionsDataBeenFetchedKey, + value: true, + }, + ]; + + const parameters: OpenPolicyAccountingPageParams = { + policyID, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE, parameters, { + optimisticData, + finallyData, + }); +} + +// More action functions will be added later +// eslint-disable-next-line import/prefer-default-export +export {openPolicyAccountingPage}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f599208ff915..41079d20a982 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2941,8 +2941,29 @@ function getReportPrivateNote(reportID: string | undefined) { * - Sets the introSelected NVP to the choice the user made * - Creates an optimistic report comment from concierge */ -function completeEngagementModal(text: string, choice: ValueOf) { +function completeEngagementModal(choice: ValueOf, text?: string) { const conciergeAccountID = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE])[0]; + + // We do not need to send any message for some choices + if (!text) { + const parameters: CompleteEngagementModalParams = { + reportID: conciergeChatReportID ?? '', + engagementChoice: choice, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_INTRO_SELECTED, + value: {choice}, + }, + ]; + API.write(WRITE_COMMANDS.COMPLETE_ENGAGEMENT_MODAL, parameters, { + optimisticData, + }); + return; + } + const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, conciergeAccountID); const reportCommentAction: OptimisticAddCommentReportAction = reportComment.reportAction; const lastComment = reportCommentAction?.message?.[0]; @@ -3007,6 +3028,7 @@ function completeEngagementModal(text: string, choice: ValueOf = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - - const reportID = CollectionUtils.extractCollectionItemID(key); - reportActionsByReport[reportID] = actions; + allReportActions = actions; }, }); @@ -94,7 +92,7 @@ function clearAllRelatedReportActionErrors(reportID: string, reportAction: Repor } if (reportAction.childReportID && ignore !== 'child') { - const childActions = reportActionsByReport?.[reportAction.childReportID] ?? {}; + const childActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction.childReportID}`] ?? {}; Object.values(childActions).forEach((action) => { const childErrorKeys = Object.keys(action.errors ?? {}).filter((err) => errorKeys.includes(err)); clearAllRelatedReportActionErrors(reportAction.childReportID ?? '', action, 'parent', childErrorKeys); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 9cf7b0b78008..f74b451c29f6 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -5,7 +5,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as API from '@libs/API'; import type {CancelTaskParams, CompleteTaskParams, CreateTaskParams, EditTaskAssigneeParams, EditTaskParams, ReopenTaskParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; -import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; @@ -67,16 +66,16 @@ Onyx.connect({ }, }); -const allReportActions: OnyxCollection = {}; +let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + waitForCollectionCallback: true, + callback: (actions) => { + if (!actions) { return; } - const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = actions; + allReportActions = actions; }, }); @@ -795,7 +794,7 @@ function getParentReportAction(report: OnyxEntry): ReportActio if (!report?.parentReportID || !report.parentReportActionID) { return {}; } - return allReportActions?.[report.parentReportID]?.[report.parentReportActionID] ?? {}; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] ?? {}; } /** diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9ddaca686e95..a95bf9a825f0 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -67,7 +67,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp [`waypoint${index}`]: waypoint, }, }, - // We want to reset the amount only for draft transactions (when creating the request). + // We want to reset the amount only for draft transactions (when creating the expense). // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving a new waypoint as this indicates the user is updating their input @@ -141,7 +141,7 @@ function removeWaypoint(transaction: OnyxEntry, currentIndex: strin ...transaction?.comment, waypoints: reIndexedWaypoints, }, - // We want to reset the amount only for draft transactions (when creating the request). + // We want to reset the amount only for draft transactions (when creating the expense). // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), }; @@ -239,7 +239,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i comment: { waypoints, }, - // We want to reset the amount only for draft transactions (when creating the request). + // We want to reset the amount only for draft transactions (when creating the expense). // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving new waypoints as this indicates the user is updating their input diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index dcd6e025e23b..f347655b6a4d 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -525,7 +525,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { const types = flatten.map((data) => data?.originalMessage).filter(Boolean) as OriginalMessage[]; for (const message of types) { - // someone sent money + // Pay someone flow if ('IOUDetails' in message) { return playSound(SOUNDS.SUCCESS); } @@ -545,12 +545,12 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { return playSound(SOUNDS.ATTENTION); } - // request money + // Submit expense flow if ('IOUTransactionID' in message) { return playSound(SOUNDS.ATTENTION); } - // Someone completes a money request + // Someone reimburses an expense if ('IOUReportID' in message) { return playSound(SOUNDS.SUCCESS); } diff --git a/src/libs/getSectionsWithIndexOffset.ts b/src/libs/getSectionsWithIndexOffset.ts index 7de78d048a4d..3237651a0385 100644 --- a/src/libs/getSectionsWithIndexOffset.ts +++ b/src/libs/getSectionsWithIndexOffset.ts @@ -3,7 +3,7 @@ import type {SectionListData} from 'react-native'; /** * Returns a list of sections with indexOffset */ -export default function getSectionsWithIndexOffset(sections: Array>): Array> { +export default function getSectionsWithIndexOffset(sections: Array>): Array> { return sections.map((section, index) => { const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0); return {...section, indexOffset}; diff --git a/src/libs/migrations/TransactionBackupsToCollection.ts b/src/libs/migrations/TransactionBackupsToCollection.ts index 407bc70e1f38..a7167492007a 100644 --- a/src/libs/migrations/TransactionBackupsToCollection.ts +++ b/src/libs/migrations/TransactionBackupsToCollection.ts @@ -8,7 +8,7 @@ import type {Transaction} from '@src/types/onyx'; * This migration moves all the transaction backups stored in the transaction collection, ONYXKEYS.COLLECTION.TRANSACTION, to a reserved collection that only * stores draft transactions, ONYXKEYS.COLLECTION.TRANSACTION_DRAFT. The purpose of the migration is that there is a possibility that transaction backups are * not filtered by most functions, e.g, getAllReportTransactions (src/libs/TransactionUtils.ts). One problem that arose from storing transaction backups with - * the other transactions is that for every distance request which have their waypoints updated offline, we expect the ReportPreview component to display the + * the other transactions is that for every distance expense which have their waypoints updated offline, we expect the ReportPreview component to display the * default image of a pending map. However, due to the presence of the transaction backup, the previous map image will be displayed alongside the pending map. * The problem was further discussed in this PR. https://github.com/Expensify/App/pull/30232#issuecomment-178110172 */ diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx similarity index 64% rename from src/pages/SearchPage/SearchPageFooter.tsx rename to src/pages/ChatFinderPage/ChatFinderPageFooter.tsx index a9369ff9f0b7..4c006abacfc7 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx @@ -2,10 +2,10 @@ import React from 'react'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import CONST from '@src/CONST'; -function SearchPageFooter() { +function ChatFinderPageFooter() { return ; } -SearchPageFooter.displayName = 'SearchPageFooter'; +ChatFinderPageFooter.displayName = 'ChatFinderPageFooter'; -export default SearchPageFooter; +export default ChatFinderPageFooter; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/ChatFinderPage/index.tsx similarity index 85% rename from src/pages/SearchPage/index.tsx rename to src/pages/ChatFinderPage/index.tsx index d79c60ff4f45..f992fa37d8c5 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -25,9 +25,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import SearchPageFooter from './SearchPageFooter'; +import ChatFinderPageFooter from './ChatFinderPageFooter'; -type SearchPageOnyxProps = { +type ChatFinderPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; @@ -35,23 +35,23 @@ type SearchPageOnyxProps = { isSearchingForReports: OnyxEntry; }; -type SearchPageProps = SearchPageOnyxProps & StackScreenProps; +type ChatFinderPageProps = ChatFinderPageOnyxProps & StackScreenProps; -type SearchPageSectionItem = { +type ChatFinderPageSectionItem = { data: OptionData[]; shouldShow: boolean; }; -type SearchPageSectionList = SearchPageSectionItem[]; +type ChatFinderPageSectionList = ChatFinderPageSectionItem[]; const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); + Timing.end(CONST.TIMING.CHAT_FINDER_RENDER); + Performance.markEnd(CONST.TIMING.CHAT_FINDER_RENDER); }; -const SerachPageFooterInstance = ; +const ChatFinderPageFooterInstance = ; -function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { +function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); const themeStyles = useThemeStyles(); const {translate} = useLocalize(); @@ -65,8 +65,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); useEffect(() => { - Timing.start(CONST.TIMING.SEARCH_RENDER); - Performance.markStart(CONST.TIMING.SEARCH_RENDER); + Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); + Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); }, []); useEffect(() => { @@ -113,8 +113,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; - const sections = useMemo((): SearchPageSectionList => { - const newSections: SearchPageSectionList = []; + const sections = useMemo((): ChatFinderPageSectionList => { + const newSections: ChatFinderPageSectionList = []; if (recentReports?.length > 0) { newSections.push({ @@ -162,12 +162,13 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) return ( @@ -183,16 +184,16 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) onLayout={setPerformanceTimersEnd} onSelectRow={selectReport} showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} - footerContent={!isDismissed && SerachPageFooterInstance} + footerContent={!isDismissed && ChatFinderPageFooterInstance} isLoadingNewOptions={isSearchingForReports ?? undefined} /> ); } -SearchPage.displayName = 'SearchPage'; +ChatFinderPage.displayName = 'ChatFinderPage'; -export default withOnyx({ +export default withOnyx({ betas: { key: ONYXKEYS.BETAS, }, @@ -200,4 +201,4 @@ export default withOnyx({ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, -})(SearchPage); +})(ChatFinderPage); diff --git a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx index fb642c897dbc..c6ec7dc49b9f 100644 --- a/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx +++ b/src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx @@ -56,7 +56,7 @@ function ManageTeamsExpensesModal() { ); const completeEngagement = () => { - Report.completeEngagementModal(messageCopy, CONST.INTRO_CHOICES.MANAGE_TEAM); + Report.completeEngagementModal(CONST.INTRO_CHOICES.MANAGE_TEAM, messageCopy); Report.navigateToConciergeChat(); }; diff --git a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx index 3c7520b850b4..0618a2bcc5b1 100644 --- a/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx +++ b/src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx @@ -32,7 +32,7 @@ const messageCopy = { '\n' + 'Next, start adding expenses to your workspace:\n' + '\n' + - '1. Click the green *+* > *Request money*.\n' + + '1. Click the green *+* > *Submit expense*.\n' + '2. Add an expense or scan a receipt.\n' + '3. Choose your workspace as the destination.\n' + '\n' + @@ -40,7 +40,7 @@ const messageCopy = { [CONST.INTRO_CHOICES.SUBMIT]: "Here's how to submit expenses for reimbursement:\n" + '\n' + - '1. Click the green *+* > *Request money*.\n' + + '1. Click the green *+* > *Submit expense*.\n' + '2. Add an expense or scan a receipt.\n' + "3. Enter your reimburser's email or phone number.\n" + '\n' + @@ -49,15 +49,15 @@ const messageCopy = { [CONST.INTRO_CHOICES.CHAT_SPLIT]: "Here's how to split expenses with friends:\n" + '\n' + - '1. Tap the green *+* > *Request money*.\n' + + '1. Tap the green *+* > *Split expense*.\n' + '2. Add an expense or scan a receipt.\n' + "3. Enter your friend's email or phone number.\n" + - '4. Tap *Split* next to their contact info.\n' + + '4. Select the option.\n' + '5. Repeat for any additional friends.\n' + "6. Tap *Add to split* when you're done.\n" + - '7. Review and tap *Split* to send your request(s).\n' + + '7. Review and tap *Split* to split your expense(s).\n' + '\n' + - "We'll send a money request to each of your friends and make sure you get paid back. Let me know how it goes!", + "We'll submit an expense to each of your friends and make sure you get paid back. Let me know how it goes!", }; const menuIcons = { @@ -86,7 +86,7 @@ function PurposeForUsingExpensifyModal() { return Navigation.navigate(ROUTES.ONBOARD_MANAGE_EXPENSES); } - Report.completeEngagementModal(message, choice); + Report.completeEngagementModal(choice, message); Report.navigateToConciergeChat(true); }, []); diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 3b82a79e6c48..82f171e12f14 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -83,7 +83,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on return; } - Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose); + Report.completeEngagementModal(selectedPurpose, CONST.ONBOARDING_CONCIERGE[selectedPurpose]); Navigation.dismissModal(); // Only navigate to concierge chat when central pane is visible diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx deleted file mode 100644 index f1a439548f1b..000000000000 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import {MagnifyingGlass} from '@components/Icon/Expensicons'; -import OptionRow from '@components/OptionRow'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; -import * as App from '@userActions/App'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import WorkspaceCardCreateAWorkspace from './workspace/card/WorkspaceCardCreateAWorkspace'; - -type SimpleWorkspaceItem = { - text?: string; - policyID?: string; - isPolicyAdmin?: boolean; -}; - -const sortWorkspacesBySelected = (workspace1: SimpleWorkspaceItem, workspace2: SimpleWorkspaceItem, selectedWorkspaceID: string | undefined): number => { - if (workspace1.policyID === selectedWorkspaceID) { - return -1; - } - if (workspace2.policyID === selectedWorkspaceID) { - return 1; - } - return workspace1.text?.toLowerCase().localeCompare(workspace2.text?.toLowerCase() ?? '') ?? 0; -}; - -type WorkspaceSwitcherPageOnyxProps = { - /** The list of this user's policies */ - policies: OnyxCollection; -}; - -type WorkspaceSwitcherPageProps = WorkspaceSwitcherPageOnyxProps; - -function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - const [searchTerm, setSearchTerm] = useState(''); - const {inputCallbackRef} = useAutoFocusInput(); - const {translate} = useLocalize(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); - - const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(), []); - const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(), []); - - const getIndicatorTypeForPolicy = useCallback( - (policyId?: string) => { - if (policyId && policyId !== activeWorkspaceID) { - return brickRoadsForPolicies[policyId]; - } - - if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } - - if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; - } - - return undefined; - }, - [activeWorkspaceID, brickRoadsForPolicies], - ); - - const hasUnreadData = useCallback( - // TO DO: Implement checking if policy has some unread data - (policyId?: string) => { - if (policyId) { - return unreadStatusesForPolicies[policyId]; - } - - return Object.values(unreadStatusesForPolicies).some((status) => status); - }, - [unreadStatusesForPolicies], - ); - - const selectPolicy = useCallback( - (option?: SimpleWorkspaceItem) => { - if (!option) { - return; - } - - const {policyID} = option; - - setActiveWorkspaceID(policyID); - - if (policyID !== activeWorkspaceID) { - Navigation.navigateWithSwitchPolicyID({policyID}); - } else { - Navigation.goBack(); - } - }, - [activeWorkspaceID, setActiveWorkspaceID], - ); - - const usersWorkspaces = useMemo(() => { - if (!policies || isEmptyObject(policies)) { - return []; - } - - return Object.values(policies) - .filter((policy) => PolicyUtils.shouldShowPolicy(policy, !!isOffline)) - .map((policy) => ({ - text: policy?.name, - policyID: policy?.id, - brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), - icons: [ - { - source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), - fallbackIcon: Expensicons.FallbackWorkspaceAvatar, - name: policy?.name, - type: CONST.ICON_TYPE_WORKSPACE, - }, - ], - boldStyle: hasUnreadData(policy?.id), - keyForList: policy?.id, - isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), - isSelected: policy?.id === activeWorkspaceID, - })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline, activeWorkspaceID]); - - const filteredAndSortedUserWorkspaces = useMemo( - () => - usersWorkspaces - .filter((policy) => policy.text?.toLowerCase().includes(searchTerm?.toLowerCase() ?? '')) - .sort((policy1, policy2) => sortWorkspacesBySelected(policy1, policy2, activeWorkspaceID)), - [searchTerm, usersWorkspaces, activeWorkspaceID], - ); - - const usersWorkspacesSectionData = useMemo( - () => ({ - data: filteredAndSortedUserWorkspaces, - shouldShow: true, - }), - [filteredAndSortedUserWorkspaces], - ); - - const everythingSection = useMemo(() => { - const option = { - reportID: '', - text: CONST.WORKSPACE_SWITCHER.NAME, - icons: [ - { - source: Expensicons.ExpensifyAppIcon, - name: CONST.WORKSPACE_SWITCHER.NAME, - type: CONST.ICON_TYPE_AVATAR, - }, - ], - brickRoadIndicator: getIndicatorTypeForPolicy(undefined), - boldStyle: hasUnreadData(undefined), - }; - - return ( - <> - - - {translate('workspace.switcher.everythingSection')} - - - - - - - ); - }, [activeWorkspaceID, getIndicatorTypeForPolicy, hasUnreadData, selectPolicy, styles, theme.textSupporting, translate]); - - const headerMessage = filteredAndSortedUserWorkspaces.length === 0 ? translate('common.noResultsFound') : ''; - - const workspacesSection = useMemo( - () => ( - <> - 0 ? [styles.mb1] : [styles.mb3])]}> - - - {translate('common.workspaces')} - - - - { - Navigation.goBack(); - interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); - }} - > - {({hovered}) => ( - - )} - - - - - {usersWorkspaces.length > 0 ? ( - = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} - initiallyFocusedOptionKey={activeWorkspaceID} - textInputAutoFocus={false} - /> - ) : ( - - )} - - ), - [ - inputCallbackRef, - setSearchTerm, - searchTerm, - selectPolicy, - styles, - theme.textSupporting, - translate, - usersWorkspaces.length, - usersWorkspacesSectionData, - activeWorkspaceID, - theme.icon, - headerMessage, - ], - ); - - return ( - - - {everythingSection} - {workspacesSection} - - ); -} - -WorkspaceSwitcherPage.displayName = 'WorkspaceSwitcherPage'; - -export default withOnyx({ - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, -})(WorkspaceSwitcherPage); diff --git a/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx new file mode 100644 index 000000000000..85e13ba4c0a5 --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import * as App from '@userActions/App'; +import CONST from '@src/CONST'; + +function WorkspacesSectionHeader() { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + {translate('common.workspaces')} + + + + { + Navigation.goBack(); + interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); + }} + > + {({hovered}) => ( + + )} + + + + ); +} + +export default WorkspacesSectionHeader; diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx new file mode 100644 index 000000000000..489c9566d6c7 --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -0,0 +1,208 @@ +import React, {useCallback, useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import type {ListItem, SectionListDataType} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; +import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import WorkspaceCardCreateAWorkspace from '@pages/workspace/card/WorkspaceCardCreateAWorkspace'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import WorkspacesSectionHeader from './WorkspacesSectionHeader'; + +type WorkspaceListItem = { + text: string; + policyID: string; + isPolicyAdmin?: boolean; + brickRoadIndicator?: BrickRoad; +} & ListItem; + +const sortWorkspacesBySelected = (workspace1: WorkspaceListItem, workspace2: WorkspaceListItem, selectedWorkspaceID: string | undefined): number => { + if (workspace1.policyID === selectedWorkspaceID) { + return -1; + } + if (workspace2.policyID === selectedWorkspaceID) { + return 1; + } + return workspace1.text?.toLowerCase().localeCompare(workspace2.text?.toLowerCase() ?? '') ?? 0; +}; + +type WorkspaceSwitcherPageOnyxProps = { + /** The list of this user's policies */ + policies: OnyxCollection; +}; + +type WorkspaceSwitcherPageProps = WorkspaceSwitcherPageOnyxProps; + +const WorkspaceCardCreateAWorkspaceInstance = ; + +function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { + const {isOffline} = useNetwork(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {translate} = useLocalize(); + const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + + const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(), []); + const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(), []); + + const getIndicatorTypeForPolicy = useCallback( + (policyId?: string) => { + if (policyId && policyId !== activeWorkspaceID) { + return brickRoadsForPolicies[policyId]; + } + + if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } + + if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + + return undefined; + }, + [activeWorkspaceID, brickRoadsForPolicies], + ); + + const hasUnreadData = useCallback( + // TO DO: Implement checking if policy has some unread data + (policyId?: string) => { + if (policyId) { + return unreadStatusesForPolicies[policyId]; + } + + return Object.values(unreadStatusesForPolicies).some((status) => status); + }, + [unreadStatusesForPolicies], + ); + + const selectPolicy = useCallback( + (option?: WorkspaceListItem) => { + if (!option) { + return; + } + + const {policyID} = option; + + setActiveWorkspaceID(policyID); + Navigation.goBack(); + if (policyID !== activeWorkspaceID) { + Navigation.navigateWithSwitchPolicyID({policyID}); + } + }, + [activeWorkspaceID, setActiveWorkspaceID], + ); + + const usersWorkspaces = useMemo(() => { + if (!policies || isEmptyObject(policies)) { + return []; + } + + return Object.values(policies) + .filter((policy) => PolicyUtils.shouldShowPolicy(policy, !!isOffline)) + .map((policy) => ({ + text: policy?.name ?? '', + policyID: policy?.id ?? '', + brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), + icons: [ + { + source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + name: policy?.name, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + boldStyle: hasUnreadData(policy?.id), + keyForList: policy?.id, + isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), + isSelected: activeWorkspaceID === policy?.id, + })); + }, [policies, isOffline, getIndicatorTypeForPolicy, hasUnreadData, activeWorkspaceID]); + + const filteredAndSortedUserWorkspaces = useMemo( + () => + usersWorkspaces + .filter((policy) => policy.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase() ?? '')) + .sort((policy1, policy2) => sortWorkspacesBySelected(policy1, policy2, activeWorkspaceID)), + [debouncedSearchTerm, usersWorkspaces, activeWorkspaceID], + ); + + const sections = useMemo(() => { + const options: Array> = [ + { + title: translate('workspace.switcher.everythingSection'), + shouldShow: true, + indexOffset: 0, + data: [ + { + text: CONST.WORKSPACE_SWITCHER.NAME, + policyID: '', + icons: [{source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}], + brickRoadIndicator: getIndicatorTypeForPolicy(undefined), + isSelected: activeWorkspaceID === undefined, + keyForList: CONST.WORKSPACE_SWITCHER.NAME, + }, + ], + }, + ]; + options.push({ + CustomSectionHeader: WorkspacesSectionHeader, + data: filteredAndSortedUserWorkspaces, + shouldShow: true, + indexOffset: 1, + }); + return options; + }, [activeWorkspaceID, filteredAndSortedUserWorkspaces, getIndicatorTypeForPolicy, translate]); + + const headerMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length ? translate('common.noResultsFound') : ''; + const shouldShowCreateWorkspace = usersWorkspaces.length === 0; + + return ( + + {({didScreenTransitionEnd}) => ( + <> + + + ListItem={UserListItem} + sections={didScreenTransitionEnd ? sections : CONST.EMPTY_ARRAY} + onSelectRow={selectPolicy} + textInputLabel={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? translate('common.search') : undefined} + textInputValue={searchTerm} + onChangeText={setSearchTerm} + headerMessage={headerMessage} + listFooterContent={shouldShowCreateWorkspace ? WorkspaceCardCreateAWorkspaceInstance : null} + initiallyFocusedOptionKey={activeWorkspaceID ?? CONST.WORKSPACE_SWITCHER.NAME} + showLoadingPlaceholder + /> + + )} + + ); +} + +WorkspaceSwitcherPage.displayName = 'WorkspaceSwitcherPage'; + +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(WorkspaceSwitcherPage); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 56828bce7847..0ccaf4d65530 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -63,7 +63,7 @@ type HeaderViewProps = HeaderViewOnyxProps & { /** The report action the transaction is tied to from the parent report */ parentReportAction: OnyxEntry; - /** The reportID of the request */ + /** The reportID of the current report */ reportID: string; }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 332e9b080558..cdd843f65fb3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -22,6 +22,7 @@ import type {CurrentReportIDContextValue} from '@components/withCurrentReportID' import withCurrentReportID from '@components/withCurrentReportID'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; @@ -157,6 +158,7 @@ function ReportScreen({ const firstRenderRef = useRef(true); const flatListRef = useRef(null); const reactionListRef = useRef(null); + const {isOffline} = useNetwork(); /** * Create a lightweight Report so as to keep the re-rendering as light as possible by * passing in only the required props. @@ -334,7 +336,10 @@ function ReportScreen({ ); } - const transactionThreadReportID = useMemo(() => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? []), [report.reportID, reportActions]); + const transactionThreadReportID = useMemo( + () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), + [report.reportID, reportActions, isOffline], + ); useEffect(() => { if (!transactionThreadReportID || !route.params.reportActionID) { @@ -363,7 +368,7 @@ function ReportScreen({ return reportIDFromRoute !== '' && !!report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); - const isLoading = !ReportUtils.isValidReportIDFromPath(reportIDFromRoute) || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); + const isLoading = !reportIDFromRoute || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); const shouldShowSkeleton = !isLinkedMessageAvailable && (isLinkingToMessage || @@ -372,19 +377,14 @@ function ReportScreen({ isLoading || (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions)); const shouldShowReportActionList = isCurrentReportLoadedFromOnyx && !isLoading; + const currentReportIDFormRoute = route.params?.reportID; // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( (): boolean => - !shouldShowSkeleton && - ((!wasReportAccessibleRef.current && - !firstRenderRef.current && - !report.reportID && - !isOptimisticDelete && - !reportMetadata?.isLoadingInitialReportActions && - !userLeavingStatus) || - shouldHideReport || - (!!reportIDFromRoute && !ReportUtils.isValidReportIDFromPath(reportIDFromRoute))), - [shouldShowSkeleton, report.reportID, isOptimisticDelete, reportMetadata?.isLoadingInitialReportActions, userLeavingStatus, shouldHideReport, reportIDFromRoute], + (!wasReportAccessibleRef.current && !firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata?.isLoadingInitialReportActions && !userLeavingStatus) || + shouldHideReport || + (!!currentReportIDFormRoute && !ReportUtils.isValidReportIDFromPath(currentReportIDFormRoute)), + [report.reportID, isOptimisticDelete, reportMetadata?.isLoadingInitialReportActions, userLeavingStatus, shouldHideReport, currentReportIDFormRoute], ); const fetchReport = useCallback(() => { @@ -514,7 +514,7 @@ function ReportScreen({ Navigation.goBack(undefined, false, true); } if (prevReport.parentReportID) { - // Prevent navigation to the Money Request Report if it is pending deletion. + // Prevent navigation to the IOU/Expense Report if it is pending deletion. const parentReport = ReportUtils.getReport(prevReport.parentReportID); if (ReportUtils.isMoneyRequestReportPendingDeletion(parentReport)) { return; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 4f9d5e2788e6..d4497b983465 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -387,9 +387,9 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (ReportActionsUtils.isActionableTrackExpense(reportAction)) { setClipboardMessage('What would you like to do with this expense?'); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { - Clipboard.setString(Localize.translateLocal('iou.heldRequest')); + Clipboard.setString(Localize.translateLocal('iou.heldExpense')); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { - Clipboard.setString(Localize.translateLocal('iou.unheldRequest')); + Clipboard.setString(Localize.translateLocal('iou.unheldExpense')); } else if (content) { setClipboardMessage( content.replace(/()(.*?)(<\/mention-user>)/gi, (match, openTag, innerContent, closeTag): string => { diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 11a84c17f9ee..ab24c6667ad4 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -125,17 +125,17 @@ function AttachmentPickerWithMenuItems({ const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, - text: translate('iou.splitBill'), + text: translate('iou.splitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, - text: translate('iou.requestMoney'), + text: translate('iou.submitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, report?.reportID ?? ''), }, [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, - text: translate('iou.sendMoney'), + text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK_EXPENSE]: { diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index a23f3bab8dc8..4b87ca6e7eea 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -390,8 +390,8 @@ function ReportActionItem({ const transactionID = (action?.originalMessage as OriginalMessageActionableTrackedExpenseWhisper['originalMessage'])?.transactionID; return [ { - text: 'actionableMentionTrackExpense.request', - key: `${action.reportActionID}-actionableMentionTrackExpense-request`, + text: 'actionableMentionTrackExpense.submit', + key: `${action.reportActionID}-actionableMentionTrackExpense-submit`, onPress: () => { ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(transactionID, report.reportID, CONST.IOU.ACTION.MOVE, action.reportActionID); }, @@ -464,7 +464,7 @@ function ReportActionItem({ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false): React.JSX.Element => { let children; - // Show the MoneyRequestPreview for when request was created, bill was split or money was sent + // Show the MoneyRequestPreview for when expense is present if ( isIOUReport(action) && action.originalMessage && @@ -478,7 +478,7 @@ function ReportActionItem({ const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0'; children = ( ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { - children = ; + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLDCOMMENT) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { - children = ; + children = ; } else { const hasBeenFlagged = ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && @@ -736,10 +736,8 @@ function ReportActionItem({ let message: TranslationPaths; if (isReversedTransaction) { message = 'parentReportAction.reversedTransaction'; - } else if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { - message = 'parentReportAction.deletedExpense'; } else { - message = 'parentReportAction.deletedRequest'; + message = 'parentReportAction.deletedExpense'; } return ( @@ -851,7 +849,7 @@ function ReportActionItem({ ); } - // For the `pay` IOU action on non-send money flow, we don't want to render anything if `isWaitingOnBankAccount` is true + // For the `pay` IOU action on non-pay expense flow, we don't want to render anything if `isWaitingOnBankAccount` is true // Otherwise, we will see two system messages informing the payee needs to add a bank account or wallet if (isIOUReport(action) && !!report?.isWaitingOnBankAccount && action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !isSendingMoney) { return null; diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 6c225421e356..a4592075aa0c 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -108,9 +108,12 @@ function ReportActionItemParentAction({ { const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? ''); - Navigation.goBack( - ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', isVisibleAction && !isOffline ? ancestor.reportAction.reportActionID : undefined), - ); + // Pop the thread report screen before navigating to the chat report. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '')); + if (isVisibleAction && !isOffline) { + // Pop the chat report screen before navigating to the linked report action. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', ancestor.reportAction.reportActionID)); + } }} parentReportAction={parentReportAction} report={ancestor.report} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 1e0dc432b3fc..4e4ebb2a017d 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -106,7 +106,7 @@ function ReportActionItemSingle({ let secondaryAvatar: Icon; const primaryDisplayName = displayName; if (displayAllActors) { - // The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice + // The ownerAccountID and actorAccountID can be the same if the a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 392b1debc960..1af93186d133 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -145,7 +145,7 @@ function ReportActionsView({ // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - // Filter out the money request actions because we don't want to show any preview actions for one-transaction reports + // Filter out the expense actions because we don't want to show any preview actions for one-transaction reports const filteredReportActions = [...allReportActions, ...filteredTransactionThreadReportActions].filter((action) => { const actionType = (action as OnyxTypes.OriginalMessageIOU).originalMessage?.type ?? ''; return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK && !ReportActionsUtils.isSentMoneyReportAction(action); @@ -420,11 +420,11 @@ function ReportActionsView({ }; }, [isTheFirstReportActionIsLinked]); - // When we are offline before opening a money request report, - // the total of the report and sometimes the money request aren't displayed because these actions aren't returned until `OpenReport` API is complete. + // When we are offline before opening an IOU/Expense report, + // the total of the report and sometimes the expense aren't displayed because these actions aren't returned until `OpenReport` API is complete. // We generate a fake created action here if it doesn't exist to display the total whenever possible because the total just depends on report data - // and we also generate a money request action if the number of money requests in reportActions is less than the total number of money requests - // to display at least one money request action to match the total data. + // and we also generate an expense action if the number of expenses in reportActions is less than the total number of expenses + // to display at least one expense action to match the total data. const reportActionsToDisplay = useMemo(() => { if (!ReportUtils.isMoneyRequestReport(report) || !reportActions.length) { return reportActions; diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index bd143f9ef196..04fbd0308390 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -89,10 +89,10 @@ function ReportFooter({ /** * Matching task rule by group * Group 1: Start task rule with [] - * Group 2: Optional email group between \s+....\s* start rule with @+valid email + * Group 2: Optional email group between \s+....\s* start rule with @+valid email or short mention * Group 3: Title is remaining characters */ - const taskRegex = /^\[\]\s+(?:@([^\s@]+@[\w.-]+\.[a-zA-Z]{2,}))?\s*([\s\S]*)/; + const taskRegex = /^\[\]\s+(?:@([^\s@]+(?:@\w+\.\w+)?))?\s*([\s\S]*)/; const match = text.match(taskRegex); if (!match) { @@ -102,10 +102,13 @@ function ReportFooter({ if (!title) { return false; } - const email = match[1] ? match[1].trim() : undefined; + + const mention = match[1] ? match[1].trim() : undefined; + const mentionWithDomain = ReportUtils.addDomainToShortMention(mention ?? '') ?? mention; + let assignee: OnyxTypes.PersonalDetails | EmptyObject = {}; - if (email) { - assignee = Object.values(allPersonalDetails).find((value) => value?.login === email) ?? {}; + if (mentionWithDomain) { + assignee = Object.values(allPersonalDetails).find((value) => value?.login === mentionWithDomain) ?? {}; } Task.createTaskAndNavigate(report.reportID, title, '', assignee?.login ?? '', assignee.accountID, undefined, report.policyID); return true; diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index f8acf63832b7..edbe2c665752 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -31,7 +31,12 @@ function ThreadDivider({ancestor}: ThreadDividerProps) { { const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? ''); - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', isVisibleAction && !isOffline ? ancestor.reportAction.reportActionID : undefined)); + // Pop the thread report screen before navigating to the chat report. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '')); + if (isVisibleAction && !isOffline) { + // Pop the chat report screen before navigating to the linked report action. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.parentReportID ?? '', ancestor.reportAction.reportActionID)); + } }} accessibilityLabel={translate('threads.thread')} role={CONST.ROLE.BUTTON} diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 212bf93166a1..b55fecf48ffb 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -294,12 +294,12 @@ function FloatingActionButtonAndPopover( : []), { icon: Expensicons.MoneyCircle, - text: translate('iou.requestMoney'), + text: translate('iou.submitExpense'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest( CONST.IOU.TYPE.REQUEST, - // When starting to create a money request from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used // for all of the routes in the creation flow. ReportUtils.generateReportID(), ), @@ -307,12 +307,12 @@ function FloatingActionButtonAndPopover( }, { icon: Expensicons.Send, - text: translate('iou.sendMoney'), + text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest( CONST.IOU.TYPE.SEND, - // When starting to create a send money request from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // When starting to pay someone from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used // for all of the routes in the creation flow. ReportUtils.generateReportID(), ), diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 2a5cba810759..8f016ec0d8d9 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -91,12 +91,12 @@ function HoldReasonPage({route}: HoldReasonPageProps) { testID={HoldReasonPage.displayName} > reportActions?.[route.params.reportActionID] ?? ({} as ReportAction), [reportActions, route.params.reportActionID]); const participantAccountIDs = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? reportAction?.originalMessage.participantAccountIDs ?? [] : []; - // In case this is workspace split bill, we manually add the workspace as the second participant of the split bill + // In case this is workspace split expense, we manually add the workspace as the second participant of the split expense // because we don't save any accountID in the report action's originalMessage other than the payee's accountID let participants: Array; if (ReportUtils.isPolicyExpenseChat(report)) { diff --git a/src/pages/iou/propTypes/index.js b/src/pages/iou/propTypes/index.js index a03ed65dda9c..46abafac23e4 100644 --- a/src/pages/iou/propTypes/index.js +++ b/src/pages/iou/propTypes/index.js @@ -3,16 +3,16 @@ import participantPropTypes from '@components/participantPropTypes'; import CONST from '@src/CONST'; const iouPropTypes = PropTypes.shape({ - /** ID (iouType + reportID) of the request */ + /** ID (iouType + reportID) of the expense */ id: PropTypes.string, - /** Amount of the request */ + /** Amount of the expense */ amount: PropTypes.number, - /** Currency of the request */ + /** Currency of the expense */ currency: PropTypes.string, - /** Description of the request */ + /** Description of the expense */ comment: PropTypes.string, /** The merchant name */ @@ -21,16 +21,16 @@ const iouPropTypes = PropTypes.shape({ /** The category name */ category: PropTypes.string, - /** Whether the request is billable */ + /** Whether the expense is billable */ billable: PropTypes.bool, /** The tag */ tag: PropTypes.string, - /** Date that the request was created */ + /** Date that the expense was created */ created: PropTypes.string, - /** The path to an image of the receipt attached to the request */ + /** The path to an image of the receipt attached to the expense */ receiptPath: PropTypes.string, /** List of the participants */ diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 6c69598893c5..8d64598ed838 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -63,9 +63,9 @@ function IOURequestStartPage({ const navigation = useNavigation(); const [isDraggingOver, setIsDraggingOver] = useState(false); const tabTitles = { - [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), - [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), - [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), + [CONST.IOU.TYPE.REQUEST]: translate('iou.submitExpense'), + [CONST.IOU.TYPE.SEND]: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), + [CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'), [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); @@ -88,7 +88,7 @@ function IOURequestStartPage({ }, []), ); - // Clear out the temporary money request if the reportID in the URL has changed from the transaction's reportID + // Clear out the temporary expense if the reportID in the URL has changed from the transaction's reportID useEffect(() => { if (transaction?.reportID === reportID) { return; @@ -100,7 +100,7 @@ function IOURequestStartPage({ const isExpenseReport = ReportUtils.isExpenseReport(report); const shouldDisplayDistanceRequest = !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; - // Allow the user to create the request if we are creating the request in global menu or the report can create the request + // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); const navigateBack = () => { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 87883972f84f..95cb043122d4 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -50,10 +50,10 @@ const propTypes = { }), ), - /** The type of IOU report, i.e. bill, request, send */ + /** The type of IOU report, i.e. split, request, send, track */ iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired, - /** The request type, ie. manual, scan, distance */ + /** The expense type, ie. manual, scan, distance */ iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, /** The action of the IOU, i.e. create, split, move */ @@ -107,8 +107,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF participants, CONST.EXPENSIFY_EMAILS, - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. + // If we are using this component in the "Submit expense" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to submit an expense from their admin on their own Workspace Chat. iouType === CONST.IOU.TYPE.REQUEST && action !== CONST.IOU.ACTION.MOVE, (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action), @@ -182,7 +182,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF ]); /** - * Adds a single participant to the request + * Adds a single participant to the expense * * @param {Object} option */ @@ -256,7 +256,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], ); - // Right now you can't split a request with a workspace and other additional participants + // Right now you can't split an expense with a workspace and other additional participants // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); @@ -304,14 +304,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF )} {!!participants.length && (